Google Cloud Spanner into Apache Arrow with ADBC
Google Cloud Spanner (emulator + PGAdapter 0.55), read and written through adbcBridge — the ADBC driver that loads the database's ODBC driver — verified on Linux, macOS arm64 and Windows x64. Everything below is the compatibility matrix's record for this row.
Status
PASS
PASS (Spanner emulator + PGAdapter, PostgreSQL wire 14.1; 300/2,000 rows as on Linux)
PASS (Spanner emulator + PGAdapter 0.55.2, PostgreSQL wire 14.1.0; compat only — the Python bench is not run against the emulator, see the Windows benchmark file)
Driver and connection string
- ODBC driver
- psqlodbc 16 (PG wire)
- Wire
- PostgreSQL wire
- adbcBridge
- 0.1.1 (release)
The connection string the matrix used, as the connection strings reference records it (PostgreSQL wire); {drv} is the driver library or its name from odbcinst.ini, and the host, port and credentials are the test server's.
Driver={drv};Server=127.0.0.1;Port=15442;Database=test-database;Uid=adbc;
Python
import adbcbridge
# {drv}: the ODBC driver library (path), or its name from odbcinst.ini
conn = adbcbridge.connect(uri="Driver={drv};Server=127.0.0.1;Port=15442;Database=test-database;Uid=adbc;")
with conn.cursor() as cur:
cur.execute("SELECT * FROM my_table")
table = cur.fetch_arrow_table() # pyarrow.Table
Polars and pandas
import polars as pl, pandas as pd
df = pl.read_database("SELECT * FROM my_table", connection=conn) # Polars, Arrow-native
pdf = pd.read_sql("SELECT * FROM my_table", conn) # pandas 2.2+, ADBC connection
pip install adbcbridge brings the driver library; the Google Cloud Spanner ODBC driver is installed the way its vendor documents, then named in Driver=. Rust, Go, C# and Java use the same connection string through their ADBC driver managers — see the docs.
What the matrix recorded
two driver quirks, both keyed on a PGAdapter-only setting because version() just says PostgreSQL 14.1: psqlodbc inlines a parameter array's timestamps as '...'::timestamp, a type Spanner does not have, so a batch binding a timestamp goes row-at-a-time (no_timestamp_param_arrays); and every Spanner table needs a PRIMARY KEY, so generated ingest DDL adds a surrogate GENERATED BY DEFAULT AS IDENTITY column (ingest_key_column).
What this stack needed
- Server side: no 32-bit integer, no
TIMESTAMP WITHOUT TIME ZONE(sotsreads back zone-aware), no modifier onNUMERIC, no DDL inside a transaction; also ingests into and reads back anINTERLEAVE IN PARENTchild table. - A third quirk on the same key is Spanner's ceiling of 950 parameters per statement (
max_statement_params): PGAdapter prepares a multi-row INSERT that carries more without complaint and then closes the connection atSQLExecute(08S01), leaving the batching's halving search no connection to halve on -- measured exactly, 948 parameters go through and 952 drop the connection -- so it is declared rather than probed, and ingest runs at 237 four-column rows per INSERT. ingest 7.3k rows/s (7.4k with array binding), fetch 95.8k rows/s at--rows 300 --fetch-rows 2000, which is the size this entry is benchmarked at
Native ADBC driver, compared
Google Cloud Spanner speaks the PostgreSQL wire protocol, so Apache Arrow's native PostgreSQL ADBC driver (adbc-driver-postgresql 1.12.0) can be pointed at it too. On 2026-09-05 the same seven-step ADBC workload (connect, SELECT 1, DDL and inserts and a read, a 1,000-row adbc_ingest, table schema, catalog listing, read back) ran through both:
| Path | Result | First error |
|---|---|---|
| Native ADBC driver | fails at ingest | INVALID_ARGUMENT: [libpq] Failed to create table: ERROR: Primary key must be defined for table "adbc_ing_np". |
| adbcBridge over ODBC | ✓ all seven steps |
Where the native driver stops it is by design of that driver, not a defect of Google Cloud Spanner; the full table for all 28 wire-compatible databases, with each first error, is in the note.