Skip to content

Commit ef2142d

Browse files
committed
FEAT: Adding set_attrs function for connection class
1 parent 1de924c commit ef2142d

File tree

7 files changed

+523
-17
lines changed

7 files changed

+523
-17
lines changed

mssql_python/__init__.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,53 @@
5252
SQL_WCHAR = ConstantsDDBC.SQL_WCHAR.value
5353
SQL_WMETADATA = -99
5454

55+
# Export connection attribute constants for set_attr()
56+
SQL_ATTR_ACCESS_MODE = ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value
57+
SQL_ATTR_AUTOCOMMIT = ConstantsDDBC.SQL_ATTR_AUTOCOMMIT.value
58+
SQL_ATTR_CONNECTION_TIMEOUT = ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value
59+
SQL_ATTR_CURRENT_CATALOG = ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value
60+
SQL_ATTR_LOGIN_TIMEOUT = ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value
61+
SQL_ATTR_ODBC_CURSORS = ConstantsDDBC.SQL_ATTR_ODBC_CURSORS.value
62+
SQL_ATTR_PACKET_SIZE = ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value
63+
SQL_ATTR_QUIET_MODE = ConstantsDDBC.SQL_ATTR_QUIET_MODE.value
64+
SQL_ATTR_TXN_ISOLATION = ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value
65+
SQL_ATTR_TRACE = ConstantsDDBC.SQL_ATTR_TRACE.value
66+
SQL_ATTR_TRACEFILE = ConstantsDDBC.SQL_ATTR_TRACEFILE.value
67+
SQL_ATTR_TRANSLATE_LIB = ConstantsDDBC.SQL_ATTR_TRANSLATE_LIB.value
68+
SQL_ATTR_TRANSLATE_OPTION = ConstantsDDBC.SQL_ATTR_TRANSLATE_OPTION.value
69+
SQL_ATTR_CONNECTION_POOLING = ConstantsDDBC.SQL_ATTR_CONNECTION_POOLING.value
70+
SQL_ATTR_CP_MATCH = ConstantsDDBC.SQL_ATTR_CP_MATCH.value
71+
SQL_ATTR_ASYNC_ENABLE = ConstantsDDBC.SQL_ATTR_ASYNC_ENABLE.value
72+
SQL_ATTR_ENLIST_IN_DTC = ConstantsDDBC.SQL_ATTR_ENLIST_IN_DTC.value
73+
SQL_ATTR_ENLIST_IN_XA = ConstantsDDBC.SQL_ATTR_ENLIST_IN_XA.value
74+
SQL_ATTR_CONNECTION_DEAD = ConstantsDDBC.SQL_ATTR_CONNECTION_DEAD.value
75+
SQL_ATTR_SERVER_NAME = ConstantsDDBC.SQL_ATTR_SERVER_NAME.value
76+
SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE = ConstantsDDBC.SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE.value
77+
SQL_ATTR_ASYNC_DBC_EVENT = ConstantsDDBC.SQL_ATTR_ASYNC_DBC_EVENT.value
78+
SQL_ATTR_RESET_CONNECTION = ConstantsDDBC.SQL_ATTR_RESET_CONNECTION.value
79+
80+
# Transaction Isolation Level Constants
81+
SQL_TXN_READ_UNCOMMITTED = ConstantsDDBC.SQL_TXN_READ_UNCOMMITTED.value
82+
SQL_TXN_READ_COMMITTED = ConstantsDDBC.SQL_TXN_READ_COMMITTED.value
83+
SQL_TXN_REPEATABLE_READ = ConstantsDDBC.SQL_TXN_REPEATABLE_READ.value
84+
SQL_TXN_SERIALIZABLE = ConstantsDDBC.SQL_TXN_SERIALIZABLE.value
85+
86+
# Access Mode Constants
87+
SQL_MODE_READ_WRITE = ConstantsDDBC.SQL_MODE_READ_WRITE.value
88+
SQL_MODE_READ_ONLY = ConstantsDDBC.SQL_MODE_READ_ONLY.value
89+
90+
# Connection Dead Constants
91+
SQL_CD_TRUE = ConstantsDDBC.SQL_CD_TRUE.value
92+
SQL_CD_FALSE = ConstantsDDBC.SQL_CD_FALSE.value
93+
94+
# ODBC Cursors Constants
95+
SQL_CUR_USE_IF_NEEDED = ConstantsDDBC.SQL_CUR_USE_IF_NEEDED.value
96+
SQL_CUR_USE_ODBC = ConstantsDDBC.SQL_CUR_USE_ODBC.value
97+
SQL_CUR_USE_DRIVER = ConstantsDDBC.SQL_CUR_USE_DRIVER.value
98+
99+
# Reset Connection Constants
100+
SQL_RESET_CONNECTION_YES = ConstantsDDBC.SQL_RESET_CONNECTION_YES.value
101+
55102
# GLOBALS
56103
# Read-Only
57104
apilevel = "2.0"

mssql_python/connection.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class Connection:
7575
setencoding(encoding=None, ctype=None) -> None:
7676
setdecoding(sqltype, encoding=None, ctype=None) -> None:
7777
getdecoding(sqltype) -> dict:
78+
set_attr(attribute, value) -> None: # Add this line
7879
"""
7980

8081
def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_before: dict = None, **kwargs) -> None:
@@ -520,6 +521,78 @@ def rollback(self) -> None:
520521
self._conn.rollback()
521522
log('info', "Transaction rolled back successfully.")
522523

524+
def set_attr(self, attribute, value):
525+
"""
526+
Set a connection attribute.
527+
528+
This method sets a connection attribute using SQLSetConnectAttr.
529+
It provides pyodbc-compatible functionality for configuring connection
530+
behavior such as autocommit mode, transaction isolation level, and
531+
connection timeouts.
532+
533+
Args:
534+
attribute (int): The connection attribute to set. Should be one of the
535+
SQL_ATTR_* constants (e.g., SQL_ATTR_AUTOCOMMIT,
536+
SQL_ATTR_TXN_ISOLATION).
537+
value: The value to set for the attribute. Can be an integer or bytes/bytearray
538+
depending on the attribute type.
539+
540+
Raises:
541+
InterfaceError: If the connection is closed or attribute is invalid.
542+
ProgrammingError: If the value type or range is invalid.
543+
544+
Example:
545+
>>> conn.set_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF)
546+
>>> conn.set_attr(SQL_ATTR_TXN_ISOLATION, SQL_TXN_READ_COMMITTED)
547+
548+
Note:
549+
This method is compatible with pyodbc's set_attr functionality.
550+
Attribute values must be within valid SQLUINTEGER range (0 to 4294967295).
551+
"""
552+
if self._closed:
553+
raise InterfaceError("Cannot set attribute on closed connection", "Connection is closed")
554+
555+
# Validate attribute type and range for SQLUINTEGER compatibility
556+
if not isinstance(attribute, int) or attribute < 0:
557+
raise ProgrammingError("Connection attribute must be a non-negative integer", f"Invalid attribute: {attribute}")
558+
559+
# Validate attribute is within SQLUINTEGER range
560+
if attribute > 4294967295: # 2^32 - 1
561+
raise ProgrammingError("Connection attribute must be within SQLUINTEGER range (0-4294967295)", f"Attribute out of range: {attribute}")
562+
563+
# Validate value type - must be integer, bytes, or bytearray
564+
if not isinstance(value, (int, bytes, bytearray)):
565+
raise ProgrammingError("Attribute value must be an integer, bytes, or bytearray", f"Invalid value type: {type(value)}")
566+
567+
# For integer values, validate SQLUINTEGER range
568+
if isinstance(value, int):
569+
if value < 0 or value > 4294967295: # 2^32 - 1
570+
raise ProgrammingError("Attribute value out of range for SQLUINTEGER (0-4294967295)", f"Value out of range: {value}")
571+
572+
# Sanitize user input for security
573+
try:
574+
sanitized_input = sanitize_user_input(str(attribute))
575+
log('debug', f"Setting connection attribute: {sanitized_input}")
576+
except Exception:
577+
# If sanitization fails, log without user input
578+
log('debug', "Setting connection attribute")
579+
580+
try:
581+
# Call the underlying C++ method
582+
self._conn.set_attr(attribute, value)
583+
log('info', f"Connection attribute {attribute} set successfully")
584+
585+
except Exception as e:
586+
error_msg = f"Failed to set connection attribute {attribute}: {str(e)}"
587+
log('error', error_msg)
588+
589+
# Determine appropriate exception type based on error content
590+
error_str = str(e).lower()
591+
if 'invalid' in error_str or 'unsupported' in error_str or 'cast' in error_str:
592+
raise InterfaceError(error_msg, str(e)) from e
593+
else:
594+
raise ProgrammingError(error_msg, str(e)) from e
595+
523596
def close(self) -> None:
524597
"""
525598
Close the connection now (rather than whenever .__del__() is called).

mssql_python/constants.py

Lines changed: 55 additions & 11 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,21 +88,16 @@ 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_FETCH_NEXT = 1
10192
SQL_ROW_SUCCESS = 0
10293
SQL_ROW_SUCCESS_WITH_INFO = 1
10394
SQL_ROW_NOROW = 100
104-
SQL_ATTR_CURSOR_TYPE = 6
10595
SQL_CURSOR_FORWARD_ONLY = 0
10696
SQL_CURSOR_STATIC = 3
10797
SQL_CURSOR_KEYSET_DRIVEN = 2
10898
SQL_CURSOR_DYNAMIC = 3
10999
SQL_NULL_DATA = -1
110100
SQL_C_DEFAULT = 99
111-
SQL_ATTR_ROW_BIND_TYPE = 5
112101
SQL_BIND_BY_COLUMN = 0
113102
SQL_PARAM_INPUT = 1
114103
SQL_PARAM_OUTPUT = 2
@@ -117,6 +106,61 @@ class ConstantsDDBC(Enum):
117106
SQL_NULLABLE = 1
118107
SQL_MAX_NUMERIC_LEN = 16
119108

109+
# Connection Attribute Constants for set_attr()
110+
SQL_ATTR_ACCESS_MODE = 101
111+
SQL_ATTR_AUTOCOMMIT = 102
112+
SQL_ATTR_CURSOR_TYPE = 6
113+
SQL_ATTR_ROW_BIND_TYPE = 5
114+
SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE = 117
115+
SQL_ATTR_ROW_ARRAY_SIZE = 27
116+
SQL_ATTR_ASYNC_DBC_EVENT = 119
117+
SQL_ATTR_DDBC_VERSION = 200
118+
SQL_ATTR_ASYNC_STMT_EVENT = 29
119+
SQL_ATTR_ROWS_FETCHED_PTR = 26
120+
SQL_ATTR_ROW_STATUS_PTR = 25
121+
SQL_ATTR_CONNECTION_TIMEOUT = 113
122+
SQL_ATTR_CURRENT_CATALOG = 109
123+
SQL_ATTR_LOGIN_TIMEOUT = 103
124+
SQL_ATTR_ODBC_CURSORS = 110
125+
SQL_ATTR_PACKET_SIZE = 112
126+
SQL_ATTR_QUIET_MODE = 111
127+
SQL_ATTR_TXN_ISOLATION = 108
128+
SQL_ATTR_TRACE = 104
129+
SQL_ATTR_TRACEFILE = 105
130+
SQL_ATTR_TRANSLATE_LIB = 106
131+
SQL_ATTR_TRANSLATE_OPTION = 107
132+
SQL_ATTR_CONNECTION_POOLING = 201
133+
SQL_ATTR_CP_MATCH = 202
134+
SQL_ATTR_ASYNC_ENABLE = 4
135+
SQL_ATTR_ENLIST_IN_DTC = 1207
136+
SQL_ATTR_ENLIST_IN_XA = 1208
137+
SQL_ATTR_CONNECTION_DEAD = 1209
138+
SQL_ATTR_SERVER_NAME = 13
139+
SQL_ATTR_RESET_CONNECTION = 116
140+
141+
# Transaction Isolation Level Constants
142+
SQL_TXN_READ_UNCOMMITTED = 1
143+
SQL_TXN_READ_COMMITTED = 2
144+
SQL_TXN_REPEATABLE_READ = 4
145+
SQL_TXN_SERIALIZABLE = 8
146+
147+
# Access Mode Constants
148+
SQL_MODE_READ_WRITE = 0
149+
SQL_MODE_READ_ONLY = 1
150+
151+
# Connection Dead Constants
152+
SQL_CD_TRUE = 1
153+
SQL_CD_FALSE = 0
154+
155+
# ODBC Cursors Constants
156+
SQL_CUR_USE_IF_NEEDED = 0
157+
SQL_CUR_USE_ODBC = 1
158+
SQL_CUR_USE_DRIVER = 2
159+
160+
# Reset Connection Constants
161+
SQL_RESET_CONNECTION_YES = 1
162+
163+
120164
class AuthType(Enum):
121165
"""Constants for authentication types"""
122166
INTERACTIVE = "activedirectoryinteractive"

mssql_python/pybind/connection/connection.cpp

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,18 @@ SQLRETURN Connection::setAttribute(SQLINTEGER attribute, py::object value) {
174174
SQLINTEGER length = 0;
175175

176176
if (py::isinstance<py::int_>(value)) {
177-
int intValue = value.cast<int>();
178-
ptr = reinterpret_cast<SQLPOINTER>(static_cast<uintptr_t>(intValue));
177+
// Handle large integer values up to SQLUINTEGER range
178+
long long longValue = value.cast<long long>();
179+
180+
// Validate range for SQLUINTEGER (0 to 4294967295)
181+
if (longValue < 0 || longValue > 4294967295LL) {
182+
LOG("Integer value out of SQLUINTEGER range: {}", longValue);
183+
return SQL_ERROR;
184+
}
185+
186+
// Cast to SQLUINTEGER for proper handling
187+
SQLUINTEGER uintValue = static_cast<SQLUINTEGER>(longValue);
188+
ptr = reinterpret_cast<SQLPOINTER>(static_cast<uintptr_t>(uintValue));
179189
length = SQL_IS_INTEGER;
180190
} else if (py::isinstance<py::bytes>(value) || py::isinstance<py::bytearray>(value)) {
181191
static std::vector<std::string> buffers;
@@ -314,4 +324,34 @@ SqlHandlePtr ConnectionHandle::allocStatementHandle() {
314324
ThrowStdException("Connection object is not initialized");
315325
}
316326
return _conn->allocStatementHandle();
327+
}
328+
329+
void ConnectionHandle::setAttr(int attribute, py::object value) {
330+
if (!_conn) {
331+
ThrowStdException("Connection not established");
332+
}
333+
334+
// Use existing setAttribute with better error handling
335+
SQLRETURN ret = _conn->setAttribute(static_cast<SQLINTEGER>(attribute), value);
336+
if (!SQL_SUCCEEDED(ret)) {
337+
// Get detailed error information from ODBC
338+
try {
339+
ErrorInfo errorInfo = SQLCheckError_Wrap(SQL_HANDLE_DBC, _conn->getDbcHandle(), ret);
340+
341+
std::string errorMsg = "Failed to set connection attribute " + std::to_string(attribute);
342+
if (!errorInfo.ddbcErrorMsg.empty()) {
343+
// Convert wstring to string for concatenation
344+
std::string ddbcErrorStr = WideToUTF8(errorInfo.ddbcErrorMsg);
345+
errorMsg += ": " + ddbcErrorStr;
346+
}
347+
348+
LOG("Connection setAttribute failed: {}", errorMsg);
349+
ThrowStdException(errorMsg);
350+
} catch (...) {
351+
// Fallback to generic error if detailed error retrieval fails
352+
std::string errorMsg = "Failed to set connection attribute " + std::to_string(attribute);
353+
LOG("Connection setAttribute failed: {}", errorMsg);
354+
ThrowStdException(errorMsg);
355+
}
356+
}
317357
}

mssql_python/pybind/connection/connection.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,15 @@ class Connection {
4242
// Allocate a new statement handle on this connection.
4343
SqlHandlePtr allocStatementHandle();
4444

45+
// Move setAttribute from private to public
46+
SQLRETURN setAttribute(SQLINTEGER attribute, py::object value);
47+
48+
// Add getter for DBC handle for error reporting
49+
SqlHandlePtr getDbcHandle() const { return _dbcHandle; }
50+
4551
private:
4652
void allocateDbcHandle();
4753
void checkError(SQLRETURN ret) const;
48-
SQLRETURN setAttribute(SQLINTEGER attribute, py::object value);
4954
void applyAttrsBefore(const py::dict& attrs_before);
5055

5156
std::wstring _connStr;
@@ -66,6 +71,7 @@ class ConnectionHandle {
6671
void setAutocommit(bool enabled);
6772
bool getAutocommit() const;
6873
SqlHandlePtr allocStatementHandle();
74+
void setAttr(int attribute, py::object value); // Add this line
6975

7076
private:
7177
std::shared_ptr<Connection> _conn;

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2532,6 +2532,7 @@ PYBIND11_MODULE(ddbc_bindings, m) {
25322532
.def("rollback", &ConnectionHandle::rollback, "Rollback the current transaction")
25332533
.def("set_autocommit", &ConnectionHandle::setAutocommit)
25342534
.def("get_autocommit", &ConnectionHandle::getAutocommit)
2535+
.def("set_attr", &ConnectionHandle::setAttr, py::arg("attribute"), py::arg("value"), "Set connection attribute")
25352536
.def("alloc_statement_handle", &ConnectionHandle::allocStatementHandle);
25362537
m.def("enable_pooling", &enable_pooling, "Enable global connection pooling");
25372538
m.def("close_pooling", []() {ConnectionPoolManager::getInstance().closePools();});

0 commit comments

Comments
 (0)