MariaDB Connector/ODBC 3.2 segfaults on a NULL DATE in a parameter array
MariaDB Connector/ODBC drives three entries of a compatibility matrix that runs one workload through 53 databases' ODBC drivers — MariaDB 11, MariaDB ColumnStore, and MySQL 8 as a cross-check — and on macOS it was the connector for every MySQL-wire server in the run. Five things it does are worth writing down. The first two were found on 2026-08-24 in the macOS campaign; the third and fourth in the same campaign's later batches; the fifth on Linux, where the matrix runs 3.1.15.
The segfault
libmariadb.3.dylib store_param+128 EXC_BAD_ACCESS addr 0x1d
Bind a parameter array (SQL_ATTR_PARAMSET_SIZE > 1) into a DATE column where one of the
sets is NULL, and Connector/ODBC 3.2.9 over Connector/C 3.4.9 dies inside the client
library. It is 100% reproducible with INSERT INTO t (d DATE) VALUES (?) and the rows
[(date,), (None,)], for that single column and for a full eight-column row alike. A
non-NULL date is fine, a NULL TIMESTAMP is fine, and the same driver against MySQL 8.4 is
fine — the connector has no bulk protocol there, so the array never reaches the crashing
path. Row-at-a-time binding through pyodbc is fine, and both rows read back.
Wrong row counts on the same path
Against MySQL, a four-row bulk insert through the array path reports rows_affected 2
on create and 2 again on append, while SELECT COUNT(*) says 8: every row landed, the
count is wrong. With array binding switched off the same insert reports (4, 4). It is the
parameter-array path against MySQL only.
Between them these two retire parameter arrays for Connector/ODBC ≥ 3.2 — bind one execute
per row instead. That is a real loss: on 3.1.15 a bound array goes out as a single
COM_STMT_BULK_EXECUTE, and arrays there stay ahead of a rewritten multi-row INSERT —
which in this matrix only Vertica's, Altibase's and SAP HANA's drivers also manage.
A connect-time DUAL probe that fails on servers without DUAL
[HY000] (1105) [ma-3.2.9] Unknown table "default"."default".DUAL
[42S02] (1146) [ma-3.2.9] (TableNotFound): Table not found: greptime.public.dual
SQLDriverConnect fails outright against Databend and GreptimeDB, because the connector
runs SELECT 1 FROM DUAL WHERE @@sql_mode LIKE '%ansi_quotes%' of its own accord while
connecting, and a MySQL-wire server is under no obligation to have a DUAL table.
Identical through pyodbc, with or without NO_SSPS. Both servers connect and pass the
whole workload through MySQL's own connector.
The prepared and binary-literal path on Doris and StarRocks
[HY000] (1105) [ma-3.2.9][5.7.99] NullPointerException, msg: null
[HY000] (1064) [ma-3.2.9][8.0.33] Getting syntax error at line 1, column 59 … Unexpected input ''''
Doris 2.1.0 answers the connector's server-side prepared INSERT with a bare
NullPointerException from its front end. Switching the connector to client-side
preparation with PREPONCLIENT=1 does not help: it then inlines the VARBINARY parameter
as a _binary '<raw bytes>' literal, which Doris' parser rejects as a syntax error.
StarRocks 4.1.4 fails the same way — column 59 is the inlined _binary '' — identically
with and without PREPONCLIENT=1. Both servers pass the whole workload through MySQL's own
connector on the same machine.
3.1.15 on Linux: SQLBindCol(SQL_C_CHAR) does not NUL-terminate
A value that exactly fills the bound buffer returns SQL_SUCCESS with no terminator and no
01004, and a shorter buffer gets the data bytes with no terminator either; SQLGetData on
the same value is correct. Read bound character columns by their indicator, never by
scanning for a NUL. Two more on the same version: SQLSetPos(SQL_POSITION, n) answers
SQL_SUCCESS and a following SQLGetData returns row 1's data for every n, so the
textbook repair route for a truncated value in a block cursor silently reads the wrong row;
and a fractional-second TIME string bound SQL_C_CHAR → SQL_TYPE_TIME is refused with
22008 Datetime field overflow even into a TIME(6) column.
And a read ceiling on macOS arm64
Through Connector/ODBC 3.2.9 on an Apple M4 Max, every MySQL-wire server it drives fetches at 39–47k rows/s — in five language bindings and in pyodbc alike, which puts it in the connector's own read path rather than in any one client. The same servers read at 1.3–4.5M rows/s through MySQL's connector on the same machine, and at 1.0–2.0M on Linux. Ingest through it is unaffected, 86–208k rows/s.
What it gets right
Everything else in the workload: the emoji round trip, VARBINARY, DATETIME(6)
microseconds (it describes the column COLUMN_SIZE 26 / DECIMAL_DIGITS 6, where MySQL's
connector reports 19/0), DECIMAL(10,3), NULL parameters, affected-row counts and the
5000-row batched read. It also drives MariaDB ColumnStore unchanged, with one type-name
caveat that is the engine's and not the driver's: ColumnStore's DDL parser rejects the
LONG VARCHAR and
BIT spellings the connector's own SQLGetTypeInfo hands back, so generated DDL falls
back to the standard TEXT and BOOLEAN.
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/#mariadb, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.