Skip to content

Commit 10a8815

Browse files
FEAT: Support for Native_UUID Attribute (#282)
### 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#39059](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/39059) ------------------------------------------------------------------- ### Summary This pull request introduces a new global setting, `native_uuid`, to the `mssql_python` package, allowing users to control whether UUID values are returned as `uuid.UUID` objects or as strings. The implementation includes updates to the package initialization, row processing logic, and a comprehensive set of tests to verify the new behavior and ensure backward compatibility. **UUID Handling Improvements:** * Added a `native_uuid` setting to the global configuration in `mssql_python/__init__.py`, defaulting to `False` for backward compatibility. This setting controls whether UUIDs are returned as `uuid.UUID` objects or as strings. * Updated the `Row` class in `mssql_python/row.py` to check the `native_uuid` setting and convert UUID values to strings when `native_uuid` is `False`, ensuring consistent output based on configuration. **Testing Enhancements:** * Updated and extended tests in `tests/test_004_cursor.py` to verify correct UUID handling for both `native_uuid=True` and `native_uuid=False`, including new tests for the setting and resetting of the `native_uuid` option. **Internal Refactoring:** * Removed unused UUID mapping logic in `mssql_python/cursor.py` that is now handled via the new setting and row processing logic. * Minor import and code organization cleanups in affected modules for clarity and maintainability. These changes provide greater flexibility and control over how UUIDs are handled in query results, improving the usability of the package in different application contexts. --------- Co-authored-by: gargsaumya <saumyagarg.100@gmail.com>
1 parent cd828b6 commit 10a8815

File tree

5 files changed

+401
-91
lines changed

5 files changed

+401
-91
lines changed

mssql_python/__init__.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"""
66
import threading
77
import locale
8+
import sys
9+
import types
810

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

3538
# Global settings instance
3639
_settings = Settings()
@@ -39,11 +42,8 @@ def __init__(self):
3942
def get_settings():
4043
"""Return the global settings object"""
4144
with _settings_lock:
42-
_settings.lowercase = lowercase
4345
return _settings
4446

45-
lowercase = _settings.lowercase # Default is False
46-
4747
# Set the initial decimal separator in C++
4848
from .ddbc_bindings import DDBCSetDecimalSeparator
4949
DDBCSetDecimalSeparator(_settings.decimal_separator)
@@ -166,22 +166,8 @@ def pooling(max_size=100, idle_timeout=600, enabled=True):
166166
else:
167167
PoolingManager.enable(max_size, idle_timeout)
168168

169-
import sys
170169
_original_module_setattr = sys.modules[__name__].__setattr__
171170

172-
def _custom_setattr(name, value):
173-
if name == 'lowercase':
174-
with _settings_lock:
175-
_settings.lowercase = bool(value)
176-
# Update the module's lowercase variable
177-
_original_module_setattr(name, _settings.lowercase)
178-
else:
179-
_original_module_setattr(name, value)
180-
181-
# Replace the module's __setattr__ with our custom version
182-
sys.modules[__name__].__setattr__ = _custom_setattr
183-
184-
185171
# Export SQL constants at module level
186172
SQL_CHAR = ConstantsDDBC.SQL_CHAR.value
187173
SQL_VARCHAR = ConstantsDDBC.SQL_VARCHAR.value
@@ -257,3 +243,45 @@ def get_info_constants():
257243
"""
258244
return {name: member.value for name, member in GetInfoConstants.__members__.items()}
259245

246+
# Create a custom module class that uses properties instead of __setattr__
247+
class _MSSQLModule(types.ModuleType):
248+
@property
249+
def native_uuid(self):
250+
return _settings.native_uuid
251+
252+
@native_uuid.setter
253+
def native_uuid(self, value):
254+
if not isinstance(value, bool):
255+
raise ValueError("native_uuid must be a boolean value")
256+
with _settings_lock:
257+
_settings.native_uuid = value
258+
259+
@property
260+
def lowercase(self):
261+
return _settings.lowercase
262+
263+
@lowercase.setter
264+
def lowercase(self, value):
265+
if not isinstance(value, bool):
266+
raise ValueError("lowercase must be a boolean value")
267+
with _settings_lock:
268+
_settings.lowercase = value
269+
270+
# Replace the current module with our custom module class
271+
old_module = sys.modules[__name__]
272+
new_module = _MSSQLModule(__name__)
273+
274+
# Copy all existing attributes to the new module
275+
for attr_name in dir(old_module):
276+
if attr_name != "__class__":
277+
try:
278+
setattr(new_module, attr_name, getattr(old_module, attr_name))
279+
except AttributeError:
280+
pass
281+
282+
# Replace the module in sys.modules
283+
sys.modules[__name__] = new_module
284+
285+
# Initialize property values
286+
lowercase = _settings.lowercase
287+
native_uuid = _settings.native_uuid

mssql_python/cursor.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,29 +1034,37 @@ def execute(
10341034
# After successful execution, initialize description if there are results
10351035
column_metadata = []
10361036
try:
1037+
# ODBC specification guarantees that column metadata is available immediately after
1038+
# a successful SQLExecute/SQLExecDirect for the first result set
10371039
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
10381040
self._initialize_description(column_metadata)
10391041
except Exception as e:
10401042
# If describe fails, it's likely there are no results (e.g., for INSERT)
10411043
self.description = None
1042-
1044+
10431045
# Reset rownumber for new result set (only for SELECT statements)
10441046
if self.description: # If we have column descriptions, it's likely a SELECT
1047+
# Capture settings snapshot for this result set
1048+
settings = get_settings()
1049+
self._settings_snapshot = {
1050+
'lowercase': settings.lowercase,
1051+
'native_uuid': settings.native_uuid
1052+
}
1053+
# Identify UUID columns based on Python type in description[1]
1054+
# This relies on _map_data_type correctly mapping SQL_GUID to uuid.UUID
1055+
self._uuid_indices = []
1056+
for i, desc in enumerate(self.description):
1057+
if desc and desc[1] == uuid.UUID: # Column type code at index 1
1058+
self._uuid_indices.append(i)
1059+
# Verify we have complete description tuples (7 items per PEP-249)
1060+
elif desc and len(desc) != 7:
1061+
log('warning', f"Column description at index {i} has incorrect tuple length: {len(desc)}")
10451062
self.rowcount = -1
10461063
self._reset_rownumber()
10471064
else:
10481065
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
10491066
self._clear_rownumber()
10501067

1051-
# After successful execution, initialize description if there are results
1052-
column_metadata = []
1053-
try:
1054-
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
1055-
self._initialize_description(column_metadata)
1056-
except Exception as e:
1057-
# If describe fails, it's likely there are no results (e.g., for INSERT)
1058-
self.description = None
1059-
10601068
self._reset_inputsizes() # Reset input sizes after execution
10611069
# Return self for method chaining
10621070
return self
@@ -1763,7 +1771,8 @@ def fetchone(self) -> Union[None, Row]:
17631771

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

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

18501860
# Convert raw data to Row objects
18511861
column_map = getattr(self, '_column_name_map', None)
1852-
return [Row(self, self.description, row_data, column_map) for row_data in rows_data]
1862+
settings_snapshot = getattr(self, '_settings_snapshot', None)
1863+
return [Row(self, self.description, row_data, column_map, settings_snapshot) for row_data in rows_data]
18531864
except Exception as e:
18541865
# On error, don't increment rownumber - rethrow the error
18551866
raise e

0 commit comments

Comments
 (0)