Notes · 2026-09-03

The SAP HANA ODBC client decodes narrow statement text as Latin-1

SAP HANA client ODBC 2.29.25 (libodbcHDB) takes narrow SQL and SQL_C_CHAR parameters as Latin-1, not UTF-8, and returns its type table alphabetically so SECONDDATE outranks TIMESTAMP.

If you insert 'héllo 🚀' through SAP's HANA ODBC driver as a plain narrow statement and then ask the server how long the string is, you get 11:

sent    68 c3a9 6c6c6f 20 f09f9a80              "héllo 🚀"  (8 characters)
stored  68 c383c2a9 6c6c6f 20 c3b0c29fc29ac280  LENGTH(s) = 11
        i.e. the eleven Latin-1 characters those bytes spell

The driver is taking each byte of the UTF-8 the driver manager handed it as one Latin-1 character and re-encoding it. On Linux unixODBC passes a narrow char* through as the bytes it is, so every other driver in this matrix reads UTF-8 statement text correctly; this one does not, and it does not on macOS or Windows either. A bound SQL_C_CHAR parameter is decoded the same way. Measured against SAP HANA client ODBC 2.29.25 (libodbcHDB.so, SQL_DRIVER_VER 02.29.0025) and HANA Express 2.00.088.

Why it hides

The corruption is self-consistent inside narrow statements. A narrow LIKE 'héllo%' still matches a narrow INSERT, because both are mangled identically, so a narrow-only application can run for years without noticing. It shows the moment a statement literal has to match a value sent as a bound parameter: bound text travels as SQL_C_WCHAR and arrives correct, so the literal and the parameter are two different strings on the server.

Nothing in configuration reaches it. CHAR_AS_UTF8=TRUE, CHAR_SET=UTF8 and charset=UTF8 all store the same mangled bytes, and unknown connection keywords are accepted silently rather than refused; LC_ALL=C, en_US.UTF-8 and C.UTF-8 are identical to each other. The wide entry points are correct — SQLExecDirectW with the same statement stores héllo 🚀 exactly — so the workaround is to send caller statement text through SQLExecDirectW / SQLPrepareW on every platform, not only on Windows.

The type table comes back in alphabetical order

SQLGetTypeInfo returns all 33 types sorted by TYPE_NAME, with DATA_TYPE unsorted, against ODBC's ordering contract. That matters to anything that generates DDL by taking the first row for a type:

SQL_TYPE_TIMESTAMP   SECONDDATE   size=19  CREATE_PARAMS=None
SQL_TYPE_TIMESTAMP   TIMESTAMP    size=23  CREATE_PARAMS=None
SQL_TYPE_TIMESTAMP   TIMESTAMP    size=27  CREATE_PARAMS=None

SECONDDATE is HANA's whole-second timestamp (COLUMN_SIZE 19, MAXIMUM_SCALE 0), and it sorts before TIMESTAMP, so a generated timestamp column silently drops every fractional second. Nothing in the metadata separates the two: SECONDDATE carries no CREATE_PARAMS to ask a scale of, and HANA's TIMESTAMP takes no precision argument either — TIMESTAMP(6) is 42000, 257 sql syntax error: incorrect syntax near "(", because it is always seven fractional digits. Naming TIMESTAMP outright is the only route. The same ordering puts BINTEXT before NCLOB.

CLOB is what the type table offers for a long string, and HANA will not sort it

SQLGetTypeInfo(SQL_LONGVARCHAR) names CLOB, and a CLOB column cannot be ordered or de-duplicated:

ORDER BY  -> HY000  264 invalid datatype: "V" LOB type in ORDER BY clause
DISTINCT  -> HY000  264 invalid datatype: LOB type in distinct select clause

so a table created from the driver's own type names cannot be queried on its own string column. SQL_VARCHAR is VARCHAR with CREATE_PARAMS length, and since HANA 2.0 merged VARCHAR into NVARCHAR it is fully Unicode, emoji included — VARCHAR(5000) is a usable column where CLOB is not. Worth knowing if you read the table yourself: the first row for SQL_WVARCHAR is ALPHANUM, a HANA-specific 127-character type.

Two more things that cost a statement

There is no multi-row VALUES at all. INSERT INTO t VALUES (1,'a'),(2,'b') is 42000, 257 sql syntax error: incorrect syntax near ",", with literals as with parameters; the form HANA has is INSERT ... SELECT ... FROM DUMMY UNION ALL SELECT ... FROM DUMMY. Without a multi-row insert, bulk loading falls back to one execute per row — 7,675 rows/s against 1,142,092 for the same 20,000 rows through ODBC parameter arrays.

And if the server is remote, leave DATABASENAME out of the connection string. With it set, HANA answers by redirecting the client to the address it knows itself by, which for a containerised server is the container-internal one:

08S01 -10709 ... rc=10060 {172.18.0.2:39041}

Connecting straight to the tenant SQL port works from anywhere the port is reachable.

Status

Found on 2026-09-03 while adding HANA Express to the matrix, and both the Latin-1 decode and the type-table ordering were reproduced on 2026-09-04 without any of this project's code in the stack — in C, through isql, and through pyodbc. They are recorded in docs/UPSTREAM.md under "documented here, not yet reported"; no vendor issue is filed yet, so there is no fix version to point at.

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/#hana, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.