Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,28 @@ def getDecimalSeparator():
SQL_WCHAR = ConstantsDDBC.SQL_WCHAR.value
SQL_WMETADATA = -99

# Export connection attribute constants for set_attr()
# Only include driver-level attributes that the SQL Server ODBC driver can handle directly

# Core driver-level attributes
SQL_ATTR_ACCESS_MODE = ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value
SQL_ATTR_CONNECTION_TIMEOUT = ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value
SQL_ATTR_CURRENT_CATALOG = ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value
SQL_ATTR_LOGIN_TIMEOUT = ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value
SQL_ATTR_PACKET_SIZE = ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value
SQL_ATTR_TXN_ISOLATION = ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value

# Transaction Isolation Level Constants
SQL_TXN_READ_UNCOMMITTED = ConstantsDDBC.SQL_TXN_READ_UNCOMMITTED.value
SQL_TXN_READ_COMMITTED = ConstantsDDBC.SQL_TXN_READ_COMMITTED.value
SQL_TXN_REPEATABLE_READ = ConstantsDDBC.SQL_TXN_REPEATABLE_READ.value
SQL_TXN_SERIALIZABLE = ConstantsDDBC.SQL_TXN_SERIALIZABLE.value

# Access Mode Constants
SQL_MODE_READ_WRITE = ConstantsDDBC.SQL_MODE_READ_WRITE.value
SQL_MODE_READ_ONLY = ConstantsDDBC.SQL_MODE_READ_ONLY.value


from .pooling import PoolingManager
def pooling(max_size=100, idle_timeout=600, enabled=True):
# """
Expand Down
86 changes: 82 additions & 4 deletions mssql_python/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from typing import Any
import threading
from mssql_python.cursor import Cursor
from mssql_python.helpers import add_driver_to_connection_str, sanitize_connection_string, sanitize_user_input, log
from mssql_python.helpers import add_driver_to_connection_str, sanitize_connection_string, sanitize_user_input, log, validate_attribute_value
from mssql_python import ddbc_bindings
from mssql_python.pooling import PoolingManager
from mssql_python.exceptions import InterfaceError, ProgrammingError
Expand Down Expand Up @@ -109,6 +109,7 @@ class Connection:
setencoding(encoding=None, ctype=None) -> None:
setdecoding(sqltype, encoding=None, ctype=None) -> None:
getdecoding(sqltype) -> dict:
set_attr(attribute, value) -> None:
"""

# DB-API 2.0 Exception attributes
Expand All @@ -129,10 +130,16 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef
Initialize the connection object with the specified connection string and parameters.

Args:
- connection_str (str): The connection string to connect to.
- autocommit (bool): If True, causes a commit to be performed after each SQL statement.
connection_str (str): The connection string to connect to.
autocommit (bool): If True, causes a commit to be performed after each SQL statement.
attrs_before (dict, optional): Dictionary of connection attributes to set before
connection establishment. Keys are SQL_ATTR_* constants,
and values are their corresponding settings.
Use this for attributes that must be set before connecting,
such as SQL_ATTR_LOGIN_TIMEOUT, SQL_ATTR_ODBC_CURSORS,
and SQL_ATTR_PACKET_SIZE.
timeout (int): Login timeout in seconds. 0 means no timeout.
**kwargs: Additional key/value pairs for the connection string.
Not including below properties since we are driver doesn't support this:

Returns:
None
Expand All @@ -143,6 +150,12 @@ def __init__(self, connection_str: str = "", autocommit: bool = False, attrs_bef
This method sets up the initial state for the connection object,
preparing it for further operations such as connecting to the
database, executing queries, etc.

Example:
>>> # Setting login timeout using attrs_before
>>> import mssql_python as ms
>>> conn = ms.connect("Server=myserver;Database=mydb",
... attrs_before={ms.SQL_ATTR_LOGIN_TIMEOUT: 30})
"""
self.connection_str = self._construct_connection_string(
connection_str, **kwargs
Expand Down Expand Up @@ -546,6 +559,71 @@ def getdecoding(self, sqltype):
)

return self._decoding_settings[sqltype].copy()

def set_attr(self, attribute, value):
"""
Set a connection attribute.

This method sets a connection attribute using SQLSetConnectAttr.
It provides pyodbc-compatible functionality for configuring connection
behavior such as autocommit mode, transaction isolation level, and
connection timeouts.

Args:
attribute (int): The connection attribute to set. Should be one of the
SQL_ATTR_* constants (e.g., SQL_ATTR_AUTOCOMMIT,
SQL_ATTR_TXN_ISOLATION).
value: The value to set for the attribute. Can be an integer, string,
bytes, or bytearray depending on the attribute type.

Raises:
InterfaceError: If the connection is closed or attribute is invalid.
ProgrammingError: If the value type or range is invalid.
ProgrammingError: If the attribute cannot be set after connection.

Example:
>>> conn.set_attr(SQL_ATTR_TXN_ISOLATION, SQL_TXN_READ_COMMITTED)

Note:
Some attributes (like SQL_ATTR_LOGIN_TIMEOUT, SQL_ATTR_ODBC_CURSORS, and
SQL_ATTR_PACKET_SIZE) can only be set before connection establishment and
must be provided in the attrs_before parameter when creating the connection.
Attempting to set these attributes after connection will raise a ProgrammingError.
"""
if self._closed:
raise InterfaceError("Cannot set attribute on closed connection", "Connection is closed")

# Use the integrated validation helper function with connection state
is_valid, error_message, sanitized_attr, sanitized_val = validate_attribute_value(
attribute, value, is_connected=True
)

if not is_valid:
# Use the already sanitized values for logging
log('warning', f"Invalid attribute or value: {sanitized_attr}={sanitized_val}, {error_message}")
raise ProgrammingError(
driver_error=f"Invalid attribute or value: {error_message}",
ddbc_error=error_message
)

# Log with sanitized values
log('debug', f"Setting connection attribute: {sanitized_attr}={sanitized_val}")

try:
# Call the underlying C++ method
self._conn.set_attr(attribute, value)
log('info', f"Connection attribute {sanitized_attr} set successfully")

except Exception as e:
error_msg = f"Failed to set connection attribute {sanitized_attr}: {str(e)}"
log('error', error_msg)

# Determine appropriate exception type based on error content
error_str = str(e).lower()
if 'invalid' in error_str or 'unsupported' in error_str or 'cast' in error_str:
raise InterfaceError(error_msg, str(e)) from e
else:
raise ProgrammingError(error_msg, str(e)) from e

@property
def searchescape(self):
Expand Down
118 changes: 105 additions & 13 deletions mssql_python/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,14 @@ class ConstantsDDBC(Enum):
SQL_STILL_EXECUTING = 2
SQL_NTS = -3
SQL_DRIVER_NOPROMPT = 0
SQL_ATTR_ASYNC_DBC_EVENT = 119
SQL_IS_INTEGER = -6
SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE = 117
SQL_OV_DDBC3_80 = 380
SQL_ATTR_DDBC_VERSION = 200
SQL_ATTR_ASYNC_ENABLE = 4
SQL_ATTR_ASYNC_STMT_EVENT = 29
SQL_ERROR = -1
SQL_INVALID_HANDLE = -2
SQL_NULL_HANDLE = 0
SQL_OV_DDBC3 = 3
SQL_COMMIT = 0
SQL_ROLLBACK = 1
SQL_ATTR_AUTOCOMMIT = 102
SQL_SMALLINT = 5
SQL_CHAR = 1
SQL_WCHAR = -8
Expand Down Expand Up @@ -94,28 +88,22 @@ class ConstantsDDBC(Enum):
SQL_DESC_TYPE = 2
SQL_DESC_LENGTH = 3
SQL_DESC_NAME = 4
SQL_ATTR_ROW_ARRAY_SIZE = 27
SQL_ATTR_ROWS_FETCHED_PTR = 26
SQL_ATTR_ROW_STATUS_PTR = 25
SQL_ROW_SUCCESS = 0
SQL_ROW_SUCCESS_WITH_INFO = 1
SQL_ROW_NOROW = 100
SQL_ATTR_CURSOR_TYPE = 6
SQL_CURSOR_FORWARD_ONLY = 0
SQL_CURSOR_STATIC = 3
SQL_CURSOR_KEYSET_DRIVEN = 2
SQL_CURSOR_DYNAMIC = 3
SQL_NULL_DATA = -1
SQL_C_DEFAULT = 99
SQL_ATTR_ROW_BIND_TYPE = 5
SQL_BIND_BY_COLUMN = 0
SQL_PARAM_INPUT = 1
SQL_PARAM_OUTPUT = 2
SQL_PARAM_INPUT_OUTPUT = 3
SQL_C_WCHAR = -8
SQL_NULLABLE = 1
SQL_MAX_NUMERIC_LEN = 16
SQL_ATTR_QUERY_TIMEOUT = 2

SQL_FETCH_NEXT = 1
SQL_FETCH_FIRST = 2
Expand All @@ -136,6 +124,60 @@ class ConstantsDDBC(Enum):
SQL_QUICK = 0
SQL_ENSURE = 1

# Connection Attribute Constants for set_attr()
SQL_ATTR_ACCESS_MODE = 101
SQL_ATTR_AUTOCOMMIT = 102
SQL_ATTR_CURSOR_TYPE = 6
SQL_ATTR_ROW_BIND_TYPE = 5
SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE = 117
SQL_ATTR_ROW_ARRAY_SIZE = 27
SQL_ATTR_ASYNC_DBC_EVENT = 119
SQL_ATTR_DDBC_VERSION = 200
SQL_ATTR_ASYNC_STMT_EVENT = 29
SQL_ATTR_ROWS_FETCHED_PTR = 26
SQL_ATTR_ROW_STATUS_PTR = 25
SQL_ATTR_CONNECTION_TIMEOUT = 113
SQL_ATTR_CURRENT_CATALOG = 109
SQL_ATTR_LOGIN_TIMEOUT = 103
SQL_ATTR_ODBC_CURSORS = 110
SQL_ATTR_PACKET_SIZE = 112
SQL_ATTR_QUIET_MODE = 111
SQL_ATTR_TXN_ISOLATION = 108
SQL_ATTR_TRACE = 104
SQL_ATTR_TRACEFILE = 105
SQL_ATTR_TRANSLATE_LIB = 106
SQL_ATTR_TRANSLATE_OPTION = 107
SQL_ATTR_CONNECTION_POOLING = 201
SQL_ATTR_CP_MATCH = 202
SQL_ATTR_ASYNC_ENABLE = 4
SQL_ATTR_ENLIST_IN_DTC = 1207
SQL_ATTR_ENLIST_IN_XA = 1208
SQL_ATTR_CONNECTION_DEAD = 1209
SQL_ATTR_SERVER_NAME = 13
SQL_ATTR_RESET_CONNECTION = 116

# Transaction Isolation Level Constants
SQL_TXN_READ_UNCOMMITTED = 1
SQL_TXN_READ_COMMITTED = 2
SQL_TXN_REPEATABLE_READ = 4
SQL_TXN_SERIALIZABLE = 8

# Access Mode Constants
SQL_MODE_READ_WRITE = 0
SQL_MODE_READ_ONLY = 1

# Connection Dead Constants
SQL_CD_TRUE = 1
SQL_CD_FALSE = 0

# ODBC Cursors Constants
SQL_CUR_USE_IF_NEEDED = 0
SQL_CUR_USE_ODBC = 1
SQL_CUR_USE_DRIVER = 2

# Reset Connection Constants
SQL_RESET_CONNECTION_YES = 1

class GetInfoConstants(Enum):
"""
These constants are used with various methods like getinfo().
Expand Down Expand Up @@ -324,4 +366,54 @@ def get_numeric_types(cls) -> set:
ConstantsDDBC.SQL_SMALLINT.value, ConstantsDDBC.SQL_INTEGER.value,
ConstantsDDBC.SQL_BIGINT.value, ConstantsDDBC.SQL_REAL.value,
ConstantsDDBC.SQL_FLOAT.value, ConstantsDDBC.SQL_DOUBLE.value
}
}

class AttributeSetTime(Enum):
"""
Defines when connection attributes can be set in relation to connection establishment.

This enum is used to validate if a specific connection attribute can be set before
connection, after connection, or at either time.
"""
BEFORE_ONLY = 1 # Must be set before connection is established
AFTER_ONLY = 2 # Can only be set after connection is established
EITHER = 3 # Can be set either before or after connection

# Dictionary mapping attributes to their valid set times
ATTRIBUTE_SET_TIMING = {
# Must be set before connection
ConstantsDDBC.SQL_ATTR_LOGIN_TIMEOUT.value: AttributeSetTime.BEFORE_ONLY,
ConstantsDDBC.SQL_ATTR_ODBC_CURSORS.value: AttributeSetTime.BEFORE_ONLY,
ConstantsDDBC.SQL_ATTR_PACKET_SIZE.value: AttributeSetTime.BEFORE_ONLY,

# Can only be set after connection
ConstantsDDBC.SQL_ATTR_CONNECTION_DEAD.value: AttributeSetTime.AFTER_ONLY,
ConstantsDDBC.SQL_ATTR_ENLIST_IN_DTC.value: AttributeSetTime.AFTER_ONLY,
ConstantsDDBC.SQL_ATTR_TRANSLATE_LIB.value: AttributeSetTime.AFTER_ONLY,
ConstantsDDBC.SQL_ATTR_TRANSLATE_OPTION.value: AttributeSetTime.AFTER_ONLY,

# Can be set either before or after connection
ConstantsDDBC.SQL_ATTR_ACCESS_MODE.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_ASYNC_DBC_EVENT.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_ASYNC_DBC_FUNCTIONS_ENABLE.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_ASYNC_ENABLE.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_AUTOCOMMIT.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_CONNECTION_TIMEOUT.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_CURRENT_CATALOG.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_QUIET_MODE.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_TRACE.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_TRACEFILE.value: AttributeSetTime.EITHER,
ConstantsDDBC.SQL_ATTR_TXN_ISOLATION.value: AttributeSetTime.EITHER,
}

def get_attribute_set_timing(attribute):
"""
Get when an attribute can be set (before connection, after, or either).

Args:
attribute (int): The connection attribute (SQL_ATTR_*)

Returns:
AttributeSetTime: When the attribute can be set
"""
return ATTRIBUTE_SET_TIMING.get(attribute, AttributeSetTime.AFTER_ONLY)
Loading
Loading