33
44class 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