Two ways a Go ODBC program dies before it prints anything
A Go program that talks to a database over ODBC has two crashes waiting for it, and neither one leaves a diagnostic behind. Both were found while running the same five-language benchmark — the same workload from Python, Rust, C#, Java and Go against one ODBC driver — across three operating systems, which is what made them separable: where Go died, the other four languages produced their numbers.
Windows: an access violation while collecting a diagnostic
Exception 0xc0000005
api.SQLGetDiagRec (zapi_windows.go:151)
odbc.NewError <- (*Rows).Next <- odbcFetch
alexbrainman/odbc crashes the process with an access violation inside api.SQLGetDiagRec,
reached from odbc.NewError ← (*Rows).Next. The path only runs when the driver raises a
diagnostic — which SQLite never does — so on the Windows grid the failure appears on every
server but SQLite, and the SQLite row looks like proof that the binding is fine. The second
Windows machine saw the same library take the process down in SQLExecute against DuckDB where
the first machine saw it in SQLGetDiagRec. The finding is on file as not yet reported.
Linux and macOS: an Arrow finalizer inside a cgo call
SIGSEGV: segmentation violation
signal arrived during cgo execution
with the crashing goroutine in arrow-go/v18 cdata.initReader.func2 -> _Cfunc_ArrowArrayStreamRelease
— the finalizer goroutine — while goroutine 1 sits somewhere else entirely. This is an
ArrowArrayStream from an earlier Arrow read being finalised long after the step that
produced it, releasing itself through cgo while another cgo call is in flight.
Where goroutine 1 was standing when it happened:
- mssql and percona — inside the
database/sqlfetch loop. - hana —
alexbrainman/odbc (*Rows).Next -> SQLFetch, the same signature. - monetdb — the closing cleanup connect.
- altibase — still building the Arrow batch for step 1, in
main.makeRecord, before any ingest or plain-ODBC step had run at all. Three separate invocations died identically, fault address0x20002fevery time; the streams being finalised came from the harness's earlier reads (the vendor probe, a row count). - oracle —
free(): invalid pointerinArrowArrayStreamReleaseon the first run; the repeat of the same command finished, which is what GC timing does to a bug like this.
Two neighbours that look the same and are not
Two more databases in the same sweep died from the plain-ODBC path without the finalizer being involved, and it is worth telling them apart:
- yugabyte — SIGSEGV under
alexbrainman/odbc's(*Stmt).Close -> (*ODBCStmt).releaseHandlewhile closing the preparedINSERT; that is psqlodbc, the same crash QuestDB produces wheredatabase/sqldies in psqlodbc'sSQLFreeHandle. - columnstore —
double free or corruption (!prev), SIGABRT, insiderows.Closeat the end of the read; that is MariaDB Connector/ODBC.
On macOS the same binding faulted on every server except SQLite, Access and Informix, and the
faulting cgo call varied by database: _Cfunc_SQLBindParameter on SQL Server,
_Cfunc_SQLGetData on SAP HANA, _Cfunc_SQLExecute on SingleStore.
What to do about it
Every one of these crashes is on the plain ODBC side of the harness — the database/sql +
alexbrainman/odbc comparison — not on the Arrow path being measured. So the runs were
re-taken with -no-native (ADBC_BENCH_NO_NATIVE=1), which switches off only that comparison
and leaves the Arrow columns as the real measurement. That matters because the harness prints
its row at the end: a crash in the comparison step loses the numbers already earned, so the
empty comparison cells in those files are absent by construction rather than measured and
failed.
The flag does not always help. On altibase the finalizer fired before the plain-ODBC step
would ever have started, so -no-native changed nothing and all four of that database's Go
cells stayed empty while the other four languages produced both of theirs. Where the crash is a
finalizer racing a cgo call rather than a code path you chose to run, the only reliable lever
is keeping the Arrow streams alive — or out of the process — until the ODBC work is done.
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/#mssql, and the finding is on file with its reproduction in docs/UPSTREAM.md of the repository.