Skip to content

Commit 857b443

Browse files
authored
Merge branch 'main' into saumya/gh-458
2 parents 03e7b4f + 85f8659 commit 857b443

11 files changed

Lines changed: 1729 additions & 60 deletions

mssql_python/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,24 @@ def lowercase(self, value: bool) -> None:
339339
with _settings_lock:
340340
_settings.lowercase = value
341341

342+
@property
343+
def native_uuid(self) -> bool:
344+
"""Get the native_uuid setting.
345+
346+
Controls whether UNIQUEIDENTIFIER columns return uuid.UUID objects (True)
347+
or str (False). Default is True.
348+
Set to False to return str for pyodbc-compatible migration.
349+
"""
350+
return _settings.native_uuid
351+
352+
@native_uuid.setter
353+
def native_uuid(self, value: bool) -> None:
354+
"""Set the native_uuid setting."""
355+
if not isinstance(value, bool):
356+
raise ValueError("native_uuid must be a boolean value")
357+
with _settings_lock:
358+
_settings.native_uuid = value
359+
342360

343361
# Replace the current module with our custom module class
344362
old_module: types.ModuleType = sys.modules[__name__]
@@ -357,3 +375,4 @@ def lowercase(self, value: bool) -> None:
357375

358376
# Initialize property values
359377
lowercase: bool = _settings.lowercase
378+
native_uuid: bool = _settings.native_uuid

mssql_python/connection.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ def __init__(
203203
autocommit: bool = False,
204204
attrs_before: Optional[Dict[int, Union[int, str, bytes]]] = None,
205205
timeout: int = 0,
206+
native_uuid: Optional[bool] = None,
206207
**kwargs: Any,
207208
) -> None:
208209
"""
@@ -219,6 +220,9 @@ def __init__(
219220
connecting, such as SQL_ATTR_LOGIN_TIMEOUT,
220221
SQL_ATTR_ODBC_CURSORS, and SQL_ATTR_PACKET_SIZE.
221222
timeout (int): Login timeout in seconds. 0 means no timeout.
223+
native_uuid (bool, optional): Controls whether UNIQUEIDENTIFIER columns return
224+
uuid.UUID objects (True) or str (False) for cursors created from this connection.
225+
None (default) defers to the module-level ``mssql_python.native_uuid`` setting (True).
222226
**kwargs: Additional key/value pairs for the connection string.
223227
224228
Returns:
@@ -236,7 +240,16 @@ def __init__(
236240
>>> import mssql_python as ms
237241
>>> conn = ms.connect("Server=myserver;Database=mydb",
238242
... attrs_before={ms.SQL_ATTR_LOGIN_TIMEOUT: 30})
243+
244+
>>> # Return native uuid.UUID objects instead of strings
245+
>>> conn = ms.connect("Server=myserver;Database=mydb", native_uuid=True)
239246
"""
247+
# Store per-connection native_uuid override.
248+
# None means "use module-level mssql_python.native_uuid".
249+
if native_uuid is not None and not isinstance(native_uuid, bool):
250+
raise ValueError("native_uuid must be a boolean value or None")
251+
self._native_uuid = native_uuid
252+
240253
self.connection_str = self._construct_connection_string(connection_str, **kwargs)
241254
self._attrs_before = attrs_before or {}
242255

mssql_python/cursor.py

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ def __init__(self, connection: "Connection", timeout: int = 0) -> None:
135135

136136
self._cached_column_map = None
137137
self._cached_converter_map = None
138+
self._uuid_str_indices = None # Pre-computed UUID column indices for str conversion
139+
# Cache the effective native_uuid setting for this cursor's connection.
140+
# Resolution order: connection._native_uuid (if not None) → module-level setting.
141+
self._conn_native_uuid = getattr(self.connection, "_native_uuid", None)
138142
self._next_row_index = 0 # internal: index of the next row the driver will return (0-based)
139143
self._has_result_set = False # Track if we have an active result set
140144
self._skip_increment_for_next_fetch = (
@@ -1009,6 +1013,32 @@ def _build_converter_map(self):
10091013

10101014
return converter_map
10111015

1016+
def _compute_uuid_str_indices(self):
1017+
"""
1018+
Compute the tuple of column indices whose uuid.UUID values should be
1019+
stringified (as uppercase), based on the effective native_uuid setting.
1020+
1021+
Resolution order: connection-level (if set) → module-level (fallback).
1022+
1023+
Returns:
1024+
tuple of int or None: Column indices to stringify, or None when
1025+
native_uuid is True — meaning zero per-row overhead.
1026+
"""
1027+
if not self.description:
1028+
return None
1029+
1030+
effective_native_uuid = (
1031+
self._conn_native_uuid
1032+
if self._conn_native_uuid is not None
1033+
else get_settings().native_uuid
1034+
)
1035+
if not effective_native_uuid:
1036+
indices = tuple(
1037+
i for i, desc in enumerate(self.description) if desc and desc[1] is uuid.UUID
1038+
)
1039+
return indices if indices else None
1040+
return None
1041+
10121042
def _get_column_and_converter_maps(self):
10131043
"""
10141044
Get column map and converter map for Row construction (thread-safe).
@@ -1429,20 +1459,13 @@ def execute( # pylint: disable=too-many-locals,too-many-branches,too-many-state
14291459
col_desc[0]: i for i, col_desc in enumerate(self.description)
14301460
}
14311461
self._cached_converter_map = self._build_converter_map()
1462+
self._uuid_str_indices = self._compute_uuid_str_indices()
14321463
else:
14331464
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
14341465
self._clear_rownumber()
14351466
self._cached_column_map = None
14361467
self._cached_converter_map = None
1437-
1438-
# After successful execution, initialize description if there are results
1439-
column_metadata = []
1440-
try:
1441-
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
1442-
self._initialize_description(column_metadata)
1443-
except Exception as e:
1444-
# If describe fails, it's likely there are no results (e.g., for INSERT)
1445-
self.description = None
1468+
self._uuid_str_indices = None
14461469

14471470
self._reset_inputsizes() # Reset input sizes after execution
14481471
# Return self for method chaining
@@ -2283,14 +2306,29 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s
22832306
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
22842307
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
22852308
self.last_executed_stmt = operation
2286-
self._initialize_description()
2309+
2310+
# Fetch column metadata (e.g. for INSERT … OUTPUT)
2311+
column_metadata = []
2312+
try:
2313+
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
2314+
self._initialize_description(column_metadata)
2315+
except Exception: # pylint: disable=broad-exception-caught
2316+
self.description = None
22872317

22882318
if self.description:
22892319
self.rowcount = -1
22902320
self._reset_rownumber()
2321+
self._cached_column_map = {
2322+
col_desc[0]: i for i, col_desc in enumerate(self.description)
2323+
}
2324+
self._cached_converter_map = self._build_converter_map()
2325+
self._uuid_str_indices = self._compute_uuid_str_indices()
22912326
else:
22922327
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
22932328
self._clear_rownumber()
2329+
self._cached_column_map = None
2330+
self._cached_converter_map = None
2331+
self._uuid_str_indices = None
22942332
finally:
22952333
# Reset input sizes after execution
22962334
self._reset_inputsizes()
@@ -2338,7 +2376,13 @@ def fetchone(self) -> Union[None, Row]:
23382376

23392377
# Get column and converter maps
23402378
column_map, converter_map = self._get_column_and_converter_maps()
2341-
return Row(row_data, column_map, cursor=self, converter_map=converter_map)
2379+
return Row(
2380+
row_data,
2381+
column_map,
2382+
cursor=self,
2383+
converter_map=converter_map,
2384+
uuid_str_indices=self._uuid_str_indices,
2385+
)
23422386
except Exception as e:
23432387
# On error, don't increment rownumber - rethrow the error
23442388
raise e
@@ -2396,8 +2440,15 @@ def fetchmany(self, size: Optional[int] = None) -> List[Row]:
23962440
column_map, converter_map = self._get_column_and_converter_maps()
23972441

23982442
# Convert raw data to Row objects
2443+
uuid_idx = self._uuid_str_indices
23992444
return [
2400-
Row(row_data, column_map, cursor=self, converter_map=converter_map)
2445+
Row(
2446+
row_data,
2447+
column_map,
2448+
cursor=self,
2449+
converter_map=converter_map,
2450+
uuid_str_indices=uuid_idx,
2451+
)
24012452
for row_data in rows_data
24022453
]
24032454
except Exception as e:
@@ -2449,8 +2500,15 @@ def fetchall(self) -> List[Row]:
24492500
column_map, converter_map = self._get_column_and_converter_maps()
24502501

24512502
# Convert raw data to Row objects
2503+
uuid_idx = self._uuid_str_indices
24522504
return [
2453-
Row(row_data, column_map, cursor=self, converter_map=converter_map)
2505+
Row(
2506+
row_data,
2507+
column_map,
2508+
cursor=self,
2509+
converter_map=converter_map,
2510+
uuid_str_indices=uuid_idx,
2511+
)
24542512
for row_data in rows_data
24552513
]
24562514
except Exception as e:
@@ -2476,6 +2534,7 @@ def nextset(self) -> Union[bool, None]:
24762534
# Clear cached column and converter maps for the new result set
24772535
self._cached_column_map = None
24782536
self._cached_converter_map = None
2537+
self._uuid_str_indices = None
24792538

24802539
# Skip to the next result set
24812540
ret = ddbc_bindings.DDBCSQLMoreResults(self.hstmt)
@@ -2501,6 +2560,7 @@ def nextset(self) -> Union[bool, None]:
25012560
col_desc[0]: i for i, col_desc in enumerate(self.description)
25022561
}
25032562
self._cached_converter_map = self._build_converter_map()
2563+
self._uuid_str_indices = self._compute_uuid_str_indices()
25042564
except Exception as e: # pylint: disable=broad-exception-caught
25052565
# If describe fails, there might be no results in this result set
25062566
self.description = None

mssql_python/db_connection.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def connect(
1414
autocommit: bool = False,
1515
attrs_before: Optional[Dict[int, Union[int, str, bytes]]] = None,
1616
timeout: int = 0,
17+
native_uuid: Optional[bool] = None,
1718
**kwargs: Any,
1819
) -> Connection:
1920
"""
@@ -22,10 +23,18 @@ def connect(
2223
Args:
2324
connection_str (str): The connection string to connect to.
2425
autocommit (bool): If True, causes a commit to be performed after each SQL statement.
25-
TODO: Add the following parameters to the function signature:
26+
attrs_before (dict, optional): A dictionary of connection attributes to set before
27+
connecting.
2628
timeout (int): The timeout for the connection attempt, in seconds.
27-
readonly (bool): If True, the connection is set to read-only.
28-
attrs_before (dict): A dictionary of connection attributes to set before connecting.
29+
native_uuid (bool, optional): Controls whether UNIQUEIDENTIFIER columns return
30+
uuid.UUID objects (True) or str (False) for this connection.
31+
- True: UNIQUEIDENTIFIER columns return uuid.UUID objects.
32+
- False: UNIQUEIDENTIFIER columns return str (pyodbc-compatible).
33+
- None (default): Uses the module-level ``mssql_python.native_uuid`` setting (True).
34+
35+
This per-connection override is useful for migration from pyodbc:
36+
connections that need string UUIDs can pass native_uuid=False, while the default (True)
37+
returns native uuid.UUID objects.
2938
Keyword Args:
3039
**kwargs: Additional key/value pairs for the connection string.
3140
Below attributes are not implemented in the internal driver:
@@ -44,6 +53,11 @@ def connect(
4453
transactions, and closing the connection.
4554
"""
4655
conn = Connection(
47-
connection_str, autocommit=autocommit, attrs_before=attrs_before, timeout=timeout, **kwargs
56+
connection_str,
57+
autocommit=autocommit,
58+
attrs_before=attrs_before,
59+
timeout=timeout,
60+
native_uuid=native_uuid,
61+
**kwargs,
4862
)
4963
return conn

mssql_python/helpers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,13 +360,17 @@ class Settings:
360360
Settings class for mssql_python package configuration.
361361
362362
This class holds global settings that affect the behavior of the package,
363-
including lowercase column names, decimal separator.
363+
including lowercase column names, decimal separator, and UUID handling.
364364
"""
365365

366366
def __init__(self) -> None:
367367
self.lowercase: bool = False
368368
# Use the pre-determined separator - no locale access here
369369
self.decimal_separator: str = _default_decimal_separator
370+
# Controls whether UNIQUEIDENTIFIER columns return uuid.UUID (True)
371+
# or str (False). Default True returns native uuid.UUID objects.
372+
# Set to False to return str for pyodbc-compatible migration.
373+
self.native_uuid: bool = True
370374

371375

372376
# Global settings instance

mssql_python/mssql_python.pyi

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -129,21 +129,11 @@ class Row:
129129

130130
def __init__(
131131
self,
132-
cursor: "Cursor",
133-
description: List[
134-
Tuple[
135-
str,
136-
Any,
137-
Optional[int],
138-
Optional[int],
139-
Optional[int],
140-
Optional[int],
141-
Optional[bool],
142-
]
143-
],
144132
values: List[Any],
145-
column_map: Optional[Dict[str, int]] = None,
146-
settings_snapshot: Optional[Dict[str, Any]] = None,
133+
column_map: Dict[str, int],
134+
cursor: Optional["Cursor"] = None,
135+
converter_map: Optional[List[Any]] = None,
136+
uuid_str_indices: Optional[Tuple[int, ...]] = None,
147137
) -> None: ...
148138
def __getitem__(self, index: int) -> Any: ...
149139
def __getattr__(self, name: str) -> Any: ...
@@ -247,6 +237,7 @@ class Connection:
247237
autocommit: bool = False,
248238
attrs_before: Optional[Dict[int, Union[int, str, bytes]]] = None,
249239
timeout: int = 0,
240+
native_uuid: Optional[bool] = None,
250241
**kwargs: Any,
251242
) -> None: ...
252243

@@ -289,6 +280,7 @@ def connect(
289280
autocommit: bool = False,
290281
attrs_before: Optional[Dict[int, Union[int, str, bytes]]] = None,
291282
timeout: int = 0,
283+
native_uuid: Optional[bool] = None,
292284
**kwargs: Any,
293285
) -> Connection: ...
294286

0 commit comments

Comments
 (0)