Skip to content

Commit fc99220

Browse files
authored
Merge pull request #54 from microsoft/saumya/conn_implementation
FEAT: Implementation of C++ Connection Class API
2 parents 316f018 + e97b287 commit fc99220

File tree

7 files changed

+236
-495
lines changed

7 files changed

+236
-495
lines changed

mssql_python/connection.py

Lines changed: 10 additions & 232 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,12 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef
5353
preparing it for further operations such as connecting to the
5454
database, executing queries, etc.
5555
"""
56-
self.henv = None
57-
self.hdbc = None
5856
self.connection_str = self._construct_connection_string(
5957
connection_str, **kwargs
6058
)
61-
self._attrs_before = attrs_before
62-
self._autocommit = autocommit # Initialize _autocommit before calling _initializer
63-
self._initializer()
59+
self._attrs_before = attrs_before or {}
60+
self._conn = ddbc_bindings.Connection(self.connection_str, autocommit)
61+
self._conn.connect(self._attrs_before)
6462
self.setautocommit(autocommit)
6563

6664
def _construct_connection_string(self, connection_str: str = "", **kwargs) -> str:
@@ -100,193 +98,15 @@ def _construct_connection_string(self, connection_str: str = "", **kwargs) -> st
10098
logger.info("Final connection string: %s", conn_str)
10199

102100
return conn_str
103-
104-
def _is_closed(self) -> bool:
105-
"""
106-
Check if the connection is closed.
107-
108-
Returns:
109-
bool: True if the connection is closed, False otherwise.
110-
"""
111-
return self.hdbc is None
112101

113-
def _initializer(self) -> None:
114-
"""
115-
Initialize the environment and connection handles.
116-
117-
This method is responsible for setting up the environment and connection
118-
handles, allocating memory for them, and setting the necessary attributes.
119-
It should be called before establishing a connection to the database.
120-
"""
121-
self._allocate_environment_handle()
122-
self._set_environment_attributes()
123-
self._allocate_connection_handle()
124-
if self._attrs_before != {}:
125-
self._apply_attrs_before() # Apply pre-connection attributes
126-
if self._autocommit:
127-
self._set_connection_attributes(
128-
ddbc_sql_const.SQL_ATTR_AUTOCOMMIT.value,
129-
ddbc_sql_const.SQL_AUTOCOMMIT_ON.value,
130-
)
131-
self._connect_to_db()
132-
133-
def _apply_attrs_before(self):
134-
"""
135-
Apply specific pre-connection attributes.
136-
Currently, this method only processes an attribute with key 1256 (e.g., SQL_COPT_SS_ACCESS_TOKEN)
137-
if present in `self._attrs_before`. Other attributes are ignored.
138-
139-
Returns:
140-
bool: True.
141-
"""
142-
143-
if ENABLE_LOGGING:
144-
logger.info("Attempting to apply pre-connection attributes (attrs_before): %s", self._attrs_before)
145-
146-
if not isinstance(self._attrs_before, dict):
147-
if self._attrs_before is not None and ENABLE_LOGGING:
148-
logger.warning(
149-
f"_attrs_before is of type {type(self._attrs_before).__name__}, "
150-
f"expected dict. Skipping attribute application."
151-
)
152-
elif self._attrs_before is None and ENABLE_LOGGING:
153-
logger.debug("_attrs_before is None. No pre-connection attributes to apply.")
154-
return True # Exit if _attrs_before is not a dictionary or is None
155-
156-
for key, value in self._attrs_before.items():
157-
ikey = None
158-
if isinstance(key, int):
159-
ikey = key
160-
elif isinstance(key, str) and key.isdigit():
161-
try:
162-
ikey = int(key)
163-
except ValueError:
164-
if ENABLE_LOGGING:
165-
logger.debug(
166-
f"Skipping attribute with key '{key}' in attrs_before: "
167-
f"could not convert string to int."
168-
)
169-
continue # Skip if string key is not a valid integer
170-
else:
171-
if ENABLE_LOGGING:
172-
logger.debug(
173-
f"Skipping attribute with key '{key}' in attrs_before due to "
174-
f"unsupported key type: {type(key).__name__}. Expected int or string representation of an int."
175-
)
176-
continue # Skip keys that are not int or string representation of an int
177-
178-
if ikey == ddbc_sql_const.SQL_COPT_SS_ACCESS_TOKEN.value:
179-
if ENABLE_LOGGING:
180-
logger.info(
181-
f"Found attribute {ddbc_sql_const.SQL_COPT_SS_ACCESS_TOKEN.value}. Attempting to set it."
182-
)
183-
self._set_connection_attributes(ikey, value)
184-
if ENABLE_LOGGING:
185-
logger.info(
186-
f"Call to set attribute {ddbc_sql_const.SQL_COPT_SS_ACCESS_TOKEN.value} with value '{value}' completed."
187-
)
188-
# If you expect only one such key, you could add 'break' here.
189-
else:
190-
if ENABLE_LOGGING:
191-
logger.debug(
192-
f"Ignoring attribute with key '{key}' (resolved to {ikey}) in attrs_before "
193-
f"as it is not the target attribute ({ddbc_sql_const.SQL_COPT_SS_ACCESS_TOKEN.value})."
194-
)
195-
return True
196-
197-
def _allocate_environment_handle(self):
198-
"""
199-
Allocate the environment handle.
200-
"""
201-
ret, handle = ddbc_bindings.DDBCSQLAllocHandle(
202-
ddbc_sql_const.SQL_HANDLE_ENV.value, # SQL environment handle type
203-
None
204-
)
205-
check_error(ddbc_sql_const.SQL_HANDLE_ENV.value, handle, ret)
206-
self.henv = handle
207-
208-
def _set_environment_attributes(self):
209-
"""
210-
Set the environment attributes.
211-
"""
212-
ret = ddbc_bindings.DDBCSQLSetEnvAttr(
213-
self.henv, # Use the wrapper class
214-
ddbc_sql_const.SQL_ATTR_DDBC_VERSION.value, # Attribute
215-
ddbc_sql_const.SQL_OV_DDBC3_80.value, # String Length
216-
0, # Null-terminated string
217-
)
218-
check_error(ddbc_sql_const.SQL_HANDLE_ENV.value, self.henv, ret)
219-
220-
def _allocate_connection_handle(self):
221-
"""
222-
Allocate the connection handle.
223-
"""
224-
ret, handle = ddbc_bindings.DDBCSQLAllocHandle(
225-
ddbc_sql_const.SQL_HANDLE_DBC.value, # SQL connection handle type
226-
self.henv
227-
)
228-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, handle, ret)
229-
self.hdbc = handle
230-
231-
def _set_connection_attributes(self, ikey: int, ivalue: any) -> None:
232-
"""
233-
Set the connection attributes before connecting.
234-
235-
Args:
236-
ikey (int): The attribute key to set.
237-
ivalue (Any): The value to set for the attribute. Can be bytes, bytearray, int, or unicode.
238-
vallen (int): The length of the value.
239-
240-
Raises:
241-
DatabaseError: If there is an error while setting the connection attribute.
242-
"""
243-
244-
ret = ddbc_bindings.DDBCSQLSetConnectAttr(
245-
self.hdbc, # Connection handle
246-
ikey, # Attribute
247-
ivalue, # Value
248-
)
249-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
250-
251-
def _connect_to_db(self) -> None:
252-
"""
253-
Establish a connection to the database.
254-
255-
This method is responsible for creating a connection to the specified database.
256-
It does not take any arguments and does not return any value. The connection
257-
details such as database name, user credentials, host, and port should be
258-
configured within the class or passed during the class instantiation.
259-
260-
Raises:
261-
DatabaseError: If there is an error while trying to connect to the database.
262-
InterfaceError: If there is an error related to the database interface.
263-
"""
264-
if ENABLE_LOGGING:
265-
logger.info("Connecting to the database")
266-
ret = ddbc_bindings.DDBCSQLDriverConnect(
267-
self.hdbc, # Connection handle (wrapper)
268-
0, # Window handle
269-
self.connection_str, # Connection string
270-
)
271-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
272-
if ENABLE_LOGGING:
273-
logger.info("Connection established successfully.")
274-
275102
@property
276103
def autocommit(self) -> bool:
277104
"""
278105
Return the current autocommit mode of the connection.
279106
Returns:
280107
bool: True if autocommit is enabled, False otherwise.
281108
"""
282-
autocommit_mode = ddbc_bindings.DDBCSQLGetConnectionAttr(
283-
self.hdbc, # Connection handle (wrapper)
284-
ddbc_sql_const.SQL_ATTR_AUTOCOMMIT.value, # Attribute
285-
)
286-
check_error(
287-
ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, autocommit_mode
288-
)
289-
return autocommit_mode == ddbc_sql_const.SQL_AUTOCOMMIT_ON.value
109+
return self._conn.get_autocommit()
290110

291111
@autocommit.setter
292112
def autocommit(self, value: bool) -> None:
@@ -296,20 +116,8 @@ def autocommit(self, value: bool) -> None:
296116
value (bool): True to enable autocommit, False to disable it.
297117
Returns:
298118
None
299-
Raises:
300-
DatabaseError: If there is an error while setting the autocommit mode.
301119
"""
302-
ret = ddbc_bindings.DDBCSQLSetConnectAttr(
303-
self.hdbc, # Connection handle
304-
ddbc_sql_const.SQL_ATTR_AUTOCOMMIT.value, # Attribute
305-
(
306-
ddbc_sql_const.SQL_AUTOCOMMIT_ON.value
307-
if value
308-
else ddbc_sql_const.SQL_AUTOCOMMIT_OFF.value
309-
), # Value
310-
)
311-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
312-
self._autocommit = value
120+
self.setautocommit(value)
313121
if ENABLE_LOGGING:
314122
logger.info("Autocommit mode set to %s.", value)
315123

@@ -323,7 +131,7 @@ def setautocommit(self, value: bool = True) -> None:
323131
Raises:
324132
DatabaseError: If there is an error while setting the autocommit mode.
325133
"""
326-
self.autocommit = value
134+
self._conn.set_autocommit(value)
327135

328136
def cursor(self) -> Cursor:
329137
"""
@@ -340,9 +148,6 @@ def cursor(self) -> Cursor:
340148
DatabaseError: If there is an error while creating the cursor.
341149
InterfaceError: If there is an error related to the database interface.
342150
"""
343-
if self._is_closed():
344-
# Cannot create a cursor if the connection is closed
345-
raise Exception("Connection is closed. Cannot create cursor.")
346151
return Cursor(self)
347152

348153
def commit(self) -> None:
@@ -357,17 +162,8 @@ def commit(self) -> None:
357162
Raises:
358163
DatabaseError: If there is an error while committing the transaction.
359164
"""
360-
if self._is_closed():
361-
# Cannot commit if the connection is closed
362-
raise Exception("Connection is closed. Cannot commit.")
363-
364165
# Commit the current transaction
365-
ret = ddbc_bindings.DDBCSQLEndTran(
366-
ddbc_sql_const.SQL_HANDLE_DBC.value, # Handle type
367-
self.hdbc, # Connection handle (wrapper)
368-
ddbc_sql_const.SQL_COMMIT.value, # Commit the transaction
369-
)
370-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
166+
self._conn.commit()
371167
if ENABLE_LOGGING:
372168
logger.info("Transaction committed successfully.")
373169

@@ -382,17 +178,8 @@ def rollback(self) -> None:
382178
Raises:
383179
DatabaseError: If there is an error while rolling back the transaction.
384180
"""
385-
if self._is_closed():
386-
# Cannot roll back if the connection is closed
387-
raise Exception("Connection is closed. Cannot roll back.")
388-
389181
# Roll back the current transaction
390-
ret = ddbc_bindings.DDBCSQLEndTran(
391-
ddbc_sql_const.SQL_HANDLE_DBC.value, # Handle type
392-
self.hdbc, # Connection handle (wrapper)
393-
ddbc_sql_const.SQL_ROLLBACK.value, # Roll back the transaction
394-
)
395-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
182+
self._conn.rollback()
396183
if ENABLE_LOGGING:
397184
logger.info("Transaction rolled back successfully.")
398185

@@ -409,16 +196,7 @@ def close(self) -> None:
409196
Raises:
410197
DatabaseError: If there is an error while closing the connection.
411198
"""
412-
if self._is_closed():
413-
# Connection is already closed
414-
return
415-
# Disconnect from the database
416-
ret = ddbc_bindings.DDBCSQLDisconnect(self.hdbc)
417-
check_error(ddbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc, ret)
418-
419-
# Set the reference to None to trigger destructor
420-
self.hdbc.free()
421-
self.hdbc = None
422-
199+
# Close the connection
200+
self._conn.close()
423201
if ENABLE_LOGGING:
424202
logger.info("Connection closed successfully.")

mssql_python/cursor.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ 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.")
5351
self.connection = connection
5452
# self.connection.autocommit = False
5553
self.hstmt = None
@@ -417,19 +415,14 @@ def _allocate_statement_handle(self):
417415
"""
418416
Allocate the DDBC statement handle.
419417
"""
420-
ret, handle = ddbc_bindings.DDBCSQLAllocHandle(
421-
ddbc_sql_const.SQL_HANDLE_STMT.value,
422-
self.connection.hdbc
423-
)
424-
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, handle, ret)
425-
self.hstmt = handle
418+
self.hstmt = self.connection._conn.alloc_statement_handle()
426419

427420
def _reset_cursor(self) -> None:
428421
"""
429422
Reset the DDBC statement handle.
430423
"""
431424
if self.hstmt:
432-
self.hstmt.free() # Free the existing statement handle
425+
self.hstmt.free()
433426
self.hstmt = None
434427
if ENABLE_LOGGING:
435428
logger.debug("SQLFreeHandle succeeded")
@@ -557,7 +550,6 @@ def execute(
557550
reset_cursor: Whether to reset the cursor before execution.
558551
"""
559552
self._check_closed() # Check if the cursor is closed
560-
561553
if reset_cursor:
562554
self._reset_cursor()
563555

0 commit comments

Comments
 (0)