Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions bench/MATRIX_BENCHMARKS.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Per-database benchmarks

Generated by `bench/matrix_bench.py` on 2026-09-03. Table `(id int32, val double, txt varchar(20), dt date)`. **Ingest** = `adbc_ingest(mode="create")` of the row count in *Rows* on an autocommit connection (the driver batches the stream into one transaction itself), DDL + data + commit, row count verified; row-at-a-time (`adbc.odbc.array_binding=false`) vs *array* = the driver default, parameter arrays where the ODBC driver handles them; pyodbc = `executemany` (`fast_executemany` where the driver allows). **Fetch** = full read of the second *Rows* count, median of 3 after a warmup; `fetch_arrow_table()` vs pyodbc `fetchall()` -> `pyarrow.Table`; *native* = the same read with [native delegation](../README.md#native-delegation) handing the connection to the database's own ADBC driver. All rates are rows/s; higher is better. Servers run locally in Docker, so numbers reflect the ODBC driver + database, not the network.
Generated by `bench/matrix_bench.py` on 2026-09-04. Table `(id int32, val double, txt varchar(20), dt date)`. **Ingest** = `adbc_ingest(mode="create")` of the row count in *Rows* on an autocommit connection (the driver batches the stream into one transaction itself), DDL + data + commit, row count verified; row-at-a-time (`adbc.odbc.array_binding=false`) vs *array* = the driver default, parameter arrays where the ODBC driver handles them; pyodbc = `executemany` (`fast_executemany` where the driver allows). **Fetch** = full read of the second *Rows* count, median of 3 after a warmup; `fetch_arrow_table()` vs pyodbc `fetchall()` -> `pyarrow.Table`; *native* = the same read with [native delegation](../README.md#native-delegation) handing the connection to the database's own ADBC driver. All rates are rows/s; higher is better. Servers run locally in Docker, so numbers reflect the ODBC driver + database, not the network.

| Database | Rows | Ingest | Ingest (array) | Ingest pyodbc | Fetch | Fetch pyodbc | Fetch vs pyodbc | Fetch native |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| hana (HDB (via ODBC) 02.00.0088 00-1760424921) | 20,000 / 200,000 | 7,675 | 1,142,092 | — | 6,768,257 | — | — | — |
| exasol (EXASolution (via ODBC) 2025.01.0014) | 10,000 / 100,000 | 10,018 | 10,226 | 16,946 | 1,535,643 | 389,562 | 3.9× | — |
| firebird (Firebird (via ODBC) 06.03.1812 LI-V Firebird 5.0) | 20,000 / 200,000 | 40,468 | 41,050 | — | 297,837 | — | — | — |
| matrixone (MySQL (via ODBC) 8.0.30-MatrixOne-v4.2.0) | 10,000 / 100,000 | 97,521 | 67,034 | 4,639 | 1,919,466 | 888,285 | 2.2× | — |
| singlestore (MySQL (via ODBC) 5.7.32) | 10,000 / 100,000 | 100,745 | 201,384 | 18,767 | 1,676,765 | 785,127 | 2.1× | — |
| hana (HDB (via ODBC) 02.00.0088 00-1760424921) | 10,000 / 100,000 | 10,555 | 682,120 | 564,060 | 7,283,051 | 666,905 | 10.9× | — |
| exasol (EXASolution (via ODBC) 2025.01.0014) | 10,000 / 100,000 | 10,072 | 10,323 | 21,291 | 1,507,321 | 389,599 | 3.9× | — |
| altibase (Altibase (via ODBC) 7.3.0.1.4) | 10,000 / 100,000 | 31,485 | 959,383 | 421,608 | 2,387,341 | 770,776 | 3.1× | — |
| kinetica (Kinetica (via ODBC) 7.1.9.33.20240329114503) | — / 100,000 | read-only driver | — | — | 597,315 | — | — | — |
| ibmi (DB2/400 SQL (via ODBC) 07.05.0015) | 10,000 / 100,000 | 966 | 1,392 | 6,735 | 1,929 | — | — | — |
| ingres (INGRES (via ODBC) 10.00.0000) | 10,000 / 100,000 | 4,261 | 1,853 | 1,328 | — | — | — | — |
| sqlite (SQLite (via ODBC) 3.45.1) | 10,000 / 100,000 | 343,225 | 394,035 | 189,214 | 517,403 | 559,219 | 0.9× | 3,284,704 |
| duckdb (DuckDB (via ODBC) ) | 10,000 / 100,000 | 11,752 | 9,669 | 738 | 1,730,148 | 650,933 | 2.7× | — |
| postgres (PostgreSQL (via ODBC) 16.0.15) | 10,000 / 100,000 | 52,585 | 61,029 | 16,594 | 1,347,913 | 722,001 | 1.9× | 5,052,935 |
Expand All @@ -26,6 +31,8 @@ Failures:

* **firebird** ingest_pyodbc: Error: ('0000', '[0000] [ (0) (SQLDriverConnect)')
* **firebird** fetch_pyodbc: Error: ('0000', '[0000] [ (0) (SQLDriverConnect)')
* **ibmi** fetch_pyodbc: pyodbc timed out after 600s
* **ingres** fetch: OperationalError: UNKNOWN: [ODBC] SQLExecute failed

Caveats for this run: Oracle and Db2 were measured while a CockroachDB container on the same host
was stuck in schema-change GC at ~5 cores (pyodbc is equally slow on them here, so it is the host, not
Expand Down
2 changes: 1 addition & 1 deletion docs/UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ before it is filed.
| 2026-08-29 | QuestDB | [questdb/questdb#7566](https://github.com/questdb/questdb/issues/7566) | A `BINARY` column in a simple-query result is sent with format code 1 (raw bytes) where PostgreSQL always sends text, so libpq/psycopg and psqlodbc truncate the value at its first `0x00`; the extended protocol returns it whole. Side by side with PostgreSQL 16 via `PQfformat`. | fix PR open — [questdb/questdb#7569](https://github.com/questdb/questdb/pull/7569) |
| 2026-08-29 | QuestDB | [questdb/questdb#7567](https://github.com/questdb/questdb/issues/7567) | `DEALLOCATE ALL` fails with `Cannot invoke "io.questdb.cutlass.pgwire.PGPipelineEntry.close()" because "pe" is null` — even on a fresh autocommit connection with nothing prepared; a named `DEALLOCATE` works. | fix PR open — [questdb/questdb#7568](https://github.com/questdb/questdb/pull/7568) |
| 2026-08-29 | CrateDB | [crate/crate#20085](https://github.com/crate/crate/issues/20085) | A multi-statement simple query returns each `CommandComplete` tagged with the leading text of the whole string (`BEGIN;INSERT 0`, `BEGIN;INSERT 1`) instead of per-statement tags (`BEGIN`, `INSERT 0 1`), so tag-parsing clients such as psqlodbc get no row count inside a transaction. | **fixed** — [crate/crate#20088](https://github.com/crate/crate/pull/20088) merged 2026-08-31, ships in 6.4.4 |
| 2026-08-29 | psqlodbc | [postgresql-interfaces/psqlodbc#207](https://github.com/postgresql-interfaces/psqlodbc/issues/207) | `SQLGetData`/`SQLBindCol` narrow an out-of-range `int8` to `SQL_C_SLONG` (and to `SQL_C_SSHORT`) silently — `9223372036854775807::bigint` reads as `-1` under `SQL_SUCCESS` where ODBC specifies `22003`; `pg_atol`/`pg_atoi` in `convert.c` store the low bits with no range check. Stock PostgreSQL 16. | open |
| 2026-08-29 | psqlodbc | [postgresql-interfaces/psqlodbc#207](https://github.com/postgresql-interfaces/psqlodbc/issues/207) | `SQLGetData`/`SQLBindCol` narrow an out-of-range `int8` to `SQL_C_SLONG` (and to `SQL_C_SSHORT`) silently — `9223372036854775807::bigint` reads as `-1` under `SQL_SUCCESS` where ODBC specifies `22003`; `pg_atol`/`pg_atoi` in `convert.c` store the low bits with no range check. Stock PostgreSQL 16. | **fixed** — [postgresql-interfaces/psqlodbc#210](https://github.com/postgresql-interfaces/psqlodbc/pull/210) by Dave Cramer, merged 2026-09-04 (`SQLGetData`/`SQLBindCol` now return `SQL_ERROR` with `22003` for an out-of-range integer conversion instead of the wrapped low bits); re-test against the merge commit pending |
| 2026-08-29 | psqlodbc | [postgresql-interfaces/psqlodbc#208](https://github.com/postgresql-interfaces/psqlodbc/issues/208) | With `Protocol=7.4-0`, `SQLFreeHandle` of a prepared statement inside a transaction still sends `SAVEPOINT _per_query_svp_;DEALLOCATE …;RELEASE`; on a server without `SAVEPOINT` (QuestDB, Materialize) the error is swallowed, the server rolls the transaction back and `SQLEndTran(SQL_COMMIT)` returns `SQL_SUCCESS` for rows that are gone. Wire capture included. | **fixed** — [postgresql-interfaces/psqlodbc#209](https://github.com/postgresql-interfaces/psqlodbc/pull/209) by Dave Cramer, merged 2026-09-02 (rollback level 0 no longer savepoint-wraps the internal `DEALLOCATE`; the tested branch commit is the merged one); QuestDB confirmed, Materialize waits on its own fix |
| 2026-09-01 | Materialize | [MaterializeInc/materialize#38605](https://github.com/MaterializeInc/materialize/discussions/38605) (bug-report discussion; their tracker) | `DEALLOCATE "Name"` does not unquote the identifier, so a statement prepared at the protocol level (`Parse`) with a mixed-case name — psqlodbc's `_PLAN0x…` — cannot be released: `26000 prepared statement "\"_PLAN0x7\"" does not exist`, and inside a transaction the failure aborts it; PostgreSQL 16 releases the same statement. Filed with a psycopg side-by-side against PostgreSQL 16. The server half of the psqlodbc#208 row-loss pair. | **fix in review** — [MaterializeInc/materialize#38606](https://github.com/MaterializeInc/materialize/pull/38606) by antiguru (Materialize), opened 2026-09-01; unquotes prepared-statement, portal and cursor names before lookup |

Expand Down
Loading