Skip to content

Commit ee5b295

Browse files
committed
Resolving conflicts
1 parent 5ddd661 commit ee5b295

File tree

4 files changed

+221
-83
lines changed

4 files changed

+221
-83
lines changed

mssql_python/__init__.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ def __init__(self):
4141
def get_settings():
4242
"""Return the global settings object"""
4343
with _settings_lock:
44-
_settings.lowercase = lowercase
45-
_settings.native_uuid = native_uuid
44+
_settings.lowercase = bool(lowercase)
45+
_settings.native_uuid = bool(native_uuid)
4646
return _settings
4747

4848
lowercase = _settings.lowercase # Default is False
@@ -174,10 +174,24 @@ def pooling(max_size=100, idle_timeout=600, enabled=True):
174174

175175
def _custom_setattr(name, value):
176176
if name == 'lowercase':
177+
# Strict boolean type check for lowercase
178+
if not isinstance(value, bool):
179+
raise ValueError("lowercase must be a boolean value (True or False)")
180+
177181
with _settings_lock:
178-
_settings.lowercase = bool(value)
182+
_settings.lowercase = value
179183
# Update the module's lowercase variable
180184
_original_module_setattr(name, _settings.lowercase)
185+
elif name == 'native_uuid':
186+
187+
# Strict boolean type check for native_uuid
188+
if not isinstance(value, bool):
189+
raise ValueError("native_uuid must be a boolean value (True or False)")
190+
191+
with _settings_lock:
192+
_settings.native_uuid = value
193+
# Update the module's native_uuid variable
194+
_original_module_setattr(name, _settings.native_uuid)
181195
else:
182196
_original_module_setattr(name, value)
183197

mssql_python/cursor.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,24 +1005,26 @@ def execute(
10051005
except Exception as e:
10061006
# If describe fails, it's likely there are no results (e.g., for INSERT)
10071007
self.description = None
1008-
1008+
10091009
# Reset rownumber for new result set (only for SELECT statements)
10101010
if self.description: # If we have column descriptions, it's likely a SELECT
1011+
# Capture settings snapshot for this result set
1012+
settings = get_settings()
1013+
self._settings_snapshot = {
1014+
'lowercase': settings.lowercase,
1015+
'native_uuid': settings.native_uuid
1016+
}
1017+
# Identify UUID columns (SQL_GUID = -11)
1018+
self._uuid_indices = []
1019+
for i, desc in enumerate(self.description):
1020+
if desc and desc[1] == uuid.UUID: # Column type code at index 1
1021+
self._uuid_indices.append(i)
10111022
self.rowcount = -1
10121023
self._reset_rownumber()
10131024
else:
10141025
self.rowcount = ddbc_bindings.DDBCSQLRowCount(self.hstmt)
10151026
self._clear_rownumber()
10161027

1017-
# After successful execution, initialize description if there are results
1018-
column_metadata = []
1019-
try:
1020-
ddbc_bindings.DDBCSQLDescribeCol(self.hstmt, column_metadata)
1021-
self._initialize_description(column_metadata)
1022-
except Exception as e:
1023-
# If describe fails, it's likely there are no results (e.g., for INSERT)
1024-
self.description = None
1025-
10261028
self._reset_inputsizes() # Reset input sizes after execution
10271029
# Return self for method chaining
10281030
return self
@@ -1729,7 +1731,8 @@ def fetchone(self) -> Union[None, Row]:
17291731

17301732
# Create and return a Row object, passing column name map if available
17311733
column_map = getattr(self, '_column_name_map', None)
1732-
return Row(self, self.description, row_data, column_map)
1734+
settings_snapshot = getattr(self, '_settings_snapshot', None)
1735+
return Row(self, self.description, row_data, column_map, settings_snapshot)
17331736
except Exception as e:
17341737
# On error, don't increment rownumber - rethrow the error
17351738
raise e
@@ -1777,7 +1780,8 @@ def fetchmany(self, size: int = None) -> List[Row]:
17771780

17781781
# Convert raw data to Row objects
17791782
column_map = getattr(self, '_column_name_map', None)
1780-
return [Row(self, self.description, row_data, column_map) for row_data in rows_data]
1783+
settings_snapshot = getattr(self, '_settings_snapshot', None)
1784+
return [Row(self, self.description, row_data, column_map, settings_snapshot) for row_data in rows_data]
17811785
except Exception as e:
17821786
# On error, don't increment rownumber - rethrow the error
17831787
raise e
@@ -1815,7 +1819,8 @@ def fetchall(self) -> List[Row]:
18151819

18161820
# Convert raw data to Row objects
18171821
column_map = getattr(self, '_column_name_map', None)
1818-
return [Row(self, self.description, row_data, column_map) for row_data in rows_data]
1822+
settings_snapshot = getattr(self, '_settings_snapshot', None)
1823+
return [Row(self, self.description, row_data, column_map, settings_snapshot) for row_data in rows_data]
18191824
except Exception as e:
18201825
# On error, don't increment rownumber - rethrow the error
18211826
raise e

mssql_python/row.py

Lines changed: 99 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,10 @@
33

44
class Row:
55
"""
6-
A row of data from a cursor fetch operation. Provides both tuple-like indexing
7-
and attribute access to column values.
8-
9-
Column attribute access behavior depends on the global 'lowercase' setting:
10-
- When enabled: Case-insensitive attribute access
11-
- When disabled (default): Case-sensitive attribute access matching original column names
12-
13-
Example:
14-
row = cursor.fetchone()
15-
print(row[0]) # Access by index
16-
print(row.column_name) # Access by column name (case sensitivity varies)
6+
A row of data from a cursor fetch operation.
177
"""
188

19-
def __init__(self, cursor, description, values, column_map=None):
9+
def __init__(self, cursor, description, values, column_map=None, settings_snapshot=None):
2010
"""
2111
Initialize a Row object with values and description.
2212
@@ -25,46 +15,98 @@ def __init__(self, cursor, description, values, column_map=None):
2515
description: The cursor description containing column metadata
2616
values: List of values for this row
2717
column_map: Optional pre-built column map (for optimization)
18+
settings_snapshot: Settings snapshot from cursor to ensure consistency
2819
"""
2920
self._cursor = cursor
3021
self._description = description
3122

23+
# Use settings snapshot if provided, otherwise fallback to global settings
24+
if settings_snapshot is not None:
25+
self._settings = settings_snapshot
26+
else:
27+
settings = get_settings()
28+
self._settings = {
29+
'lowercase': settings.lowercase,
30+
'native_uuid': settings.native_uuid
31+
}
32+
# Create mapping of column names to indices first
33+
# If column_map is not provided, build it from description
34+
if column_map is None:
35+
self._column_map = {}
36+
for i, col_desc in enumerate(description):
37+
if col_desc: # Ensure column description exists
38+
col_name = col_desc[0] # Name is first item in description tuple
39+
if self._settings.get('lowercase'):
40+
col_name = col_name.lower()
41+
self._column_map[col_name] = i
42+
else:
43+
self._column_map = column_map
44+
45+
# First make a mutable copy of values
46+
processed_values = list(values)
47+
3248
# Apply output converters if available
3349
if hasattr(cursor.connection, '_output_converters') and cursor.connection._output_converters:
34-
self._values = self._apply_output_converters(values)
35-
else:
36-
self._values = self._process_uuid_values(values, description)
37-
50+
processed_values = self._apply_output_converters(processed_values)
51+
52+
# Process UUID values using the snapshotted setting
53+
self._values = self._process_uuid_values(processed_values, description)
54+
3855
def _process_uuid_values(self, values, description):
3956
"""
40-
Convert UUID objects to strings if native_uuid setting is False.
57+
Convert string UUIDs to uuid.UUID objects if native_uuid setting is True,
58+
or ensure UUIDs are returned as strings if False.
4159
"""
42-
if get_settings().native_uuid:
60+
import uuid
61+
62+
# Use the snapshot setting for native_uuid
63+
native_uuid = self._settings.get('native_uuid')
64+
65+
# Early return if no conversion needed
66+
if not native_uuid and not any(isinstance(v, uuid.UUID) for v in values):
4367
return values
44-
processed_values = []
45-
for i, value in enumerate(values):
46-
if i < len(description) and description[i] and isinstance(value, uuid.UUID):
47-
processed_values.append(str(value))
68+
69+
# Get pre-identified UUID indices from cursor if available
70+
uuid_indices = getattr(self._cursor, '_uuid_indices', None)
71+
processed_values = list(values) # Create a copy to modify
72+
73+
# Process only UUID columns when native_uuid is True
74+
if native_uuid:
75+
# If we have pre-identified UUID columns
76+
if uuid_indices is not None:
77+
for i in uuid_indices:
78+
if i < len(processed_values) and processed_values[i] is not None:
79+
value = processed_values[i]
80+
if isinstance(value, str):
81+
try:
82+
# Remove braces if present
83+
clean_value = value.strip('{}')
84+
processed_values[i] = uuid.UUID(clean_value)
85+
except (ValueError, AttributeError):
86+
pass # Keep original if conversion fails
87+
# Fallback to scanning all columns if indices weren't pre-identified
4888
else:
49-
processed_values.append(value)
50-
return processed_values
89+
for i, value in enumerate(processed_values):
90+
if value is None:
91+
continue
92+
93+
if i < len(description) and description[i]:
94+
# Check SQL type for UNIQUEIDENTIFIER (-11)
95+
sql_type = description[i][1]
96+
if sql_type == -11: # SQL_GUID
97+
if isinstance(value, str):
98+
try:
99+
processed_values[i] = uuid.UUID(value.strip('{}'))
100+
except (ValueError, AttributeError):
101+
pass
102+
# When native_uuid is False, convert UUID objects to strings
103+
else:
104+
for i, value in enumerate(processed_values):
105+
if isinstance(value, uuid.UUID):
106+
processed_values[i] = str(value)
51107

52-
# TODO: ADO task - Optimize memory usage by sharing column map across rows
53-
# Instead of storing the full cursor_description in each Row object:
54-
# 1. Build the column map once at the cursor level after setting description
55-
# 2. Pass only this map to each Row instance
56-
# 3. Remove cursor_description from Row objects entirely
108+
return processed_values
57109

58-
# Create mapping of column names to indices
59-
# If column_map is not provided, build it from description
60-
if column_map is None:
61-
column_map = {}
62-
for i, col_desc in enumerate(description):
63-
col_name = col_desc[0] # Name is first item in description tuple
64-
column_map[col_name] = i
65-
66-
self._column_map = column_map
67-
68110
def _apply_output_converters(self, values):
69111
"""
70112
Apply output converters to raw values.
@@ -100,17 +142,22 @@ def _apply_output_converters(self, values):
100142
if converter:
101143
try:
102144
# If value is already a Python type (str, int, etc.),
103-
# we need to convert it to bytes for our converters
145+
# we need to handle it appropriately
104146
if isinstance(value, str):
105147
# Encode as UTF-16LE for string values (SQL_WVARCHAR format)
106148
value_bytes = value.encode('utf-16-le')
107149
converted_values[i] = converter(value_bytes)
150+
elif isinstance(value, int):
151+
# For integers, we'll convert to bytes
152+
value_bytes = value.to_bytes(8, byteorder='little')
153+
converted_values[i] = converter(value_bytes)
108154
else:
155+
# Pass the value directly for other types
109156
converted_values[i] = converter(value)
110-
except Exception:
157+
except Exception as e:
111158
# Log the exception for debugging without leaking sensitive data
112159
if hasattr(self._cursor, 'log'):
113-
self._cursor.log('debug', 'Exception occurred in output converter', exc_info=True)
160+
self._cursor.log('debug', f'Exception occurred in output converter: {type(e).__name__}', exc_info=True)
114161
# If conversion fails, keep the original value
115162
pass
116163

@@ -123,24 +170,21 @@ def __getitem__(self, index):
123170
def __getattr__(self, name):
124171
"""
125172
Allow accessing by column name as attribute: row.column_name
126-
127-
Note: Case sensitivity depends on the global 'lowercase' setting:
128-
- When lowercase=True: Column names are stored in lowercase, enabling
129-
case-insensitive attribute access (e.g., row.NAME, row.name, row.Name all work).
130-
- When lowercase=False (default): Column names preserve original casing,
131-
requiring exact case matching for attribute access.
132173
"""
133-
# Handle lowercase attribute access - if lowercase is enabled,
134-
# try to match attribute names case-insensitively
174+
# _column_map should already be set in __init__, but check to be safe
175+
if not hasattr(self, '_column_map'):
176+
self._column_map = {}
177+
178+
# Try direct lookup first
135179
if name in self._column_map:
136180
return self._values[self._column_map[name]]
137181

138-
# If lowercase is enabled on the cursor, try case-insensitive lookup
139-
if hasattr(self._cursor, 'lowercase') and self._cursor.lowercase:
182+
# Use the snapshot lowercase setting instead of global
183+
if self._settings.get('lowercase'):
184+
# If lowercase is enabled, try case-insensitive lookup
140185
name_lower = name.lower()
141-
for col_name in self._column_map:
142-
if col_name.lower() == name_lower:
143-
return self._values[self._column_map[col_name]]
186+
if name_lower in self._column_map:
187+
return self._values[self._column_map[name_lower]]
144188

145189
raise AttributeError(f"Row has no attribute '{name}'")
146190

0 commit comments

Comments
 (0)