1616from mssql_python .helpers import check_error , log
1717from mssql_python import ddbc_bindings
1818from mssql_python .exceptions import InterfaceError , NotSupportedError , ProgrammingError
19- from .row import Row
19+ from mssql_python .row import Row
20+ from mssql_python import get_settings
2021
2122# Constants for string handling
2223MAX_INLINE_CHAR = 4000 # NVARCHAR/VARCHAR inline limit; this triggers NVARCHAR(MAX)/VARCHAR(MAX) + DAE
@@ -543,26 +544,32 @@ def _create_parameter_types_list(self, parameter, param_info, parameters_list, i
543544
544545 return paraminfo
545546
546- def _initialize_description (self ):
547- """
548- Initialize the description attribute using SQLDescribeCol.
549- """
550- col_metadata = []
551- ret = ddbc_bindings .DDBCSQLDescribeCol (self .hstmt , col_metadata )
552- check_error (ddbc_sql_const .SQL_HANDLE_STMT .value , self .hstmt , ret )
547+ def _initialize_description (self , column_metadata = None ):
548+ """Initialize the description attribute from column metadata."""
549+ if not column_metadata :
550+ self .description = None
551+ return
553552
554- self .description = [
555- (
556- col ["ColumnName" ],
557- self ._map_data_type (col ["DataType" ]),
558- None ,
559- col ["ColumnSize" ],
560- col ["ColumnSize" ],
561- col ["DecimalDigits" ],
562- col ["Nullable" ] == ddbc_sql_const .SQL_NULLABLE .value ,
563- )
564- for col in col_metadata
565- ]
553+ description = []
554+ for i , col in enumerate (column_metadata ):
555+ # Get column name - lowercase it if the lowercase flag is set
556+ column_name = col ["ColumnName" ]
557+
558+ # Use the current global setting to ensure tests pass correctly
559+ if get_settings ().lowercase :
560+ column_name = column_name .lower ()
561+
562+ # Add to description tuple (7 elements as per PEP-249)
563+ description .append ((
564+ column_name , # name
565+ self ._map_data_type (col ["DataType" ]), # type_code
566+ None , # display_size
567+ col ["ColumnSize" ], # internal_size
568+ col ["ColumnSize" ], # precision - should match ColumnSize
569+ col ["DecimalDigits" ], # scale
570+ col ["Nullable" ] == ddbc_sql_const .SQL_NULLABLE .value , # null_ok
571+ ))
572+ self .description = description
566573
567574 def _map_data_type (self , sql_type ):
568575 """
@@ -746,6 +753,16 @@ def execute(
746753 use_prepare: Whether to use SQLPrepareW (default) or SQLExecDirectW.
747754 reset_cursor: Whether to reset the cursor before execution.
748755 """
756+
757+ # Restore original fetch methods if they exist
758+ if hasattr (self , '_original_fetchone' ):
759+ self .fetchone = self ._original_fetchone
760+ self .fetchmany = self ._original_fetchmany
761+ self .fetchall = self ._original_fetchall
762+ del self ._original_fetchone
763+ del self ._original_fetchmany
764+ del self ._original_fetchall
765+
749766 self ._check_closed () # Check if the cursor is closed
750767 if reset_cursor :
751768 self ._reset_cursor ()
@@ -822,7 +839,14 @@ def execute(
822839 self .rowcount = ddbc_bindings .DDBCSQLRowCount (self .hstmt )
823840
824841 # Initialize description after execution
825- self ._initialize_description ()
842+ # After successful execution, initialize description if there are results
843+ column_metadata = []
844+ try :
845+ ddbc_bindings .DDBCSQLDescribeCol (self .hstmt , column_metadata )
846+ self ._initialize_description (column_metadata )
847+ except Exception as e :
848+ # If describe fails, it's likely there are no results (e.g., for INSERT)
849+ self .description = None
826850
827851 # Reset rownumber for new result set (only for SELECT statements)
828852 if self .description : # If we have column descriptions, it's likely a SELECT
@@ -975,7 +999,7 @@ def fetchone(self) -> Union[None, Row]:
975999
9761000 # Create and return a Row object, passing column name map if available
9771001 column_map = getattr (self , '_column_name_map' , None )
978- return Row (row_data , self .description , column_map )
1002+ return Row (self , self .description , row_data , column_map )
9791003 except Exception as e :
9801004 # On error, don't increment rownumber - rethrow the error
9811005 raise e
@@ -1017,7 +1041,7 @@ def fetchmany(self, size: int = None) -> List[Row]:
10171041
10181042 # Convert raw data to Row objects
10191043 column_map = getattr (self , '_column_name_map' , None )
1020- return [Row (row_data , self .description , column_map ) for row_data in rows_data ]
1044+ return [Row (self , self .description , row_data , column_map ) for row_data in rows_data ]
10211045 except Exception as e :
10221046 # On error, don't increment rownumber - rethrow the error
10231047 raise e
@@ -1049,7 +1073,7 @@ def fetchall(self) -> List[Row]:
10491073
10501074 # Convert raw data to Row objects
10511075 column_map = getattr (self , '_column_name_map' , None )
1052- return [Row (row_data , self .description , column_map ) for row_data in rows_data ]
1076+ return [Row (self , self .description , row_data , column_map ) for row_data in rows_data ]
10531077 except Exception as e :
10541078 # On error, don't increment rownumber - rethrow the error
10551079 raise e
@@ -1363,30 +1387,20 @@ def tables(self, table=None, catalog=None, schema=None, tableType=None):
13631387 Example: "TABLE" or ["TABLE", "VIEW"]
13641388
13651389 Returns:
1366- list: A list of Row objects containing table information with these columns:
1367- - table_cat: Catalog name
1368- - table_schem: Schema name
1369- - table_name: Table name
1370- - table_type: Table type (e.g., "TABLE", "VIEW")
1371- - remarks: Comments about the table
1372-
1373- Notes:
1374- This method only processes the standard five columns as defined in the ODBC
1375- specification. Any additional columns that might be returned by specific ODBC
1376- drivers are not included in the result set.
1377-
1390+ Cursor: The cursor object itself for method chaining with fetch methods.
1391+
13781392 Example:
13791393 # Get all tables in the database
1380- tables = cursor.tables()
1394+ tables = cursor.tables().fetchall()
13811395
13821396 # Get all tables in schema 'dbo'
1383- tables = cursor.tables(schema='dbo')
1397+ tables = cursor.tables(schema='dbo').fetchall()
13841398
13851399 # Get table named 'Customers'
1386- tables = cursor.tables(table='Customers')
1400+ tables = cursor.tables(table='Customers').fetchone()
13871401
1388- # Get all views
1389- tables = cursor.tables(tableType='VIEW')
1402+ # Get all views with fetchmany
1403+ tables = cursor.tables(tableType='VIEW').fetchmany(10)
13901404 """
13911405 self ._check_closed ()
13921406
@@ -1418,7 +1432,13 @@ def tables(self, table=None, catalog=None, schema=None, tableType=None):
14181432 try :
14191433 ddbc_bindings .DDBCSQLDescribeCol (self .hstmt , column_metadata )
14201434 self ._initialize_description (column_metadata )
1421- except Exception :
1435+ except InterfaceError as e :
1436+ log ('error' , f"Driver interface error during metadata retrieval: { e } " )
1437+ except Exception as e :
1438+ # Log the exception with appropriate context
1439+ log ('error' , f"Failed to retrieve column metadata: { e } . Using standard ODBC column definitions instead." )
1440+
1441+ if not self .description :
14221442 # If describe fails, create a manual description for the standard columns
14231443 column_types = [str , str , str , str , str ]
14241444 self .description = [
@@ -1428,23 +1448,54 @@ def tables(self, table=None, catalog=None, schema=None, tableType=None):
14281448 ("table_type" , column_types [3 ], None , 128 , 128 , 0 , False ),
14291449 ("remarks" , column_types [4 ], None , 254 , 254 , 0 , True )
14301450 ]
1431-
1432- # Define column names in ODBC standard order
1433- column_names = [
1434- "table_cat" , "table_schem" , "table_name" , "table_type" , "remarks"
1435- ]
1436-
1437- # Fetch all rows
1438- rows_data = []
1439- ddbc_bindings .DDBCSQLFetchAll (self .hstmt , rows_data )
1440-
1441- # Create a column map for attribute access
1442- column_map = {name : i for i , name in enumerate (column_names )}
1443-
1444- # Create Row objects with the column map
1445- result_rows = []
1446- for row_data in rows_data :
1447- row = Row (row_data , self .description , column_map )
1448- result_rows .append (row )
1449-
1450- return result_rows
1451+
1452+ # Store the column mappings for this specific tables() call
1453+ column_names = [desc [0 ] for desc in self .description ]
1454+
1455+ # Create a specialized column map for this result set
1456+ columns_map = {}
1457+ for i , name in enumerate (column_names ):
1458+ columns_map [name ] = i
1459+ columns_map [name .lower ()] = i
1460+
1461+ # Define wrapped fetch methods that preserve existing column mapping
1462+ # but add our specialized mapping just for column results
1463+ def fetchone_with_columns_mapping ():
1464+ row = self ._original_fetchone ()
1465+ if row is not None :
1466+ # Create a merged map with columns result taking precedence
1467+ merged_map = getattr (row , '_column_map' , {}).copy ()
1468+ merged_map .update (columns_map )
1469+ row ._column_map = merged_map
1470+ return row
1471+
1472+ def fetchmany_with_columns_mapping (size = None ):
1473+ rows = self ._original_fetchmany (size )
1474+ for row in rows :
1475+ # Create a merged map with columns result taking precedence
1476+ merged_map = getattr (row , '_column_map' , {}).copy ()
1477+ merged_map .update (columns_map )
1478+ row ._column_map = merged_map
1479+ return rows
1480+
1481+ def fetchall_with_columns_mapping ():
1482+ rows = self ._original_fetchall ()
1483+ for row in rows :
1484+ # Create a merged map with columns result taking precedence
1485+ merged_map = getattr (row , '_column_map' , {}).copy ()
1486+ merged_map .update (columns_map )
1487+ row ._column_map = merged_map
1488+ return rows
1489+
1490+ # Save original fetch methods
1491+ if not hasattr (self , '_original_fetchone' ):
1492+ self ._original_fetchone = self .fetchone
1493+ self ._original_fetchmany = self .fetchmany
1494+ self ._original_fetchall = self .fetchall
1495+
1496+ # Override fetch methods with our wrapped versions
1497+ self .fetchone = fetchone_with_columns_mapping
1498+ self .fetchmany = fetchmany_with_columns_mapping
1499+ self .fetchall = fetchall_with_columns_mapping
1500+
1501+ return self
0 commit comments