Notes · 2026-08-25

Two ways a Go ODBC program dies before it prints anything

alexbrainman/odbc takes an access violation inside api.SQLGetDiagRec on Windows; on Linux and macOS an arrow-go finalizer releases an ArrowArrayStream mid-cgo and takes the process down.

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:

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:

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.