Notes · 2026-08-25

Five Windows-only ODBC failures are one thing wearing different clothes

Five drivers that pass on Linux fail on Windows on text: the system ANSI code page sits between the caller and the driver, and the repairs differ only in which call has to leave the wide path.

What you see

Four strings, four drivers, one cause:

héllo 🚀                                                     (Apache Ignite, wide fetch)
héllo                                                       (Informix, stored value)
HY000 (50311) Error converting invalid input with source encoding UTF-8 using ICU.
  Rejected bytes began with: E96C                              (Kinetica, every read of the row)
[iconv] Character set conversion for UTF-32LE to CP1252 failed  (TDengine, NCHAR column)

A fifth does not look like mojibake at all: the Arrow Flight SQL driver's Windows build returns U+1F680 as U+F680 — the low 16 bits — through SQL_C_WCHAR, and ? through SQL_C_CHAR. All five entries pass on Linux, and pyodbc reproduces each failure, so none is a client-library bug.

What is actually happening

On Linux a driver's narrow entry points usually carry UTF-8. On Windows the narrow path is the system ANSI code page, cp1252 by default, and there are two ways onto it by accident.

The first is the driver manager, which maps every W call onto a driver that exports no W entry points, converting through the ANSI code page. Apache Ignite's ignite.odbc.dll is exactly such a driver, and its own narrow path is untouched UTF-8 in both directions. So everything sent wide is mangled on the way in — a statement literal 'héllo' matches nothing — and everything read wide comes back double-encoded, while a narrow SQLExecDirect and a SQL_C_CHAR read are byte-exact. Routing statement text and fetches narrow passes the workload and is 2.5× faster: 775,660 rows/s against 316,230.

The second is a driver doing it to itself. Kinetica's KineticaODBC.dll does export the W entry points and then transcodes wide statement text through the ANSI code page inside itself: the literal 'héllo 🚀' reaches the server as cp1252 bytes and every later read of that row fails the ICU conversion above. Its narrow statement path hands UTF-8 through byte for byte, so sending statement text narrow fixes it — but its narrow fetch is not UTF-8, so only half the Ignite treatment fits. TDengine's taos_odbc.dll is the same family: it takes the character set from GetACP() and ignores its own CHARSET_FOR_COL_BIND keywords, and an earlier build's iconv had no CP1252 → UTF-8 table at all, so statement text in the system code page could not be converted and nothing executed.

Informix shows the third way in: the caller's own portability assumption. IBM's CLI driver takes an astral parameter through SQL_C_CHAR byte-exact — wide UTF-16 gets -415 Data conversion error — but a client that resets the Linux "narrow is UTF-8" rule wholesale on Windows sends it wide and fails. It also needs DB2CODEPAGE=1208 in the environment, or the driver reads those UTF-8 bytes as cp1252 and stores them double-encoded (héllo). The Arrow Flight SQL driver has no correct text route in either width, but its SQL_C_BINARY conversion of a character column hands the server's UTF-8 through byte-exact, so reading text as binary is both correct and 1.8× faster than the wide read.

Two that look the same and are not

Firebird was a name. The driver answers SQL_DRIVER_NAME as OdbcFb on Linux and FirebirdODBC on Windows — its DLL's name — so a workaround keyed on the Linux spelling silently did not apply, and the run stopped at a guard: accepted a parameter array of 2 sets but reported neither SQL_ATTR_PARAMS_PROCESSED_PTR nor SQL_ATTR_PARAM_STATUS_PTR. Matching both names passes the whole workload.

Exasol was the registry. Windows' driver manager reads driver definitions from HKLM only and answers a DLL path in DRIVER= with IM002, where unixODBC accepts one. Nothing is wrong with EXAODBC.dll: install it machine-wide, name it by its registered name, and the entry runs verbatim. The same rule caught psqlodbc.

What the repairs are called

Three flags in this project's quirk table carry all of the above, and name what you would otherwise build yourself:

Above them sits one rule: on Windows the assumption "this driver's narrow path is UTF-8" is turned back off for every driver except Ignite, the one ANSI-only driver where it is true.

Why a UTF-8 system code page is the only general fix

Every repair above chooses which entry point the caller uses, which only helps while one of the driver's paths is clean. Where the driver reads the code page itself — GetACP(), in taos_odbc's case — and ignores every keyword offered to override it, no call reaches the value intact: it is converted before either C type sees it, and the row is already double-encoded in storage. Only a UTF-8 system code page changes that — set it before you start debugging a driver.

The Ignite, Informix, Arrow Flight SQL, TDengine and Firebird findings were recorded in the Windows campaign of 2026-08-25; Kinetica's and Exasol's cells come from the batch measured 2026-09-03/04.

Where this comes from: the adbcBridge compatibility matrix runs one workload through every database's ODBC driver on Linux, macOS and Windows and records each failure with its first error; the row for this database is at https://adbcbridge.org/matrix/#ignite, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.