Skip to content

add_output_converter documents/accepts an integer SQL type, but dispatch keys on the Python type (integer keys silently never fire) #684

Description

@dlevy-msft-sql

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    triage neededFor new issues, not triaged yet.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions