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