Skip to content

Commit 73e5a03

Browse files
authored
FEAT: Adding set_attr in connection class (#177)
### Work Item / Issue Reference <!-- IMPORTANT: Please follow the PR template guidelines below. For mssql-python maintainers: Insert your ADO Work Item ID below (e.g. AB#37452) For external contributors: Insert Github Issue number below (e.g. #149) Only one reference is required - either GitHub issue OR ADO Work Item. --> <!-- mssql-python maintainers: ADO Work Item --> > [AB#39058](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/39058) ------------------------------------------------------------------- ### Summary This pull request introduces comprehensive support for setting ODBC connection attributes in the `mssql_python` library, aligning its functionality with pyodbc's `set_attr` API. The changes include new constants for connection attributes, transaction isolation levels, and related options, as well as robust error handling and input validation in both Python and C++ layers. This enables users to configure connection behavior (e.g., autocommit, isolation level, timeouts) in a standardized and secure manner. ### Connection Attribute Support * Added a wide set of ODBC connection attribute constants, transaction isolation level constants, access mode constants, and related enums to `mssql_python/__init__.py` and `mssql_python/constants.py`, making them available for use in Python code. * Implemented the `set_attr` method in the `Connection` Python class, providing pyodbc-compatible functionality for setting connection attributes with detailed input validation and error handling. ### C++ Backend Enhancements * Exposed `setAttribute` as a public method in the C++ `Connection` class, and added a new `setAttr` method in `ConnectionHandle`, with improved error reporting and range validation for SQLUINTEGER values. * Registered the new `set_attr` method with the Python bindings, making it accessible from Python code. ### Code Cleanup and Refactoring * Moved and consolidated connection attribute constants in `ConstantsDDBC` to improve maintainability, and removed legacy/unused constants. These changes provide a robust interface for configuring ODBC connection attributes, improve compatibility with pyodbc, and enhance error handling for attribute operations.
1 parent 10a8815 commit 73e5a03

File tree

8 files changed

+2154
-48
lines changed

8 files changed

+2154
-48
lines changed

mssql_python/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,28 @@ def getDecimalSeparator():
147147
SQL_WCHAR = ConstantsDDBC.SQL_WCHAR.value
148148
SQL_WMETADATA = -99
149149

150+
# Export connection attribute constants for set_attr()
151+
# Only include driver-level attributes that the SQL Server ODBC driver can handle directly
152+
153+
# Core driver-level attributes
154+
SQL_ATTR_ACCESS_MODE = ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value
155+
SQL_ATTR_CONNECTION_TIMEOUT = ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value
156+
SQL_ATTR_CURRENT_CATALOG = ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value
157+
SQL_ATTR_LOGIN_TIMEOUT = ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value
158+
SQL_ATTR_PACKET_SIZE = ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value
159+
SQL_ATTR_TXN_ISOLATION = ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value
160+
161+
# Transaction Isolation Level Constants
162+
SQL_TXN_READ_UNCOMMITTED = ConstantsDDBC.SQL_TXN_READ_UNCOMMITTED.value
163+
SQL_TXN_READ_COMMITTED = ConstantsDDBC.SQL_TXN_READ_COMMITTED.value
164+
SQL_TXN_REPEATABLE_READ = ConstantsDDBC.SQL_TXN_REPEATABLE_READ.value
165+
SQL_TXN_SERIALIZABLE = ConstantsDDBC.SQL_TXN_SERIALIZABLE.value
166+
167+
# Access Mode Constants
168+
SQL_MODE_READ_WRITE = ConstantsDDBC.SQL_MODE_READ_WRITE.value
169+
SQL_MODE_READ_ONLY = ConstantsDDBC.SQL_MODE_READ_ONLY.value
170+
171+
150172
from .pooling import PoolingManager
151173
def pooling(max_size=100, idle_timeout=600, enabled=True):
152174
# """

mssql_python/connection.py

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from typing import Any
1717
import threading
1818
from mssql_python.cursor import Cursor
19-
from mssql_python.helpers import add_driver_to_connection_str, sanitize_connection_string, sanitize_user_input, log
19+
from mssql_python.helpers import add_driver_to_connection_str, sanitize_connection_string, sanitize_user_input, log, validate_attribute_value
2020
from mssql_python import ddbc_bindings
2121
from mssql_python.pooling import PoolingManager
2222
from mssql_python.exceptions import InterfaceError, ProgrammingError
@@ -109,6 +109,7 @@ class Connection:
109109
setencoding(encoding=None, ctype=None) -> None:
110110
setdecoding(sqltype, encoding=None, ctype=None) -> None:
111111
getdecoding(sqltype) -> dict:
112+
set_attr(attribute, value) -> None:
112113
"""
113114

114115
# DB-API 2.0 Exception attributes
@@ -129,10 +130,16 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef
129130
Initialize the connection object with the specified connection string and parameters.
130131
131132
Args:
132-
- connection_str (str): The connection string to connect to.
133-
- autocommit (bool): If True, causes a commit to be performed after each SQL statement.
133+
connection_str (str): The connection string to connect to.
134+
autocommit (bool): If True, causes a commit to be performed after each SQL statement.
135+
attrs_before (dict, optional): Dictionary of connection attributes to set before
136+
connection establishment. Keys are SQL_ATTR_* constants,
137+
and values are their corresponding settings.
138+
Use this for attributes that must be set before connecting,
139+
such as SQL_ATTR_LOGIN_TIMEOUT, SQL_ATTR_ODBC_CURSORS,
140+
and SQL_ATTR_PACKET_SIZE.
141+
timeout (int): Login timeout in seconds. 0 means no timeout.
134142
**kwargs: Additional key/value pairs for the connection string.
135-
Not including below properties since we are driver doesn't support this:
136143
137144
Returns:
138145
None
@@ -143,6 +150,12 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef
143150
This method sets up the initial state for the connection object,
144151
preparing it for further operations such as connecting to the
145152
database, executing queries, etc.
153+
154+
Example:
155+
>>> # Setting login timeout using attrs_before
156+
>>> import mssql_python as ms
157+
>>> conn = ms.connect("Server=myserver;Database=mydb",
158+
... attrs_before={ms.SQL_ATTR_LOGIN_TIMEOUT: 30})
146159
"""
147160
self.connection_str = self._construct_connection_string(
148161
connection_str, **kwargs
@@ -546,6 +559,71 @@ def getdecoding(self, sqltype):
546559
)
547560

548561
return self._decoding_settings[sqltype].copy()
562+
563+
def set_attr(self, attribute, value):
564+
"""
565+
Set a connection attribute.
566+
567+
This method sets a connection attribute using SQLSetConnectAttr.
568+
It provides pyodbc-compatible functionality for configuring connection
569+
behavior such as autocommit mode, transaction isolation level, and
570+
connection timeouts.
571+
572+
Args:
573+
attribute (int): The connection attribute to set. Should be one of the
574+
SQL_ATTR_* constants (e.g., SQL_ATTR_AUTOCOMMIT,
575+
SQL_ATTR_TXN_ISOLATION).
576+
value: The value to set for the attribute. Can be an integer, string,
577+
bytes, or bytearray depending on the attribute type.
578+
579+
Raises:
580+
InterfaceError: If the connection is closed or attribute is invalid.
581+
ProgrammingError: If the value type or range is invalid.
582+
ProgrammingError: If the attribute cannot be set after connection.
583+
584+
Example:
585+
>>> conn.set_attr(SQL_ATTR_TXN_ISOLATION, SQL_TXN_READ_COMMITTED)
586+
587+
Note:
588+
Some attributes (like SQL_ATTR_LOGIN_TIMEOUT, SQL_ATTR_ODBC_CURSORS, and
589+
SQL_ATTR_PACKET_SIZE) can only be set before connection establishment and
590+
must be provided in the attrs_before parameter when creating the connection.
591+
Attempting to set these attributes after connection will raise a ProgrammingError.
592+
"""
593+
if self._closed:
594+
raise InterfaceError("Cannot set attribute on closed connection", "Connection is closed")
595+
596+
# Use the integrated validation helper function with connection state
597+
is_valid, error_message, sanitized_attr, sanitized_val = validate_attribute_value(
598+
attribute, value, is_connected=True
599+
)
600+
601+
if not is_valid:
602+
# Use the already sanitized values for logging
603+
log('warning', f"Invalid attribute or value: {sanitized_attr}={sanitized_val}, {error_message}")
604+
raise ProgrammingError(
605+
driver_error=f"Invalid attribute or value: {error_message}",
606+
ddbc_error=error_message
607+
)
608+
609+
# Log with sanitized values
610+
log('debug', f"Setting connection attribute: {sanitized_attr}={sanitized_val}")
611+
612+
try:
613+
# Call the underlying C++ method
614+
self._conn.set_attr(attribute, value)
615+
log('info', f"Connection attribute {sanitized_attr} set successfully")
616+
617+
except Exception as e:
618+
error_msg = f"Failed to set connection attribute {sanitized_attr}: {str(e)}"
619+
log('error', error_msg)
620+
621+
# Determine appropriate exception type based on error content
622+
error_str = str(e).lower()
623+
if 'invalid' in error_str or 'unsupported' in error_str or 'cast' in error_str:
624+
raise InterfaceError(error_msg, str(e)) from e
625+
else:
626+
raise ProgrammingError(error_msg, str(e)) from e
549627

550628
@property
551629
def searchescape(self):

mssql_python/constants.py

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,14 @@ class ConstantsDDBC(Enum):
2020
SQL_STILL_EXECUTING = 2
2121
SQL_NTS = -3
2222
SQL_DRIVER_NOPROMPT = 0
23-
SQL_ATTR_ASYNC_DBC_EVENT = 119
2423
SQL_IS_INTEGER = -6
25-
SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE = 117
2624
SQL_OV_DDBC3_80 = 380
27-
SQL_ATTR_DDBC_VERSION = 200
28-
SQL_ATTR_ASYNC_ENABLE = 4
29-
SQL_ATTR_ASYNC_STMT_EVENT = 29
3025
SQL_ERROR = -1
3126
SQL_INVALID_HANDLE = -2
3227
SQL_NULL_HANDLE = 0
3328
SQL_OV_DDBC3 = 3
3429
SQL_COMMIT = 0
3530
SQL_ROLLBACK = 1
36-
SQL_ATTR_AUTOCOMMIT = 102
3731
SQL_SMALLINT = 5
3832
SQL_CHAR = 1
3933
SQL_WCHAR = -8
@@ -94,28 +88,22 @@ class ConstantsDDBC(Enum):
9488
SQL_DESC_TYPE = 2
9589
SQL_DESC_LENGTH = 3
9690
SQL_DESC_NAME = 4
97-
SQL_ATTR_ROW_ARRAY_SIZE = 27
98-
SQL_ATTR_ROWS_FETCHED_PTR = 26
99-
SQL_ATTR_ROW_STATUS_PTR = 25
10091
SQL_ROW_SUCCESS = 0
10192
SQL_ROW_SUCCESS_WITH_INFO = 1
10293
SQL_ROW_NOROW = 100
103-
SQL_ATTR_CURSOR_TYPE = 6
10494
SQL_CURSOR_FORWARD_ONLY = 0
10595
SQL_CURSOR_STATIC = 3
10696
SQL_CURSOR_KEYSET_DRIVEN = 2
10797
SQL_CURSOR_DYNAMIC = 3
10898
SQL_NULL_DATA = -1
10999
SQL_C_DEFAULT = 99
110-
SQL_ATTR_ROW_BIND_TYPE = 5
111100
SQL_BIND_BY_COLUMN = 0
112101
SQL_PARAM_INPUT = 1
113102
SQL_PARAM_OUTPUT = 2
114103
SQL_PARAM_INPUT_OUTPUT = 3
115104
SQL_C_WCHAR = -8
116105
SQL_NULLABLE = 1
117106
SQL_MAX_NUMERIC_LEN = 16
118-
SQL_ATTR_QUERY_TIMEOUT = 2
119107

120108
SQL_FETCH_NEXT = 1
121109
SQL_FETCH_FIRST = 2
@@ -136,6 +124,60 @@ class ConstantsDDBC(Enum):
136124
SQL_QUICK = 0
137125
SQL_ENSURE = 1
138126

127+
# Connection Attribute Constants for set_attr()
128+
SQL_ATTR_ACCESS_MODE = 101
129+
SQL_ATTR_AUTOCOMMIT = 102
130+
SQL_ATTR_CURSOR_TYPE = 6
131+
SQL_ATTR_ROW_BIND_TYPE = 5
132+
SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE = 117
133+
SQL_ATTR_ROW_ARRAY_SIZE = 27
134+
SQL_ATTR_ASYNC_DBC_EVENT = 119
135+
SQL_ATTR_DDBC_VERSION = 200
136+
SQL_ATTR_ASYNC_STMT_EVENT = 29
137+
SQL_ATTR_ROWS_FETCHED_PTR = 26
138+
SQL_ATTR_ROW_STATUS_PTR = 25
139+
SQL_ATTR_CONNECTION_TIMEOUT = 113
140+
SQL_ATTR_CURRENT_CATALOG = 109
141+
SQL_ATTR_LOGIN_TIMEOUT = 103
142+
SQL_ATTR_ODBC_CURSORS = 110
143+
SQL_ATTR_PACKET_SIZE = 112
144+
SQL_ATTR_QUIET_MODE = 111
145+
SQL_ATTR_TXN_ISOLATION = 108
146+
SQL_ATTR_TRACE = 104
147+
SQL_ATTR_TRACEFILE = 105
148+
SQL_ATTR_TRANSLATE_LIB = 106
149+
SQL_ATTR_TRANSLATE_OPTION = 107
150+
SQL_ATTR_CONNECTION_POOLING = 201
151+
SQL_ATTR_CP_MATCH = 202
152+
SQL_ATTR_ASYNC_ENABLE = 4
153+
SQL_ATTR_ENLIST_IN_DTC = 1207
154+
SQL_ATTR_ENLIST_IN_XA = 1208
155+
SQL_ATTR_CONNECTION_DEAD = 1209
156+
SQL_ATTR_SERVER_NAME = 13
157+
SQL_ATTR_RESET_CONNECTION = 116
158+
159+
# Transaction Isolation Level Constants
160+
SQL_TXN_READ_UNCOMMITTED = 1
161+
SQL_TXN_READ_COMMITTED = 2
162+
SQL_TXN_REPEATABLE_READ = 4
163+
SQL_TXN_SERIALIZABLE = 8
164+
165+
# Access Mode Constants
166+
SQL_MODE_READ_WRITE = 0
167+
SQL_MODE_READ_ONLY = 1
168+
169+
# Connection Dead Constants
170+
SQL_CD_TRUE = 1
171+
SQL_CD_FALSE = 0
172+
173+
# ODBC Cursors Constants
174+
SQL_CUR_USE_IF_NEEDED = 0
175+
SQL_CUR_USE_ODBC = 1
176+
SQL_CUR_USE_DRIVER = 2
177+
178+
# Reset Connection Constants
179+
SQL_RESET_CONNECTION_YES = 1
180+
139181
class GetInfoConstants(Enum):
140182
"""
141183
These constants are used with various methods like getinfo().
@@ -324,4 +366,54 @@ def get_numeric_types(cls) -> set:
324366
ConstantsDDBC.SQL_SMALLINT.value, ConstantsDDBC.SQL_INTEGER.value,
325367
ConstantsDDBC.SQL_BIGINT.value, ConstantsDDBC.SQL_REAL.value,
326368
ConstantsDDBC.SQL_FLOAT.value, ConstantsDDBC.SQL_DOUBLE.value
327-
}
369+
}
370+
371+
class AttributeSetTime(Enum):
372+
"""
373+
Defines when connection attributes can be set in relation to connection establishment.
374+
375+
This enum is used to validate if a specific connection attribute can be set before
376+
connection, after connection, or at either time.
377+
"""
378+
BEFORE_ONLY = 1 # Must be set before connection is established
379+
AFTER_ONLY = 2 # Can only be set after connection is established
380+
EITHER = 3 # Can be set either before or after connection
381+
382+
# Dictionary mapping attributes to their valid set times
383+
ATTRIBUTE_SET_TIMING = {
384+
# Must be set before connection
385+
ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value: AttributeSetTime.BEFORE_ONLY,
386+
ConstantsDDBC.SQL_ATTR_ODBC_CURSORS.value: AttributeSetTime.BEFORE_ONLY,
387+
ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value: AttributeSetTime.BEFORE_ONLY,
388+
389+
# Can only be set after connection
390+
ConstantsDDBC.SQL_ATTR_CONNECTION_DEAD.value: AttributeSetTime.AFTER_ONLY,
391+
ConstantsDDBC.SQL_ATTR_ENLIST_IN_DTC.value: AttributeSetTime.AFTER_ONLY,
392+
ConstantsDDBC.SQL_ATTR_TRANSLATE_LIB.value: AttributeSetTime.AFTER_ONLY,
393+
ConstantsDDBC.SQL_ATTR_TRANSLATE_OPTION.value: AttributeSetTime.AFTER_ONLY,
394+
395+
# Can be set either before or after connection
396+
ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value: AttributeSetTime.EITHER,
397+
ConstantsDDBC.SQL_ATTR_ASYNC_DBC_EVENT.value: AttributeSetTime.EITHER,
398+
ConstantsDDBC.SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE.value: AttributeSetTime.EITHER,
399+
ConstantsDDBC.SQL_ATTR_ASYNC_ENABLE.value: AttributeSetTime.EITHER,
400+
ConstantsDDBC.SQL_ATTR_AUTOCOMMIT.value: AttributeSetTime.EITHER,
401+
ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value: AttributeSetTime.EITHER,
402+
ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value: AttributeSetTime.EITHER,
403+
ConstantsDDBC.SQL_ATTR_QUIET_MODE.value: AttributeSetTime.EITHER,
404+
ConstantsDDBC.SQL_ATTR_TRACE.value: AttributeSetTime.EITHER,
405+
ConstantsDDBC.SQL_ATTR_TRACEFILE.value: AttributeSetTime.EITHER,
406+
ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value: AttributeSetTime.EITHER,
407+
}
408+
409+
def get_attribute_set_timing(attribute):
410+
"""
411+
Get when an attribute can be set (before connection, after, or either).
412+
413+
Args:
414+
attribute (int): The connection attribute (SQL_ATTR_*)
415+
416+
Returns:
417+
AttributeSetTime: When the attribute can be set
418+
"""
419+
return ATTRIBUTE_SET_TIMING.get(attribute, AttributeSetTime.AFTER_ONLY)

0 commit comments

Comments
 (0)