Skip to content
62 changes: 45 additions & 17 deletions mssql_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"""
import threading
import locale
import sys
import types

# Exceptions
# https://www.python.org/dev/peps/pep-0249/#exceptions
Expand All @@ -31,6 +33,7 @@ def __init__(self):
self.lowercase = False
# Use the pre-determined separator - no locale access here
self.decimal_separator = _DEFAULT_DECIMAL_SEPARATOR
self.native_uuid = False # Default to False for backwards compatibility

# Global settings instance
_settings = Settings()
Expand All @@ -39,11 +42,8 @@ def __init__(self):
def get_settings():
"""Return the global settings object"""
with _settings_lock:
_settings.lowercase = lowercase
return _settings

lowercase = _settings.lowercase # Default is False

# Set the initial decimal separator in C++
from .ddbc_bindings import DDBCSetDecimalSeparator
DDBCSetDecimalSeparator(_settings.decimal_separator)
Expand Down Expand Up @@ -166,22 +166,8 @@ def pooling(max_size=100, idle_timeout=600, enabled=True):
else:
PoolingManager.enable(max_size, idle_timeout)

import sys
_original_module_setattr = sys.modules[__name__].__setattr__

def _custom_setattr(name, value):
if name == 'lowercase':
with _settings_lock:
_settings.lowercase = bool(value)
# Update the module's lowercase variable
_original_module_setattr(name, _settings.lowercase)
else:
_original_module_setattr(name, value)

# Replace the module's __setattr__ with our custom version
sys.modules[__name__].__setattr__ = _custom_setattr


# Export SQL constants at module level
SQL_CHAR = ConstantsDDBC.SQL_CHAR.value
SQL_VARCHAR = ConstantsDDBC.SQL_VARCHAR.value
Expand Down Expand Up @@ -257,3 +243,45 @@ def get_info_constants():
"""
return {name: member.value for name, member in GetInfoConstants.__members__.items()}

# Create a custom module class that uses properties instead of __setattr__
class _MSSQLModule(types.ModuleType):
@property
def native_uuid(self):
return _settings.native_uuid

@native_uuid.setter
def native_uuid(self, value):
if not isinstance(value, bool):
raise ValueError("native_uuid must be a boolean value")
with _settings_lock:
_settings.native_uuid = value

@property
def lowercase(self):
return _settings.lowercase

@lowercase.setter
def lowercase(self, value):
if not isinstance(value, bool):
raise ValueError("lowercase must be a boolean value")
with _settings_lock:
_settings.lowercase = value

# Replace the current module with our custom module class
old_module = sys.modules[__name__]
new_module = _MSSQLModule(__name__)

# Copy all existing attributes to the new module
for attr_name in dir(old_module):
if attr_name != "__class__":
try:
setattr(new_module, attr_name, getattr(old_module, attr_name))
except AttributeError:
pass

# Replace the module in sys.modules
sys.modules[__name__] = new_module

# Initialize property values
lowercase = _settings.lowercase
native_uuid = _settings.native_uuid
37 changes: 24 additions & 13 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1034,29 +1034,37 @@ def execute(
# After successful execution, initialize description if there are results
column_metadata = []
try:
# ODBC specification guarantees that column metadata is available immediately after
# a successful SQLExecute/SQLExecDirect for the first result set
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
self._initialize_description(column_metadata)
except Exception as e:
# If describe fails, it's likely there are no results (e.g., for INSERT)
self.description = None

# Reset rownumber for new result set (only for SELECT statements)
if self.description: # If we have column descriptions, it's likely a SELECT
# Capture settings snapshot for this result set
settings = get_settings()
self._settings_snapshot = {
'lowercase': settings.lowercase,
'native_uuid': settings.native_uuid
}
# Identify UUID columns based on Python type in description[1]
# This relies on _map_data_type correctly mapping SQL_GUID to uuid.UUID
self._uuid_indices = []
for i, desc in enumerate(self.description):
if desc and desc[1] == uuid.UUID: # Column type code at index 1
self._uuid_indices.append(i)
# Verify we have complete description tuples (7 items per PEP-249)
elif desc and len(desc) != 7:
log('warning', f"Column description at index {i} has incorrect tuple length: {len(desc)}")
self.rowcount = -1
self._reset_rownumber()
else:
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
self._clear_rownumber()

# After successful execution, initialize description if there are results
column_metadata = []
try:
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
self._initialize_description(column_metadata)
except Exception as e:
# If describe fails, it's likely there are no results (e.g., for INSERT)
self.description = None

self._reset_inputsizes() # Reset input sizes after execution
# Return self for method chaining
return self
Expand Down Expand Up @@ -1763,7 +1771,8 @@ def fetchone(self) -> Union[None, Row]:

# Create and return a Row object, passing column name map if available
column_map = getattr(self, '_column_name_map', None)
return Row(self, self.description, row_data, column_map)
settings_snapshot = getattr(self, '_settings_snapshot', None)
return Row(self, self.description, row_data, column_map, settings_snapshot)
except Exception as e:
# On error, don't increment rownumber - rethrow the error
raise e
Expand Down Expand Up @@ -1811,7 +1820,8 @@ def fetchmany(self, size: int = None) -> List[Row]:

# Convert raw data to Row objects
column_map = getattr(self, '_column_name_map', None)
return [Row(self, self.description, row_data, column_map) for row_data in rows_data]
settings_snapshot = getattr(self, '_settings_snapshot', None)
return [Row(self, self.description, row_data, column_map, settings_snapshot) for row_data in rows_data]
except Exception as e:
# On error, don't increment rownumber - rethrow the error
raise e
Expand Down Expand Up @@ -1849,7 +1859,8 @@ def fetchall(self) -> List[Row]:

# Convert raw data to Row objects
column_map = getattr(self, '_column_name_map', None)
return [Row(self, self.description, row_data, column_map) for row_data in rows_data]
settings_snapshot = getattr(self, '_settings_snapshot', None)
return [Row(self, self.description, row_data, column_map, settings_snapshot) for row_data in rows_data]
except Exception as e:
# On error, don't increment rownumber - rethrow the error
raise e
Expand Down
Loading