Summary
Connection.add_output_converter(sqltype, func) is documented to take an integer ODBC SQL type code (pyodbc-compatible signature), but at fetch time the converter is looked up by the Python type stored in cursor.description[i][1]. As a result, registering a converter with an integer SQL type constant (for example SQL_DECIMAL) silently no-ops, and the value comes back unconverted. Only registering the Python type (for example decimal.Decimal) works.
Verified against mssql-python 1.11.0.
Repro
import mssql_python
import decimal
conn = mssql_python.connect(connection_string)
# Documented style: register by integer SQL type constant -> silently ignored
conn.add_output_converter(mssql_python.SQL_DECIMAL, lambda v: None if v is None else float(v))
cur = conn.cursor()
cur.execute("SELECT TOP 1 ListPrice FROM Production.Product WHERE ListPrice > 0")
print(type(cur.fetchone()[0])) # <class 'decimal.Decimal'> (converter never ran)
# Works only when keyed by the Python type
conn.add_output_converter(decimal.Decimal, lambda v: None if v is None else float(v))
cur.execute("SELECT TOP 1 ListPrice FROM Production.Product WHERE ListPrice > 0")
print(type(cur.fetchone()[0])) # <class 'float'> (fires for money and decimal columns)
Expected
Either:
- the integer SQL type key documented in the API works, or
- the docstring reflects that the key is a Python type.
Ideally the method accepts both an integer SQL type code and a Python type (see suggested fix).
Actual
- Registering an integer SQL type constant is stored but never matched, so no conversion occurs.
- Registering the Python type works, and (because many SQL types collapse to one Python type) a single
decimal.Decimal converter fires for decimal, numeric, money, and smallmoney columns alike.
Root cause
The description type code is a Python type, not the ODBC integer code. Cursor._map_data_type maps the ODBC SQL type code to a Python type for cursor.description[i][1]:
# cursor.py, _map_data_type
sql_to_python_type = {
ddbc_sql_const.SQL_DECIMAL.value: decimal.Decimal,
ddbc_sql_const.SQL_NUMERIC.value: decimal.Decimal,
ddbc_sql_const.SQL_INTEGER.value: int,
ddbc_sql_const.SQL_VARCHAR.value: str,
# ...
}
Cursor._build_converter_map then looks up the converter using that Python type:
# cursor.py, _build_converter_map
sql_type = desc[1] # this is a Python type, e.g. decimal.Decimal
converter = self.connection.get_output_converter(sql_type)
But add_output_converter documents and stores an integer key:
# connection.py
def add_output_converter(self, sqltype: int, func):
"""
Args:
sqltype (int): The integer SQL type value to convert ... (e.g. SQL_VARCHAR)
...
"""
self._output_converters[sqltype] = func
So the stored integer key can never match the Python-type lookup key. Note the sibling methods already hint at the intended design: get_output_converter and remove_output_converter are typed sqltype: Union[int, type].
A secondary docstring inaccuracy: add_output_converter says the value passed to the converter "will be a bytes object", but in practice the converter receives the already-decoded Python object (for example a decimal.Decimal), not raw bytes.
Suggested fix — support both int and Python types
Could add_output_converter accept both an integer SQL type code and a Python type? A minimal approach: when an integer SQL type code is registered, also index it under the corresponding Python type using the same SQL type code -> Python type mapping that _map_data_type uses (or have _build_converter_map fall back to the integer code for the column). That would make the documented pyodbc-style integer keys work while keeping the current Python-type keys, and it would preserve pyodbc source compatibility.
If integer keys are intentionally unsupported, then the add_output_converter docstring and signature should be updated to document the Python-type key (Union[int, type], matching the getters) and to correct the "bytes object" wording.
Summary
Connection.add_output_converter(sqltype, func)is documented to take an integer ODBC SQL type code (pyodbc-compatible signature), but at fetch time the converter is looked up by the Python type stored incursor.description[i][1]. As a result, registering a converter with an integer SQL type constant (for exampleSQL_DECIMAL) silently no-ops, and the value comes back unconverted. Only registering the Python type (for exampledecimal.Decimal) works.Verified against
mssql-python1.11.0.Repro
Expected
Either:
Ideally the method accepts both an integer SQL type code and a Python type (see suggested fix).
Actual
decimal.Decimalconverter fires fordecimal,numeric,money, andsmallmoneycolumns alike.Root cause
The description type code is a Python type, not the ODBC integer code.
Cursor._map_data_typemaps the ODBC SQL type code to a Python type forcursor.description[i][1]:Cursor._build_converter_mapthen looks up the converter using that Python type:But
add_output_converterdocuments and stores an integer key:So the stored integer key can never match the Python-type lookup key. Note the sibling methods already hint at the intended design:
get_output_converterandremove_output_converterare typedsqltype: Union[int, type].A secondary docstring inaccuracy:
add_output_convertersays the value passed to the converter "will be a bytes object", but in practice the converter receives the already-decoded Python object (for example adecimal.Decimal), not raw bytes.Suggested fix — support both int and Python types
Could
add_output_converteraccept both an integer SQL type code and a Python type? A minimal approach: when an integer SQL type code is registered, also index it under the corresponding Python type using the sameSQL type code -> Python typemapping that_map_data_typeuses (or have_build_converter_mapfall back to the integer code for the column). That would make the documented pyodbc-style integer keys work while keeping the current Python-type keys, and it would preserve pyodbc source compatibility.If integer keys are intentionally unsupported, then the
add_output_converterdocstring and signature should be updated to document the Python-type key (Union[int, type], matching the getters) and to correct the "bytes object" wording.