Skip to content

Commit 64a671a

Browse files
fix: output converter lookup by SQL type code, not Python type
_build_converter_map looked up converters using desc[1] (the mapped Python type, e.g. bytes) but add_output_converter registers by SQL type code (e.g. -151). Added _column_sql_types to Cursor so the converter map tries the raw SQL code first, then falls back to Python type, then WVARCHAR. Also fixes the output converter test to register by SQL_SS_UDT (-151) instead of bytes, and adds docstrings to _get_c_type_for_sql_type and _map_data_type documenting the SQL Server-specific types.
1 parent fde7ea4 commit 64a671a

File tree

2 files changed

+37
-8
lines changed

2 files changed

+37
-8
lines changed

mssql_python/cursor.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,11 @@ def _reset_inputsizes(self) -> None:
844844
self._inputsizes = None
845845

846846
def _get_c_type_for_sql_type(self, sql_type: int) -> int:
847-
"""Map SQL type to appropriate C type for parameter binding"""
847+
"""Map SQL type to appropriate C type for parameter binding.
848+
849+
Includes SQL_SS_UDT(-151) for CLR UDTs (geography, geometry, hierarchyid),
850+
mapped to SQL_C_BINARY.
851+
"""
848852
sql_to_c_type = {
849853
ddbc_sql_const.SQL_CHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
850854
ddbc_sql_const.SQL_VARCHAR.value: ddbc_sql_const.SQL_C_CHAR.value,
@@ -943,9 +947,11 @@ def _initialize_description(self, column_metadata: Optional[Any] = None) -> None
943947
"""Initialize the description attribute from column metadata."""
944948
if not column_metadata:
945949
self.description = None
950+
self._column_sql_types = None
946951
return
947952

948953
description = []
954+
sql_types = []
949955
for _, col in enumerate(column_metadata):
950956
# Get column name - lowercase it if the lowercase flag is set
951957
column_name = col["ColumnName"]
@@ -954,6 +960,8 @@ def _initialize_description(self, column_metadata: Optional[Any] = None) -> None
954960
if get_settings().lowercase:
955961
column_name = column_name.lower()
956962

963+
sql_types.append(col["DataType"])
964+
957965
# Add to description tuple (7 elements as per PEP-249)
958966
description.append(
959967
(
@@ -967,6 +975,7 @@ def _initialize_description(self, column_metadata: Optional[Any] = None) -> None
967975
)
968976
)
969977
self.description = description
978+
self._column_sql_types = sql_types
970979

971980
def _build_converter_map(self):
972981
"""
@@ -982,14 +991,24 @@ def _build_converter_map(self):
982991
return None
983992

984993
converter_map = []
994+
raw_types = getattr(self, "_column_sql_types", None)
985995

986-
for desc in self.description:
996+
for i, desc in enumerate(self.description):
987997
if desc is None:
988998
converter_map.append(None)
989999
continue
990-
sql_type = desc[1]
991-
converter = self.connection.get_output_converter(sql_type)
992-
# If no converter found for the SQL type, try the WVARCHAR converter as a fallback
1000+
1001+
converter = None
1002+
1003+
# First try lookup by raw SQL type code (e.g. -151 for SQL_SS_UDT)
1004+
if raw_types and i < len(raw_types):
1005+
converter = self.connection.get_output_converter(raw_types[i])
1006+
1007+
# Fall back to lookup by Python type from description
1008+
if converter is None:
1009+
converter = self.connection.get_output_converter(desc[1])
1010+
1011+
# Last resort: try the WVARCHAR converter as a catch-all fallback
9931012
if converter is None:
9941013
from mssql_python.constants import ConstantsDDBC
9951014

@@ -1027,8 +1046,17 @@ def _map_data_type(self, sql_type):
10271046
"""
10281047
Map SQL data type to Python data type.
10291048
1049+
Maps the ODBC SQL type code returned by SQLDescribeCol to the
1050+
corresponding Python type for cursor.description[i][1].
1051+
1052+
SQL Server-specific types (from msodbcsql.h):
1053+
SQL_SS_UDT(-151) for CLR UDTs (geography, geometry, hierarchyid)
1054+
SQL_SS_XML(-152) for xml columns
1055+
SQL_SS_TIME2(-154) for time columns
1056+
SQL_DATETIMEOFFSET(-155) for datetimeoffset columns
1057+
10301058
Args:
1031-
sql_type: SQL data type.
1059+
sql_type: SQL data type code from SQLDescribeCol.
10321060
10331061
Returns:
10341062
Corresponding Python data type.

tests/test_017_spatial_types.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,15 @@ def geography_converter(value):
304304
return None
305305
return b"CONVERTED:" + value
306306

307-
db_connection.add_output_converter(bytes, geography_converter)
307+
SQL_SS_UDT = -151
308+
db_connection.add_output_converter(SQL_SS_UDT, geography_converter)
308309

309310
try:
310311
row = cursor.execute("SELECT geo_col FROM #geo_converter;").fetchone()
311312
assert isinstance(row[0], bytes)
312313
assert row[0].startswith(b"CONVERTED:")
313314
finally:
314-
db_connection.remove_output_converter(bytes)
315+
db_connection.remove_output_converter(SQL_SS_UDT)
315316

316317

317318
def test_geography_description_metadata(cursor, db_connection):

0 commit comments

Comments
 (0)