Notes · 2026-08-24

MariaDB Connector/ODBC 3.2 segfaults on a NULL DATE in a parameter array

Five findings in MariaDB Connector/ODBC with their exact errors - 3.2's array segfault and wrong row counts, its connect-time DUAL probe, its binary-literal path, and 3.1.15's missing NUL terminator.

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_CHARSQL_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.