Skip to content

GetInfoConstants: 10 information types carry the wrong ODBC id, so getinfo() silently returns the wrong information #769

Description

Describe the bug

Ten entries in mssql_python.constants.GetInfoConstants carry the wrong numeric
ODBC information-type id, and several more are not ODBC information types at all.
Because Connection.getinfo() passes the integer straight through to
SQLGetInfo, each of these returns a different, valid-looking value with no
error
. There is nothing to alert a caller: the return code is success and the
value has a plausible type and magnitude.

Two of the mistakes also make correct information types unreachable through
the enum: SQL_TIMEDATE_FUNCTIONS (52) and SQL_STATIC_CURSOR_ATTRIBUTES1/2
(167/168).

Values below are from the ODBC headers (sql.h / sqlext.h, unixODBC 2.3.x;
identical in the Microsoft SDK).

Wrong ids

Constant Declared Correct What the declared id actually is
SQL_SYSTEM_FUNCTIONS 58 51 SQL_CONVERT_DECIMAL
SQL_DATETIME_FUNCTIONS 51 52, and the ODBC name is SQL_TIMEDATE_FUNCTIONS SQL_SYSTEM_FUNCTIONS
SQL_OJ_CAPABILITIES 65 115 SQL_CONVERT_SMALLINT
SQL_DRIVER_HDBC 74 3 SQL_CORRELATION_NAME
SQL_DRIVER_HENV 75 4 SQL_NON_NULLABLE_COLUMNS
SQL_KEYSET_CURSOR_ATTRIBUTES1 148 150 SQL_INDEX_KEYWORDS
SQL_KEYSET_CURSOR_ATTRIBUTES2 149 151 SQL_INFO_SCHEMA_VIEWS
SQL_STATIC_CURSOR_ATTRIBUTES1 150 167 SQL_KEYSET_CURSOR_ATTRIBUTES1
SQL_STATIC_CURSOR_ATTRIBUTES2 151 168 SQL_KEYSET_CURSOR_ATTRIBUTES2
SQL_CATALOG_NAME 10002 10003 SQL_DESCRIBE_PARAMETER
SQL_DESCRIBE_PARAMETER 10003 10002 SQL_CATALOG_NAME

The scalar-function group is the worst of these: SQL_DATETIME_FUNCTIONS returns
the system-function mask, SQL_SYSTEM_FUNCTIONS returns a conversion
mask, and the datetime mask cannot be reached by name at all. The cursor-attribute
group is shifted by one pair, so asking for static cursor attributes returns
keyset cursor attributes — an application would conclude a cursor type supports
operations it does not.

Not information types

These are in GetInfoConstants but are not SQLGetInfo ids, so they silently
alias unrelated information types:

Constant Declared What that id is as an information type
SQL_TXN_ISOLATION_LEVEL 108 SQL_MAX_CHAR_LITERAL_LEN. 108 is SQL_ATTR_TXN_ISOLATION, a connection attribute.
SQL_ROW_NUMBER 14 SQL_SEARCH_PATTERN_ESCAPE. SQL_ROW_NUMBER is a descriptor/diagnostic field.
SQL_SQL92_ENTRY_SQL 127 SQL_CREATE_ASSERTION
SQL_SQL92_INTERMEDIATE_SQL 128 SQL_CREATE_CHARACTER_SET
SQL_SQL92_FULL_SQL 129 SQL_CREATE_COLLATION

The three SQL_SQL92_*_SQL entries look like they were meant to be the values
returned by SQL_SQL_CONFORMANCE (SQL_SC_SQL92_ENTRY = 1,
SQL_SC_SQL92_INTERMEDIATE = 2, SQL_SC_SQL92_FULL = 4), not information-type
ids.

Related: SQL_SQL_CONFORMANCE decodes as a string

Separate from the ids, info type 118 (SQL_SQL_CONFORMANCE) is in none of the
string_type_constants / yn_type_constants / numeric_type_constants routing
sets in Connection.getinfo. Its SQLUINTEGER therefore falls to the heuristic
branch, which finds the bytes ASCII-printable and returns '\x01' instead of
1. Adding 118 to numeric_type_constants fixes it.

To reproduce

from mssql_python import connect
from mssql_python.constants import GetInfoConstants as G

conn = connect(CONNECTION_STRING)

# Asks for datetime functions, receives the system-function mask.
print(conn.getinfo(G.SQL_DATETIME_FUNCTIONS.value))   # 7        (id 51)
# Asks for system functions, receives a conversion mask.
print(conn.getinfo(G.SQL_SYSTEM_FUNCTIONS.value))     # 10517887 (id 58)
# The real datetime mask is only reachable by raw id.
print(conn.getinfo(52))                               # 2097151

# Asks for static cursor attributes, receives keyset cursor attributes.
print(conn.getinfo(G.SQL_STATIC_CURSOR_ATTRIBUTES1.value))  # 1048143 (id 150)
print(conn.getinfo(167))                                    # 528975  == the real answer

# Asks for the transaction isolation level, receives a string-literal length.
print(conn.getinfo(G.SQL_TXN_ISOLATION_LEVEL.value))  # 524288 (id 108)

# Returns '\x01' rather than 1.
print(repr(conn.getinfo(G.SQL_SQL_CONFORMANCE.value)))

Every call above returns successfully. Nothing raises, and nothing warns.

Confirmed directly at the ODBC layer against Microsoft ODBC Driver 18 for SQL
Server 18.6.2.1 (SQL Server 2025), so this is the driver answering the id it was
given rather than anything specific to mssql_python's decoding:

id 51     rc=0 len=4  u32=0x00000007 (7)          <- SQL_SYSTEM_FUNCTIONS
id 52     rc=0 len=4  u32=0x001FFFFF (2097151)    <- SQL_TIMEDATE_FUNCTIONS
id 58     rc=0 len=4  u32=0x00A07D7F (10517887)   <- SQL_CONVERT_DECIMAL
id 65     rc=0 len=4  u32=0x00A07D7F (10517887)   <- SQL_CONVERT_SMALLINT
id 115    rc=0 len=4  u32=0x0000007F (127)        <- SQL_OJ_CAPABILITIES
id 74     rc=0 len=2  u16=2                       <- SQL_CORRELATION_NAME
id 75     rc=0 len=2  u16=1                       <- SQL_NON_NULLABLE_COLUMNS
id 148    rc=0 len=4  u32=0x00000003 (3)          <- SQL_INDEX_KEYWORDS
id 150    rc=0 len=4  u32=0x000FFE4F (1048143)    <- SQL_KEYSET_CURSOR_ATTRIBUTES1
id 167    rc=0 len=4  u32=0x0008124F (528975)     <- SQL_STATIC_CURSOR_ATTRIBUTES1
id 108    rc=0 len=4  u32=0x00080000 (524288)     <- SQL_MAX_CHAR_LITERAL_LEN
id 14     rc=0 len=2  u16=92                      <- SQL_SEARCH_PATTERN_ESCAPE ('\')

Expected behavior

Each GetInfoConstants member carries the ODBC-defined id for its name, so
getinfo() returns the information the caller asked for. Specifically:

  • SQL_SYSTEM_FUNCTIONS = 51.
  • The datetime mask is reachable. ODBC calls it SQL_TIMEDATE_FUNCTIONS = 52;
    keeping SQL_DATETIME_FUNCTIONS as an alias of 52 would preserve the current
    spelling for callers.
  • SQL_OJ_CAPABILITIES = 115, SQL_DRIVER_HDBC = 3, SQL_DRIVER_HENV = 4.
  • SQL_KEYSET_CURSOR_ATTRIBUTES1/2 = 150/151 and
    SQL_STATIC_CURSOR_ATTRIBUTES1/2 = 167/168.
  • SQL_CATALOG_NAME = 10003 and SQL_DESCRIBE_PARAMETER = 10002.
  • Entries that are not information types are removed, or moved to a constant
    group that reflects what they are.
  • 118 is routed as numeric so SQL_SQL_CONFORMANCE returns 1, not '\x01'.

A regression test asserting every GetInfoConstants member against the ODBC
headers would keep the enum honest; the mistakes above are all transcription
slips of a kind a table-driven test catches immediately.

Further technical details

Found while implementing SQLGetInfoW in the Rust mssql-odbc driver and
comparing its results with mssql_python running on Microsoft ODBC Driver 18.
The Rust driver implements 51 and 52 with their correct ODBC meanings, which is
why the discrepancy surfaced.

  • mssql-python: mssql_python/constants.py, class GetInfoConstants; routing
    sets in Connection.getinfo (mssql_python/connection.py)
  • ODBC reference: SQLGetInfo function
  • Driver used for verification: Microsoft ODBC Driver 18 for SQL Server 18.6.2.1
    against SQL Server 2025 on Linux

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

FIXEDarea: api-compliancePython API behavior and typing: DB-API 2.0, exceptions, type stubs, new APIs.inADOtriage doneIssues that are triaged by dev team and are in investigation.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions