Three things Exasol's ODBC driver gets wrong about C types and block cursors
Three defects in Exasol's own ODBC client, measured against Exasol 2025.1.14 with client
25.2.1 on Linux (libexaodbc.so) and 26.2.8 on Windows (EXAODBC.dll). Each is
reachable from plain ODBC: two of them fail the whole statement, and the third hands back
no data with no diagnostic record.
1. SQL_C_BINARY is refused for every target
Exasol has no binary column type. BLOB is recognised and refused —
[0A000] [Exasol][Exasol Driver]Feature not supported: data type BLOB
(Session: ...) (-466560) (SQLExecDirectW)
— and VARBINARY(10), BINARY(10), RAW(10) and BYTEA are not words its parser knows
(42000, "syntax error, unexpected IDENTIFIER_PART_"). That much is a server limitation
you can design around. The driver-side half is not: SQLBindParameter rejects the C type
outright, whatever the target column is.
[HY003] [Exasol][Exasol Driver]Invalid application buffer type: SQL_C_BINARY
(-30139779) (SQLBindParameter)
Measured against a VARCHAR(50) column and again against a HASHTYPE one. SQLGetData
with SQL_C_BINARY is refused too, with 07006. So a byte string cannot reach the server
by any bound route. What works is SQL_C_CHAR into a VARCHAR: the same bytes store and
read back byte for byte (b"\x01\x02" → "\x01\x02").
2. A NULL parameter described SQL_DECIMAL cannot be bound SQL_C_DEFAULT
The ordinary way to bind a NULL is to ask SQLDescribeParam what the server wants and
then bind a NULL pointer with SQL_C_DEFAULT. Exasol answers, at SQLExecute, and fails
the whole statement rather than the one parameter:
[SI002] (-47869058) [Exasol][Exasol Driver]C-Type not supported.
[HY010] (-30139812) [Exasol][Exasol Driver]Error creating prepared statement header.
A raw unixODBC probe, one NULL parameter per statement, isolates which type it is:
| column type | described as | SQL_C_DEFAULT |
SQL_C_CHAR |
|---|---|---|---|
DECIMAL(10,3) |
SQL_DECIMAL (12, 3) |
SI002 |
OK |
INT |
SQL_DECIMAL (20, 0) |
SI002 |
OK |
VARCHAR(50) |
SQL_VARCHAR |
OK | OK |
DATE |
SQL_TYPE_DATE |
OK | OK |
TIMESTAMP(6) |
SQL_TYPE_TIMESTAMP |
OK | OK |
BOOLEAN |
SQL_BIT |
OK | OK |
DOUBLE |
SQL_DOUBLE |
OK | OK |
SQL_DECIMAL alone — which is much wider than it sounds, because Exasol has no narrow
integer type. INT, INTEGER, SMALLINT and BIGINT are all aliases of DECIMAL(18,0)
/ DECIMAL(36,0), and SQLDescribeParam reports SQL_DECIMAL for every one of them, so
a NULL in any numeric column takes this path. Bind those as SQL_C_CHAR with a NULL
data pointer and they are accepted.
3. SQL_GD_BLOCK is claimed, and SQLGetData works once per SQLFetch
SQL_GETDATA_EXTENSIONS is 0xf — SQL_GD_ANY_COLUMN | ANY_ORDER | BLOCK | BOUND. On
the strength of that you would bind a wide text column into a modest buffer and repair
clipped values with SQLSetPos + SQLGetData. On a five-row rowset with a
32-byte buffer, one row carrying a 100-character value:
row 1 bound='v0' indicator=2
row 3 bound='W...................' indicator=100 <- clipped
SQLSetPos(2) rc=0 SQLGetData rc=0 value='v1'
SQLSetPos(3) rc=0 SQLGetData rc=SQL_NO_DATA value='' <- the clipped row
SQLSetPos(4) rc=0 SQLGetData rc=SQL_NO_DATA value='' <- and the cursor stays there
A plain-ODBC probe on 2026-09-04, C against unixODBC, isolated the actual rule, and
truncation is not part of it: only the first SQLGetData after each SQLFetch
succeeds, whichever row SQLSetPos selected. With no long value anywhere, rows 2–5
still answer SQL_NO_DATA — with no diagnostic record. With the clipped row visited
first it returns its full 100 characters and the short rows after it fail. With
SQL_ATTR_ROW_ARRAY_SIZE 1 and one SQLFetch per row, every row reads fine. SQLFetch
resets the driver's per-column "already returned" state and SQLSetPos does not.
The driver's own capability bits give no supported way round it either:
SQL_FORWARD_ONLY_CURSOR_ATTRIBUTES1 is 0x0 — not even SQL_CA1_NEXT, let alone
SQL_CA1_POS_POSITION — yet SQLSetPos(SQL_POSITION) returns SQL_SUCCESS rather than
refusing. Block fetching into bound columns is correct throughout: right data, right
indicators, a proper 01004 on the truncated row. The workaround is to leave a long
column unbound, at a real cost — 1.54M rows/s against the 3.32M/s the wrong bound read
managed. It is not a corner case, because SQLGetTypeInfo(SQL_LONGVARCHAR) names
LONG VARCHAR, which is VARCHAR(2000000) here, so an ordinary generated table has an
unbindable text column in it.
One server-side trap worth the same page
exadt records the container's Docker address in /exa/etc/EXAConf (PrivateNet =
172.17.0.2/16 on a default bridge) the first time the volume is initialised, and refuses
to start when the container later comes up on a different one — the private interface
with address '172.17.0.2/16' either does not exist or is down, container Exited (1).
Any reboot where another container claims that address first is enough. Fix it without
re-initialising: edit PrivateNet to the address docker inspect now shows, set
Checksum = COMMIT (the file refuses a silent edit), and restart. A fixed address on a
user-defined network avoids it.
Status
Found on 2026-09-03; all three were reproduced on 2026-09-04 in C against unixODBC, with no bridge code in the stack. They are recorded in docs/UPSTREAM.md under "documented here, not yet reported"; no vendor issue is filed yet.
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/#exasol, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.