Skip to content

Commit d2b82b5

Browse files
gargsaumyaSaumya Gargbewithgaurav
authored
FEAT: isolate native handles using smart pointers, eliminate raw pointer access from Python
* initial changes for encapsulating raw ODBC handles * refactor: isolate native ODBC handles using smart pointers, eliminate raw pointer access from Python * resolved review comments * added newline for test in ddbc_bindings.cpp * fix sqlhandle * FIX: Fixed Pytests and corner cases * random * line endings * added back PR template * changed pytest structure * pipeline steps change for coverage * removed pytest-azurepipelines from dependency * fixed coverage generation * cpp to use lf instead of crlf * cleanup * python owns memory * python owns memory - pipeline reverted * cleanup and comments --------- Co-authored-by: Saumya Garg <gargsaumya@microsoft.com> Co-authored-by: Gaurav Sharma <sharmag@microsoft.com>
1 parent e146e7e commit d2b82b5

File tree

12 files changed

+265
-175
lines changed

12 files changed

+265
-175
lines changed

.gitignore

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ mssql_python/pybind/build/
33

44
mssql_python/pybind/pymsbuild/build/
55

6-
# Ignore pyd file
7-
mssql_python/ddbc_bindings.pyd
6+
# Ignore all pyd files
7+
**.pyd
88

99
# Ignore pycache files and folders
1010
__pycache__/
@@ -26,6 +26,11 @@ mssql_python/.vs
2626
test-*.xml
2727
**/test-**.xml
2828

29+
# Ignore coverage files
30+
coverage.xml
31+
.coverage
32+
.coverage.*
33+
2934
# Ignore the build & mssql_python.egg-info directories
3035
build/
3136
mssql_python.egg-info/

eng/pipelines/test-pipeline.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,5 @@ jobs:
6565
- task: PublishCodeCoverageResults@1
6666
inputs:
6767
codeCoverageTool: 'Cobertura'
68-
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
68+
summaryFileLocation: 'coverage.xml'
6969
displayName: 'Publish code coverage results'

mssql_python/connection.py

Lines changed: 53 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ def __init__(self, connection_str: str, autocommit: bool = False, **kwargs) -> N
5353
preparing it for further operations such as connecting to the
5454
database, executing queries, etc.
5555
"""
56-
self.henv = ctypes.c_void_p()
57-
self.hdbc = ctypes.c_void_p()
56+
self.henv = None
57+
self.hdbc = None
5858
self.connection_str = self._construct_connection_string(
5959
connection_str, **kwargs
6060
)
@@ -95,6 +95,15 @@ def _construct_connection_string(self, connection_str: str, **kwargs) -> str:
9595
conn_str += f"{key}={value};"
9696
return conn_str
9797

98+
def _is_closed(self) -> bool:
99+
"""
100+
Check if the connection is closed.
101+
102+
Returns:
103+
bool: True if the connection is closed, False otherwise.
104+
"""
105+
return self.hdbc is None
106+
98107
def _initializer(self) -> None:
99108
"""
100109
Initialize the environment and connection handles.
@@ -113,52 +122,48 @@ def _allocate_environment_handle(self):
113122
"""
114123
Allocate the environment handle.
115124
"""
116-
ret = ddbc_bindings.DDBCSQLAllocHandle(
125+
ret, handle = ddbc_bindings.DDBCSQLAllocHandle(
117126
ddbc_sql_const.SQL_HANDLE_ENV.value, # SQL environment handle type
118-
0, # SQL input handle
119-
ctypes.cast(
120-
ctypes.pointer(self.henv), ctypes.c_void_p
121-
).value, # SQL output handle pointer
127+
None
122128
)
123-
check_error(ddbc_sql_const.SQL_HANDLE_ENV.value, self.henv.value, ret)
129+
check_error(ddbc_sql_const.SQL_HANDLE_ENV.value, handle, ret)
130+
self.henv = handle
124131

125132
def _set_environment_attributes(self):
126133
"""
127134
Set the environment attributes.
128135
"""
129136
ret = ddbc_bindings.DDBCSQLSetEnvAttr(
130-
self.henv.value, # Environment handle
137+
self.henv, # Use the wrapper class
131138
ddbc_sql_const.SQL_ATTR_DDBC_VERSION.value, # Attribute
132139
ddbc_sql_const.SQL_OV_DDBC3_80.value, # String Length
133140
0, # Null-terminated string
134141
)
135-
check_error(ddbc_sql_const.SQL_HANDLE_ENV.value, self.henv.value, ret)
142+
check_error(ddbc_sql_const.SQL_HANDLE_ENV.value, self.henv, ret)
136143

137144
def _allocate_connection_handle(self):
138145
"""
139146
Allocate the connection handle.
140147
"""
141-
ret = ddbc_bindings.DDBCSQLAllocHandle(
148+
ret, handle = ddbc_bindings.DDBCSQLAllocHandle(
142149
ddbc_sql_const.SQL_HANDLE_DBC.value, # SQL connection handle type
143-
self.henv.value, # SQL environment handle
144-
ctypes.cast(
145-
ctypes.pointer(self.hdbc), ctypes.c_void_p
146-
).value, # SQL output handle pointer
150+
self.henv
147151
)
148-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
152+
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, handle, ret)
153+
self.hdbc = handle
149154

150155
def _set_connection_attributes(self):
151156
"""
152157
Set the connection attributes before connecting.
153158
"""
154159
if self.autocommit:
155160
ret = ddbc_bindings.DDBCSQLSetConnectAttr(
156-
self.hdbc.value,
161+
self.hdbc, # Using the wrapper class
157162
ddbc_sql_const.SQL_ATTR_AUTOCOMMIT.value,
158163
ddbc_sql_const.SQL_AUTOCOMMIT_ON.value,
159-
0,
164+
0
160165
)
161-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
166+
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
162167

163168
def _connect_to_db(self) -> None:
164169
"""
@@ -176,11 +181,11 @@ def _connect_to_db(self) -> None:
176181
if ENABLE_LOGGING:
177182
logger.info("Connecting to the database")
178183
ret = ddbc_bindings.DDBCSQLDriverConnect(
179-
self.hdbc.value, # Connection handle
184+
self.hdbc, # Connection handle (wrapper)
180185
0, # Window handle
181186
self.connection_str, # Connection string
182187
)
183-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
188+
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
184189
if ENABLE_LOGGING:
185190
logger.info("Connection established successfully.")
186191

@@ -192,11 +197,11 @@ def autocommit(self) -> bool:
192197
bool: True if autocommit is enabled, False otherwise.
193198
"""
194199
autocommit_mode = ddbc_bindings.DDBCSQLGetConnectionAttr(
195-
self.hdbc.value, # Connection handle
200+
self.hdbc, # Connection handle (wrapper)
196201
ddbc_sql_const.SQL_ATTR_AUTOCOMMIT.value, # Attribute
197202
)
198203
check_error(
199-
ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, autocommit_mode
204+
ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, autocommit_mode
200205
)
201206
return autocommit_mode == ddbc_sql_const.SQL_AUTOCOMMIT_ON.value
202207

@@ -212,7 +217,7 @@ def autocommit(self, value: bool) -> None:
212217
DatabaseError: If there is an error while setting the autocommit mode.
213218
"""
214219
ret = ddbc_bindings.DDBCSQLSetConnectAttr(
215-
self.hdbc.value, # Connection handle
220+
self.hdbc, # Connection handle
216221
ddbc_sql_const.SQL_ATTR_AUTOCOMMIT.value, # Attribute
217222
(
218223
ddbc_sql_const.SQL_AUTOCOMMIT_ON.value
@@ -221,7 +226,7 @@ def autocommit(self, value: bool) -> None:
221226
), # Value
222227
0, # String length
223228
)
224-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
229+
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
225230
self._autocommit = value
226231
if ENABLE_LOGGING:
227232
logger.info("Autocommit mode set to %s.", value)
@@ -253,6 +258,9 @@ def cursor(self) -> Cursor:
253258
DatabaseError: If there is an error while creating the cursor.
254259
InterfaceError: If there is an error related to the database interface.
255260
"""
261+
if self._is_closed():
262+
# Cannot create a cursor if the connection is closed
263+
raise Exception("Connection is closed. Cannot create cursor.")
256264
return Cursor(self)
257265

258266
def commit(self) -> None:
@@ -267,13 +275,17 @@ def commit(self) -> None:
267275
Raises:
268276
DatabaseError: If there is an error while committing the transaction.
269277
"""
278+
if self._is_closed():
279+
# Cannot commit if the connection is closed
280+
raise Exception("Connection is closed. Cannot commit.")
281+
270282
# Commit the current transaction
271283
ret = ddbc_bindings.DDBCSQLEndTran(
272284
ddbc_sql_const.SQL_HANDLE_DBC.value, # Handle type
273-
self.hdbc.value, # Connection handle
285+
self.hdbc, # Connection handle (wrapper)
274286
ddbc_sql_const.SQL_COMMIT.value, # Commit the transaction
275287
)
276-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
288+
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
277289
if ENABLE_LOGGING:
278290
logger.info("Transaction committed successfully.")
279291

@@ -288,13 +300,17 @@ def rollback(self) -> None:
288300
Raises:
289301
DatabaseError: If there is an error while rolling back the transaction.
290302
"""
303+
if self._is_closed():
304+
# Cannot roll back if the connection is closed
305+
raise Exception("Connection is closed. Cannot roll back.")
306+
291307
# Roll back the current transaction
292308
ret = ddbc_bindings.DDBCSQLEndTran(
293309
ddbc_sql_const.SQL_HANDLE_DBC.value, # Handle type
294-
self.hdbc.value, # Connection handle
310+
self.hdbc, # Connection handle (wrapper)
295311
ddbc_sql_const.SQL_ROLLBACK.value, # Roll back the transaction
296312
)
297-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
313+
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
298314
if ENABLE_LOGGING:
299315
logger.info("Transaction rolled back successfully.")
300316

@@ -311,15 +327,16 @@ def close(self) -> None:
311327
Raises:
312328
DatabaseError: If there is an error while closing the connection.
313329
"""
330+
if self._is_closed():
331+
# Connection is already closed
332+
return
314333
# Disconnect from the database
315-
ret = ddbc_bindings.DDBCSQLDisconnect(self.hdbc.value)
316-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
334+
ret = ddbc_bindings.DDBCSQLDisconnect(self.hdbc)
335+
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
317336

318-
# Free the connection handle
319-
ret = ddbc_bindings.DDBCSQLFreeHandle(
320-
ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value
321-
)
322-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
337+
# Set the reference to None to trigger destructor
338+
self.hdbc.free()
339+
self.hdbc = None
323340

324341
if ENABLE_LOGGING:
325342
logger.info("Connection closed successfully.")

mssql_python/cursor.py

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ def __init__(self, connection) -> None:
4848
Args:
4949
connection: Database connection object.
5050
"""
51+
if connection.hdbc is None:
52+
raise Exception("Connection is closed. Cannot create a cursor.")
5153
self.connection = connection
5254
# self.connection.autocommit = False
53-
self.hstmt = ctypes.c_void_p()
55+
self.hstmt = None
5456
self._initialize_cursor()
5557
self.description = None
5658
self.rowcount = -1
@@ -415,22 +417,22 @@ def _allocate_statement_handle(self):
415417
"""
416418
Allocate the DDBC statement handle.
417419
"""
418-
ret = ddbc_bindings.DDBCSQLAllocHandle(
420+
ret, handle = ddbc_bindings.DDBCSQLAllocHandle(
419421
ddbc_sql_const.SQL_HANDLE_STMT.value,
420-
self.connection.hdbc.value,
421-
ctypes.cast(ctypes.pointer(self.hstmt), ctypes.c_void_p).value,
422+
self.connection.hdbc
422423
)
423-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
424+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, handle, ret)
425+
self.hstmt = handle
424426

425427
def _reset_cursor(self) -> None:
426428
"""
427429
Reset the DDBC statement handle.
428430
"""
429-
# Free the existing statement handle
430-
if self.hstmt.value:
431-
ddbc_bindings.DDBCSQLFreeHandle(
432-
ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value
433-
)
431+
if self.hstmt:
432+
self.hstmt.free() # Free the existing statement handle
433+
self.hstmt = None
434+
if ENABLE_LOGGING:
435+
logger.debug("SQLFreeHandle succeeded")
434436
# Reinitialize the statement handle
435437
self._initialize_cursor()
436438

@@ -442,15 +444,13 @@ def close(self) -> None:
442444
Error: If any operation is attempted with the cursor after it is closed.
443445
"""
444446
if self.closed:
445-
raise RuntimeError("Cursor is already closed.")
446-
447-
if self.hstmt.value:
448-
ret = ddbc_bindings.DDBCSQLFreeHandle(
449-
ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value
450-
)
451-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
452-
self.hstmt.value = None
447+
raise Exception("Cursor is already closed.")
453448

449+
if self.hstmt:
450+
self.hstmt.free()
451+
self.hstmt = None
452+
if ENABLE_LOGGING:
453+
logger.debug("SQLFreeHandle succeeded")
454454
self.closed = True
455455

456456
def _check_closed(self):
@@ -461,7 +461,7 @@ def _check_closed(self):
461461
Error: If the cursor is closed.
462462
"""
463463
if self.closed:
464-
raise RuntimeError("Operation cannot be performed: the cursor is closed.")
464+
raise Exception("Operation cannot be performed: the cursor is closed.")
465465

466466
def _create_parameter_types_list(self, parameter, param_info, parameters_list, i):
467467
"""
@@ -489,8 +489,8 @@ def _initialize_description(self):
489489
Initialize the description attribute using SQLDescribeCol.
490490
"""
491491
col_metadata = []
492-
ret = ddbc_bindings.DDBCSQLDescribeCol(self.hstmt.value, col_metadata)
493-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
492+
ret = ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, col_metadata)
493+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
494494

495495
self.description = [
496496
(
@@ -602,19 +602,19 @@ def execute(
602602
)
603603

604604
ret = ddbc_bindings.DDBCSQLExecute(
605-
self.hstmt.value,
605+
self.hstmt,
606606
operation,
607607
parameters,
608608
parameters_type,
609609
self.is_stmt_prepared,
610610
use_prepare,
611611
)
612-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
612+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
613613
self.last_executed_stmt = operation
614614

615615
# Update rowcount after execution
616616
# TODO: rowcount return code from SQL needs to be handled
617-
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt.value)
617+
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
618618

619619
# Initialize description after execution
620620
self._initialize_description()
@@ -664,8 +664,8 @@ def fetchone(self) -> Union[None, tuple]:
664664
self._check_closed() # Check if the cursor is closed
665665

666666
row = []
667-
ret = ddbc_bindings.DDBCSQLFetchOne(self.hstmt.value, row)
668-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
667+
ret = ddbc_bindings.DDBCSQLFetchOne(self.hstmt, row)
668+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
669669
if ret == ddbc_sql_const.SQL_NO_DATA.value:
670670
return None
671671
return list(row)
@@ -690,8 +690,8 @@ def fetchmany(self, size: int = None) -> List[tuple]:
690690

691691
# Fetch the next set of rows
692692
rows = []
693-
ret = ddbc_bindings.DDBCSQLFetchMany(self.hstmt.value, rows, size)
694-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
693+
ret = ddbc_bindings.DDBCSQLFetchMany(self.hstmt, rows, size)
694+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
695695
if ret == ddbc_sql_const.SQL_NO_DATA.value:
696696
return []
697697
return rows
@@ -710,8 +710,8 @@ def fetchall(self) -> List[tuple]:
710710

711711
# Fetch all remaining rows
712712
rows = []
713-
ret = ddbc_bindings.DDBCSQLFetchAll(self.hstmt.value, rows)
714-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
713+
ret = ddbc_bindings.DDBCSQLFetchAll(self.hstmt, rows)
714+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
715715
if ret != ddbc_sql_const.SQL_NO_DATA.value:
716716
return []
717717
return list(rows)
@@ -729,8 +729,8 @@ def nextset(self) -> Union[bool, None]:
729729
self._check_closed() # Check if the cursor is closed
730730

731731
# Skip to the next result set
732-
ret = ddbc_bindings.DDBCSQLMoreResults(self.hstmt.value)
733-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)
732+
ret = ddbc_bindings.DDBCSQLMoreResults(self.hstmt)
733+
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
734734
if ret == ddbc_sql_const.SQL_NO_DATA.value:
735735
return False
736736
return True

0 commit comments

Comments
 (0)