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
Describe the bug
Ten entries in
mssql_python.constants.GetInfoConstantscarry the wrong numericODBC information-type id, and several more are not ODBC information types at all.
Because
Connection.getinfo()passes the integer straight through toSQLGetInfo, each of these returns a different, valid-looking value with noerror. 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) andSQL_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
SQL_SYSTEM_FUNCTIONSSQL_CONVERT_DECIMALSQL_DATETIME_FUNCTIONSSQL_TIMEDATE_FUNCTIONSSQL_SYSTEM_FUNCTIONSSQL_OJ_CAPABILITIESSQL_CONVERT_SMALLINTSQL_DRIVER_HDBCSQL_CORRELATION_NAMESQL_DRIVER_HENVSQL_NON_NULLABLE_COLUMNSSQL_KEYSET_CURSOR_ATTRIBUTES1SQL_INDEX_KEYWORDSSQL_KEYSET_CURSOR_ATTRIBUTES2SQL_INFO_SCHEMA_VIEWSSQL_STATIC_CURSOR_ATTRIBUTES1SQL_KEYSET_CURSOR_ATTRIBUTES1SQL_STATIC_CURSOR_ATTRIBUTES2SQL_KEYSET_CURSOR_ATTRIBUTES2SQL_CATALOG_NAMESQL_DESCRIBE_PARAMETERSQL_DESCRIBE_PARAMETERSQL_CATALOG_NAMEThe scalar-function group is the worst of these:
SQL_DATETIME_FUNCTIONSreturnsthe system-function mask,
SQL_SYSTEM_FUNCTIONSreturns a conversionmask, 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
GetInfoConstantsbut are notSQLGetInfoids, so they silentlyalias unrelated information types:
SQL_TXN_ISOLATION_LEVELSQL_MAX_CHAR_LITERAL_LEN. 108 isSQL_ATTR_TXN_ISOLATION, a connection attribute.SQL_ROW_NUMBERSQL_SEARCH_PATTERN_ESCAPE.SQL_ROW_NUMBERis a descriptor/diagnostic field.SQL_SQL92_ENTRY_SQLSQL_CREATE_ASSERTIONSQL_SQL92_INTERMEDIATE_SQLSQL_CREATE_CHARACTER_SETSQL_SQL92_FULL_SQLSQL_CREATE_COLLATIONThe three
SQL_SQL92_*_SQLentries look like they were meant to be the valuesreturned by
SQL_SQL_CONFORMANCE(SQL_SC_SQL92_ENTRY= 1,SQL_SC_SQL92_INTERMEDIATE= 2,SQL_SC_SQL92_FULL= 4), not information-typeids.
Related:
SQL_SQL_CONFORMANCEdecodes as a stringSeparate from the ids, info type 118 (
SQL_SQL_CONFORMANCE) is in none of thestring_type_constants/yn_type_constants/numeric_type_constantsroutingsets in
Connection.getinfo. ItsSQLUINTEGERtherefore falls to the heuristicbranch, which finds the bytes ASCII-printable and returns
'\x01'instead of1. Adding 118 tonumeric_type_constantsfixes it.To reproduce
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:Expected behavior
Each
GetInfoConstantsmember carries the ODBC-defined id for its name, sogetinfo()returns the information the caller asked for. Specifically:SQL_SYSTEM_FUNCTIONS= 51.SQL_TIMEDATE_FUNCTIONS= 52;keeping
SQL_DATETIME_FUNCTIONSas an alias of 52 would preserve the currentspelling for callers.
SQL_OJ_CAPABILITIES= 115,SQL_DRIVER_HDBC= 3,SQL_DRIVER_HENV= 4.SQL_KEYSET_CURSOR_ATTRIBUTES1/2= 150/151 andSQL_STATIC_CURSOR_ATTRIBUTES1/2= 167/168.SQL_CATALOG_NAME= 10003 andSQL_DESCRIBE_PARAMETER= 10002.group that reflects what they are.
SQL_SQL_CONFORMANCEreturns1, not'\x01'.A regression test asserting every
GetInfoConstantsmember against the ODBCheaders 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
SQLGetInfoWin the Rustmssql-odbcdriver andcomparing its results with
mssql_pythonrunning 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/constants.py, classGetInfoConstants; routingsets in
Connection.getinfo(mssql_python/connection.py)against SQL Server 2025 on Linux