Repository navigation
Expand file tree
/
Copy pathcompatibility.json
More file actions
1410 lines (1410 loc) · 125 KB
/
Copy pathcompatibility.json
File metadata and controls
1410 lines (1410 loc) · 125 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"$schema": "https://adbcbridge.org/compatibility.schema.json",
"about": "Which databases adbcBridge is verified against, per operating system, with the driver quirks each entry needs. Generated from docs/COMPATIBILITY.md and the harness in tests/compat/test_matrix.py; do not edit by hand. Deliberately carries no\n generation date or commit: CI regenerates it and diffs, so the output has to be\n reproducible, and git records when it changed.",
"counts": {
"linux": 53,
"macos_arm64": 45,
"windows_x64": 48,
"databases": 53
},
"status_values": [
"pass",
"fail",
"driver-unavailable",
"server-unavailable",
"not-run",
"other"
],
"databases": [
{
"entry": "sqlite",
"name": "SQLite 3.45",
"driver": "sqliteodbc 0.99991",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "SQLite 3.51.0"
},
"windows_x64": {
"status": "pass",
"detail": "SQLite 3.43.2, SQLite3 ODBC Driver"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f REAL, s TEXT, b BLOB, d DATE, ts TIMESTAMP, n DECIMAL(10,3), bo BOOLEAN)",
"decimal_type": "string",
"text_sortable": true,
"ts_us": [
"123000"
]
},
"notes": "[native delegation](how-it-works/delegation.md) to `adbc_driver_sqlite` when installed"
},
{
"entry": "duckdb",
"name": "DuckDB (latest)",
"driver": "duckdb-odbc",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "DuckDB ODBC 1.5.5.0, universal binary"
},
"windows_x64": {
"status": "pass",
"detail": "duckdb_odbc 1.5.5.0, `DuckDB Driver` registered by the zip's odbc_install.exe"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE, s VARCHAR, b BLOB, d DATE, ts TIMESTAMP, n DECIMAL(10,3), bo BOOLEAN)",
"text_sortable": true
},
"notes": "driver quirks handled: 2048-row vectors (the driver writes a full vector into bound buffers whatever the rowset size), no `SQL_BIT` params (booleans go as integers), no usable parameter arrays on 1.5.5 (fixed on main by [duckdb-odbc#524](https://github.com/duckdb/duckdb-odbc/pull/524), in the first release after v1.5.5.0)"
},
{
"entry": "postgres",
"name": "PostgreSQL 16",
"driver": "psqlodbc 16",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "PostgreSQL 15.15"
},
"windows_x64": {
"status": "pass",
"detail": "PostgreSQL 16.15, psqlodbc 18.00.0002 `PostgreSQL Unicode(x64)`; postgres:16 container"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)"
},
"notes": "[native delegation](how-it-works/delegation.md) to `adbc_driver_postgresql` when installed; bulk ingest sends one array parameter per column (`INSERT \u2026 SELECT * FROM unnest(?::t[], \u2026)`) rather than K row-groups of cells \u2014 a server quirk keyed on `version()`, so no other PG-wire server here gets it; partitioned reads split on `ctid` (1.21x native at 1 M rows, 1.55x at 10 M), and a **declaratively partitioned parent** \u2014 which has no heap of its own and used to get one partition \u2014 now takes the key-range split, 1.36x native"
},
{
"entry": "mariadb",
"name": "MariaDB 11",
"driver": "MariaDB Connector/ODBC 3.1",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "MariaDB 11.8 arm64, Connector/ODBC 3.2.9) \u2014 after the maodbc \u2265 3.2 quirk (`34b5863`): at 688229f Connector/C 3.4.9 segfaulted on a NULL DATE in a parameter array"
},
"windows_x64": {
"status": "pass",
"detail": "MariaDB 11.8.9, mariadb:11 container; MySQL Connector/ODBC 26.7.1 with `NO_SSPS=1` from `{no_ssps}`"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "the driver's parameter arrays go as one `COM_STMT_BULK_EXECUTE` and beat the multi-row `INSERT`, so it opts into `prefer_param_arrays` (3.1.15 on Linux; from Connector/ODBC 3.2 arrays are off \u2014 see the macOS cell)"
},
{
"entry": "columnstore",
"name": "MariaDB ColumnStore 23.02",
"driver": "MariaDB Connector/ODBC 3.1 (MariaDB wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "MariaDB ColumnStore 11.1.1) \u2014 provisioning and the user created by hand; `columnstore.cnf` mounted from `/private/tmp`"
},
"windows_x64": {
"status": "pass",
"detail": "`MySQL (via ODBC) 11.1.1-MariaDB-log`; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`; provisioned per the README, with the `zz-adbc.cnf` copied into `/mnt/skysql/columnstore-container-configuration/` because the bind-mounted copy is world-writable under Docker Desktop and ignored"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b BLOB, d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN) ENGINE=Columnstore"
},
"notes": "columnar engine inside MariaDB 11.1: standard-SQL ingest DDL (ColumnStore rejects `maodbc`'s own `LONG VARCHAR`/`BIT` type names), no `VARBINARY` column type; needs `columnstore_cache_inserts=ON` (bound-parameter inserts are ~2 rows/s without it) and `provision` to start the backend processes; ingest 14.9k rows/s (54.6k with array binding), fetch 1.41M rows/s"
},
{
"entry": "oracle",
"name": "Oracle 23ai Free",
"driver": "Instant Client ODBC 23",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Oracle 23.26.0200, Instant Client 23.3 arm64) \u2014 `NLS_LANG=.AL32UTF8` must be in the environment before the process opens its first Oracle connection (on Linux setting it in-process before `SQLDriverConnect` is enough; on macOS the harness's in-process setting was too late, so export it before the process starts \u2014 and with it unset the corruption is *written*, the server stores U+FFFD"
},
"windows_x64": {
"status": "pass",
"detail": "Oracle 23.26.0200, gvenzl/oracle-free:slim; Instant Client 23 `Oracle in instantclient_23_0`, `NLS_LANG=.AL32UTF8` exported before the process starts; the 3,000-row wide-text/CLOB check included"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i NUMBER(10), f BINARY_DOUBLE, s VARCHAR2(50), b RAW(10), d DATE, ts TIMESTAMP(6), n NUMBER(10,3), bo BOOLEAN)",
"ident": "str.upper",
"unicode_env": "NLS_LANG=.AL32UTF8",
"wide_text_rows": 3000
},
"notes": "set `NLS_LANG=.AL32UTF8` for non-ASCII; no `SQL_C_SBIGINT`, so 64-bit ints are sent as numeric text. SQORA's rowset is fixed for the life of a cursor: changing `SQL_ATTR_ROW_ARRAY_SIZE` on an open cursor is accepted (`SQLSetStmtAttr` succeeds, `SQLGetStmtAttr` reads it back) and then goes wrong in one of three unannounced ways. Raising it segfaults inside `libsqora` on a later `SQLFetch` (`bcoReturnColData` through `bcoCacheFetch`, a per-rowset slot sized at execute time) -- on a `(NUMBER, CLOB)` cursor and equally on cursors with no LOB at all; whether a given raise dies depends on how deep the cursor already is, so the same raise survives after one rowset and crashes after twelve. Where it does not crash it can rewind, re-delivering rows already returned and dropping others while still ending on the right total. Lowering it never crashes and is no better: the driver keeps stepping by the original size and returns only the first N rows of each block, so 100,000 rows read at 1,024 and dropped to 128 come back as 13,440-14,336 depending on where the change falls, `SQL_NO_DATA` and no diagnostic. Reproduced with plain `SQLBindCol`/`SQLFetch`, nothing of ours on the stack. The reader therefore settles the rowset before the first fetch and never moves it (`fixed_rowset`), and a value that outgrew its bound buffer cannot be repaired once the rowset holds more than one row -- `SQLGetData` answers HY109 there, `SQLSetPos` HY109 and `SQLFetchScroll` HY106 at any size, although `SQL_GETDATA_EXTENSIONS` advertises `SQL_GD_BLOCK|SQL_GD_BOUND`; at a one-row rowset `SQLGetData` does re-read the whole value -- so a column with no real declared width -- CLOB, NCLOB, BLOB, LONG, all reported as 2,147,483,647 wide -- stays unbound and is read a row at a time with `SQLGetData`. Result sets without a LOB column keep the full block cursor, at the one size chosen before their first row. Also seen: Oracle 23.26 takes the standard multi-row `VALUES (\u2026),(\u2026)` form, prepared or direct, so the `INSERT ALL` fallback is never reached there; `SQL_C_SBIGINT` is refused on the read side too (07006), and `SQLGetTypeInfo(SQL_BIGINT)` returns nothing; an unconstrained `NUMBER` column is described `SQL_FLOAT` and read through a double (9223372036854775807 comes back 9223372036854780000, exact in `NUMBER(19)`); the empty string is NULL on both the literal and the bound path. This matters because ingest DDL spells an Arrow string as CLOB here, so ingest-then-read-back used to crash on the default path; the compat entry now writes 3,000 mixed-width strings (one in 250 is 9 KB) and reads them all back whole"
},
{
"entry": "clickhouse",
"name": "ClickHouse 26",
"driver": "clickhouse-odbc 1.5",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "ClickHouse 26.7.5.10, clickhouse-odbc 1.5.5 macOS zip, arm64"
},
"windows_x64": {
"status": "pass",
"detail": "ClickHouse 26.7.5.10; clickhouse-odbc 1.5.5 Unicode from the GitHub MSI"
}
},
"quirks": {
"big_rows": 300,
"ddl": "CREATE TABLE adbc_t (i Nullable(Int32), f Nullable(Float64), s Nullable(String), b Nullable(String), d Nullable(Date), ts Nullable(DateTime64(6)), n Nullable(Decimal(10,3)), bo Nullable(Bool)) ENGINE = Memory",
"rowcount": false
},
"notes": "a NULL parameter must be bound with a NULL value pointer on 1.5.5 \u2014 with a value buffer bound the driver ignores `SQL_NULL_DATA` and sends the parameter empty, which the server rejects for `Int32`/`Float64`/`Date`/`Decimal`/`Bool` and silently stores as `''` in `String` and as the epoch in `DateTime64`; fixed on master by our [clickhouse-odbc#586](https://github.com/ClickHouse/clickhouse-odbc/pull/586) (merged 2026-09-24), still present in 1.5.5.20260810 as re-checked on 2026-09-24, so the flag stays until a release carries it (`SQL_DESCRIBE_PARAMETER` is `N`; `SQLDescribeParam` answers `SQL_UNKNOWN_TYPE`); no affected-row counts on 1.5.5 (`SQLRowCount` answers 0 for every write; fixed on master by clickhouse-odbc#585, merged 2026-09-15, which answers \u22121 \u2014 the ODBC \"unknown\" \u2014 from the next release); `Nullable()` DDL wrapper on ingest; parameter arrays are the driver's own protocol \u2014 `SQLExecute` sends set 0 and each `SQLMoreResults` the next set (clickhouse-odbc#324) \u2014 and on 1.5.5 the first `SQLMoreResults` sends set 1 but answers `SQL_NO_DATA`, after which nothing advances: a 5-set array lands 2 rows under `SQL_SUCCESS`/`SQL_NO_DATA` (clickhouse-odbc#582), and a spec-conforming single `SQLExecute` lands 1 \u2014 fixed on master by our [clickhouse-odbc#588](https://github.com/ClickHouse/clickhouse-odbc/pull/588) (merged 2026-09-29): `SQLExecute` sends every set and reports the failing one through `SQL_ATTR_PARAMS_PROCESSED_PTR`/`SQL_ATTR_PARAM_STATUS_PTR`; the multi-row `INSERT` stays until a release carries it; one HTTP request per execute \u2014 ~16 rows/s one row at a time, ~1k rows/s through the K-row `INSERT` ingest uses, with K bounded by the server's 1,000-field HTTP form limit"
},
{
"entry": "mssql",
"name": "SQL Server 2022",
"driver": "msodbcsql 18",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "SQL Server 2022"
},
"windows_x64": {
"status": "pass",
"detail": "SQL Server 2022 16.00.4265, mcr.microsoft.com/mssql/server:2022 container; msodbcsql 18.6.2.1"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INT, f FLOAT, s NVARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME2(6), n DECIMAL(10,3), bo BIT)",
"text_sortable": true
},
"notes": "incl. `NVARCHAR(MAX)` via chunked `SQLGetData` \u2014 the driver describes it as `SQL_WVARCHAR` with column size 0 and reports `SQL_GETDATA_EXTENSIONS` = `SQL_GD_BLOCK` only, so a value truncated in a *bound* column cannot be re-read (07009 on every route) and the wide column is left unbound; ingest DDL spells an Arrow string `NVARCHAR(MAX)`, not the deprecated `TEXT` the driver's `SQLGetTypeInfo(SQL_LONGVARCHAR)` names (SQL Server will not sort, group or even compare it, `IS NULL` and `LIKE` excepted). A rowset above 1 makes every `SELECT` return `01S02 Cursor type changed`, benign"
},
{
"entry": "azuresqledge",
"name": "Azure SQL Edge 16.0",
"driver": "msodbcsql 18",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "SQL Server 15.00.2000, arm64 image, msodbcsql 18.6.2.1 arm64"
},
"windows_x64": {
"status": "pass",
"detail": "Azure SQL Edge, SQL Server 16.00.5100; msodbcsql 18.6.2.1"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INT, f FLOAT, s NVARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME2(6), n DECIMAL(10,3), bo BIT)"
},
"notes": "the SQL Server 2022 engine, so it takes the same path as SQL Server 2022, `TEXT` ingest-DDL quirk included \u2014 it even reports `SQL_DBMS_NAME` \"Microsoft SQL Server\""
},
{
"entry": "mysql",
"name": "MySQL 8.4",
"driver": "MySQL Connector/ODBC 9.4 (and MariaDB Connector/ODBC 3.1)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "MySQL 8.4.11 arm64, MariaDB Connector/ODBC 3.2.9) \u2014 after the maodbc \u2265 3.2 quirk (`34b5863`): at 688229f the connector's array path misreported the row count"
},
"windows_x64": {
"status": "pass",
"detail": "MySQL 8.4.11, mysql:8 container; MySQL Connector/ODBC 26.7.1"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "the driver executes parameter arrays row by row"
},
{
"entry": "tidb",
"name": "TiDB 7.5",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "TiDB, MySQL wire 8.0.11"
},
"windows_x64": {
"status": "pass",
"detail": "TiDB v7.5.1, MySQL wire 8.0.11; MySQL Connector/ODBC 26.7.1 with `NO_SSPS=1` from `{no_ssps}` \u2014 the 8.4.0-era note that no newer connector is published for Windows is obsolete: dev.mysql.com offers 26.7.1 winx64"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "no quirks; run from the tarball the driver needs `PLUGIN_DIR=` for the `mysql_native_password` client plugin TiDB's root account uses"
},
{
"entry": "dolt",
"name": "Dolt 2.3.1 (MySQL 8.0.33 wire)",
"driver": "MySQL Connector/ODBC 9.4",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Dolt, MySQL wire 8.0.33"
},
"windows_x64": {
"status": "pass",
"detail": "Dolt, MySQL wire 8.0.33; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "no driver quirks; Dolt offers only `mysql_native_password`, which Connector/ODBC 9.x loads as a plugin, so the entry points `PLUGIN_DIR` at the tarball's own `lib/plugin`"
},
{
"entry": "databend",
"name": "Databend 1.2",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "with MySQL Connector/ODBC 26.7.1 (Oracle's macOS arm64 binary, built for iODBC) through a bridge built against iODBC 3.52.16 \u2014 `MySQL (via ODBC) 8.0.90-v1.2.881`; **FAIL through MariaDB Connector/ODBC 3.2.9** at its connect-time probe `SELECT 1 FROM DUAL WHERE @@sql_mode LIKE '%ansi_quotes%'` (`Unknown table \"default\".\"default\".DUAL`), identical through pyodbc"
},
"windows_x64": {
"status": "pass",
"detail": "`MySQL (via ODBC) 8.0.90-v1.2.881`; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`, `ADBC_BENCH_AUTOCOMMIT=1` for the harness rows) \u2014 **with MySQL Connector/ODBC 26.7.1 the astral check passes**; through 8.4.0 (the earlier Windows measurement) the same entry FAILED at that check only: `h\u00e9llo \ud83d\ude80` stored byte-exact, read back as `h\u00e9llo ???` on every read path including pyodbc, because 8.4.0 substitutes `?` for a non-BMP character before either C type sees it (SQL_C_CHAR and SQL_C_WCHAR both return `3f 3f 3f`; `CHARSET=` makes no difference"
}
},
"quirks": {
"big_rows": 2000,
"bool_type": "int16",
"column_order": false,
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARCHAR(50), d DATE, ts TIMESTAMP, n DECIMAL(10,3), bo BOOLEAN)",
"decimal_type": "string"
},
"notes": "the server has no prepared statements, so the connector runs with `NO_SSPS=1`; driver quirks handled: `_binary` literals for date/timestamp/binary params, MySQL type names in ingest DDL"
},
{
"entry": "percona",
"name": "Percona Server 8.4",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Percona Server, MySQL wire 8.4.11"
},
"windows_x64": {
"status": "pass",
"detail": "Percona Server 8.4.11-11; MySQL Connector/ODBC 26.7.1, default connection string"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "drop-in MySQL fork: the `mysql` entry applies unchanged, no quirks; ingest 21.1k rows/s, fetch 1.18M rows/s"
},
{
"entry": "matrixone",
"name": "MatrixOne 4.2 (MySQL 8.0.30 wire)",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "MatrixOne, MySQL wire 8.0.30"
},
"windows_x64": {
"status": "pass",
"detail": "MatrixOne v4.2.0, MySQL wire 8.0.30; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`) \u2014 **with MySQL Connector/ODBC 26.7.1 the astral check passes**; through 8.4.0 (the earlier Windows measurement) the same entry FAILED at that check only: `h\u00e9llo \ud83d\ude80` stored byte-exact, read back as `h\u00e9llo ???` on every read path including pyodbc, because 8.4.0 substitutes `?` for a non-BMP character before either C type sees it (SQL_C_CHAR and SQL_C_WCHAR both return `3f 3f 3f`; `CHARSET=` makes no difference"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT PRIMARY KEY, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN)",
"ingest_types": "{pa.bool_(): pa.int8()}"
},
"notes": "`mysql_native_password` only, so run from the tarball the connector needs `PLUGIN_DIR=`; server side: a table without a PRIMARY KEY gets a hidden `__mo_fake_pk_col` that `SQLColumns` reports in `GetObjects`; a parameter array bound into a `BIT` column aborts the server once it holds NULLs (`malloc(): unaligned fastbin chunk detected`; single parameters and NULL-free arrays are fine), so ingest sends booleans as `TINYINT` \u2014 fixed on MatrixOne `main` by [matrixorigin/matrixone#27645](https://github.com/matrixorigin/matrixone/pull/27645) (2026-08-26): on the 2026-08-28 nightly a bound NULL stores as NULL and the same 999-set array runs clean, so the mapping is for the released 4.2.0; driver quirk handled: MatrixOne describes a TEXT column as `SQL_WLONGVARCHAR` one third of its widest value's byte length wide (5 characters for the benchmark's 16-byte strings, 0 for an empty result set), so binding at that width truncates every row; a no-declared-length column is bound at `long_bind_bytes` instead of re-reading every row (2.05M rows/s in the fix's own measurement). `SHOW VARIABLES` and `SHOW COLLATION` kill the client with `SIGFPE` inside Connector/ODBC 9.4's `get_column_size` (a zero charset width in the result metadata); the bridge issues neither; ingest 97.5k rows/s (86.5k with array binding), fetch 1.97M rows/s"
},
{
"entry": "doris",
"name": "Apache Doris 2.1.0 (MySQL 5.7.99 wire)",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "with MySQL Connector/ODBC 26.7.1 (Oracle's macOS arm64 binary, built for iODBC) through a bridge built against iODBC 3.52.16 \u2014 `MySQL (via ODBC) 5.7.99`, 300/2,000 rows as on Linux; **FAIL through MariaDB Connector/ODBC 3.2.9**: the server NPEs on its server-side prepared INSERT and rejects the `_binary '<raw bytes>'` literal its `PREPONCLIENT=1` path inlines"
},
"windows_x64": {
"status": "pass",
"detail": "Apache Doris, MySQL wire 5.7.99, FE + BE in one container, ~6 min to `Alive`; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`) \u2014 **with MySQL Connector/ODBC 26.7.1 the astral check passes**; through 8.4.0 (the earlier Windows measurement) the same entry FAILED at that check only: `h\u00e9llo \ud83d\ude80` stored byte-exact, read back as `h\u00e9llo ???` on every read path including pyodbc, because 8.4.0 substitutes `?` for a non-BMP character before either C type sees it (SQL_C_CHAR and SQL_C_WCHAR both return `3f 3f 3f`; `CHARSET=` makes no difference) \u2014 restarting a stopped Doris container fails (`boot failed!` there; on Linux the FE loops `wait catalog to be ready` instead, because it recorded its old container IP), recreate it"
}
},
"quirks": {
"big_rows": 2000,
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARCHAR(50), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN) DISTRIBUTED BY RANDOM BUCKETS AUTO",
"ingest_types": "{pa.float64(): pa.decimal128(12, 3)}",
"quote": "`"
},
"notes": "MPP analytic warehouse; reports no transaction support (`SQL_TC_NONE`), so the Databend quirk (`_binary` parameter literals rewritten as text, portable ingest type names) applies unchanged and `NO_SSPS=1` is required (the FE prepares only a point `SELECT` on a `store_row_column` unique-key table -- any other `SELECT` is refused with `Only support prepare SelectStmt point query now` -- or an `INSERT`, and a server-side prepared `INSERT` executes only for parameters bound from `SQL_C_CHAR` or to a matching non-character SQL type: a parameter bound to a character SQL type from any other C type -- `SQL_C_WCHAR` included, which is how this connector binds a string -- gets a bare `NullPointerException` from the FE); every OLAP table has to declare how its rows are distributed, so generated ingest DDL appends `DISTRIBUTED BY RANDOM BUCKETS AUTO` plus `enable_duplicate_without_keys_by_default` (`ddl_table_options`) \u2014 a duplicate table with no key columns, without which Doris refuses any table whose first column is `TEXT`/`STRING` -- what a generated string column is here -- `FLOAT` or `DOUBLE` (`The olap table first column could not be float, double, string ...`; a leading `VARCHAR(n)`, `CHAR(n)` or `DECIMAL(p,s)` is accepted) \u2014 keyed on `@@version_comment` since `version()` is a bare MySQL number; no binary column type and no `DOUBLE PRECISION` spelling; `ANSI_QUOTES` is accepted but ignored, so identifiers are backtick-quoted; ingest 2.2k rows/s (2.3k with array binding) -- like StarRocks an `INSERT` is a load transaction whatever it carries, so multi-row batching is worth ~300x (7 rows/s without it); fetch 1.36M rows/s \u2014 pyodbc's ingest column is empty: its row-at-a-time binding sends `date`, `datetime` and `bytes` parameters as `_binary'...'` literals, which Doris' parser rejects (the same values passed as `str` insert fine), and its `fast_executemany` stages first set `autocommit=False`, which Doris refuses (`Transactions are not enabled`)"
},
{
"entry": "oceanbase",
"name": "OceanBase CE 4.4.2 (MySQL 5.7.25 wire)",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "OceanBase CE, MySQL wire 5.7.25) \u2014 `MODE=SLIM`, boots in 40 s, 4.3 GiB peak"
},
"windows_x64": {
"status": "pass",
"detail": "OceanBase CE, MySQL wire 5.7.25, `MODE=SLIM`, boots in ~60 s under a 6 GB cap; MySQL Connector/ODBC 26.7.1, user `root@test`) \u2014 **needs `NO_SSPS=1` on Windows**: the entry's connection string carries no `{no_ssps}`, and without it every bound parameter fails `No data supplied for parameters in prepared statement` (pyodbc identical); run with an `OCEANBASE_CONN` override that adds it \u2014 a one-token patch to the entry"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "no driver quirks and no tolerance flag the `mysql` entry does not already have (`TINYINT(1)` booleans, `ANSI_QUOTES`) \u2014 a distributed HTAP engine whose MySQL mode takes that entry's types unchanged; multi-tenant, so the login name carries the tenant (`User=root@test`), and `mysql_native_password` only, so run from the tarball the connector needs `PLUGIN_DIR=`; the container must boot with `MODE=SLIM`, which starts a prebuilt cluster \u2014 the default `MODE=MINI` builds its tenant from scratch and times out loading the timezone tables (117,043 rows in `mysql.time_zone_transition`), and is also the only path on which `obd`'s open-files precheck (`OBD-1007`, `nofile` below 20000) fires; the SLIM path boots under Docker's default 1024, and the compose file raises `nofile` to 20000 anyway because at 1024 the observer quietly stops accepting connections once ~780 are open; ingest 105k rows/s at 20,000 rows (12.2k with `adbc.odbc.rows_per_insert=1`; `adbc.odbc.array_binding` changes nothing here -- Connector/ODBC walks a parameter array row by row, so the multi-row `INSERT` stays ahead and ingest takes it either way); fetch 1.32M rows/s. One thing to know when writing NULLs here: on a server-side prepared INSERT the connector sends a NULL as `MYSQL_TYPE_NULL` until a non-NULL execute has fixed that parameter's type, and OceanBase refuses that execute with `Object type error` (4001) -- the NULL-carrying execute itself, not the ones after it; once a value has gone through on that statement, later NULLs are accepted, and a fresh statement starts over. `NO_SSPS=1` avoids it entirely; MySQL itself is happy with the same sequence. Bulk ingest is where it shows: a multi-row `INSERT` whose first execute carries a NULL is refused, the ingest falls back to a parameter array, and Connector/ODBC answers `SQL_PARAM_ERROR` for the sets the server rejected -- adbcBridge re-runs those rows one at a time after the batch, by which point the types are fixed, so every row lands (0.1.0 dropped them silently and under-reported the count; a column that is NULL in every row raises the server's error)"
},
{
"entry": "greptimedb",
"name": "GreptimeDB 1.1.4 (MySQL 8.4.2 wire)",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "with MySQL Connector/ODBC 26.7.1 (Oracle's macOS arm64 binary, built for iODBC) through a bridge built against iODBC 3.52.16 \u2014 `MySQL (via ODBC) 8.4.2`; **FAIL through MariaDB Connector/ODBC 3.2.9** at the same `DUAL` probe (`Table not found: greptime.public.dual`), identical through pyodbc"
},
"windows_x64": {
"status": "pass",
"detail": "GreptimeDB 1.1.4, MySQL wire 8.4.2; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`) \u2014 **with MySQL Connector/ODBC 26.7.1 the astral check passes**; through 8.4.0 (the earlier Windows measurement) the same entry FAILED at that check only: `h\u00e9llo \ud83d\ude80` stored byte-exact, read back as `h\u00e9llo ???` on every read path including pyodbc, because 8.4.0 substitutes `?` for a non-BMP character before either C type sees it (SQL_C_CHAR and SQL_C_WCHAR both return `3f 3f 3f`; `CHARSET=` makes no difference"
}
},
"quirks": {
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts TIMESTAMP(6) TIME INDEX, n DECIMAL(10,3), bo BOOLEAN) WITH ('append_mode'='true')",
"ingest_types": "{pa.float64(): pa.decimal128(12, 3)}",
"not_null": [
"ts"
],
"quote": "`",
"row2_fill": [
"ts"
]
},
"notes": "time-series store, driven over its **MySQL** wire (4002); its PostgreSQL wire (4003) cannot be reached at all \u2014 psqlodbc's connect handshake asks for `show transaction_isolation`, which GreptimeDB does not implement (only `SHOW TRANSACTION ISOLATION LEVEL`), so `SQLDriverConnect` fails as it does for H2. Every table needs a `TIME INDEX` column, so generated ingest DDL appends one that defaults to the insert time (`ddl_extra_column`: `greptime_timestamp TIMESTAMP(3) TIME INDEX DEFAULT CURRENT_TIMESTAMP`) plus `append_mode` (`ddl_table_options`) \u2014 outside append mode rows sharing a timestamp are merged; prepared-statement metadata types every parameter as a string and then rejects it, so `NO_SSPS=1` plus the Databend `_binary` quirk; no `DOUBLE PRECISION` type name and no `ANSI_QUOTES` (backtick-quoted identifiers); ingest 181k rows/s (156k with array binding), fetch 989k rows/s"
},
{
"entry": "starrocks",
"name": "StarRocks 4.1.4 (MySQL 8.0.33 wire)",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "with MySQL Connector/ODBC 26.7.1 (Oracle's macOS arm64 binary, built for iODBC) through a bridge built against iODBC 3.52.16 \u2014 `MySQL (via ODBC) 8.0.33`, 300/2,000 rows; **FAIL through MariaDB Connector/ODBC 3.2.9**: syntax error at the connector's inlined `_binary ''` literal, with and without `PREPONCLIENT=1`"
},
"windows_x64": {
"status": "pass",
"detail": "StarRocks, MySQL wire 8.0.33, allin1-ubuntu; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`) \u2014 **with MySQL Connector/ODBC 26.7.1 the astral check passes**; through 8.4.0 (the earlier Windows measurement) the same entry FAILED at that check only: `h\u00e9llo \ud83d\ude80` stored byte-exact, read back as `h\u00e9llo ???` on every read path including pyodbc, because 8.4.0 substitutes `?` for a non-BMP character before either C type sees it (SQL_C_CHAR and SQL_C_WCHAR both return `3f 3f 3f`; `CHARSET=` makes no difference"
}
},
"quirks": {
"big_rows": 2000,
"bool_type": "int8",
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME, n DECIMAL(10,3), bo BOOLEAN)",
"decimal_type": "decimal128(12, 3)",
"quote": "`"
},
"notes": "MPP columnar warehouse: it prepares nothing but `SELECT`, so the connector runs with `NO_SSPS=1`, and the `_binary` date/timestamp/binary literals it then emits are sent as ordinary quoted text instead (`temporal_binary_param_as_varchar`, restored -- it had been dead code since a bad merge); MySQL type names rejected in ingest DDL, so it uses standard SQL names, and the portable fallback for a double is now `DOUBLE`, not the ISO `DOUBLE PRECISION`, which StarRocks does not parse; no `ANSI_QUOTES` mode at all, so identifiers are quoted with backticks (the driver already asks for `SQL_IDENTIFIER_QUOTE_CHAR`); `DECIMAL(10,3)` described at MySQL's display width (12,3); ingest 4.8k rows/s -- every `INSERT` is a load transaction costing a flat ~100 ms of server-side work (10 rows/s for any client sending one row per statement), so the multi-row batching is worth 480x here (pyodbc, which has no such batching, cannot ingest here at all); fetch 1.80M rows/s"
},
{
"entry": "mongodbbi",
"name": "MongoDB 7 + BI Connector 2.14 (MySQL 5.7.12 wire)",
"driver": "MySQL Connector/ODBC 9.4 (MySQL wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "BI Connector, MySQL wire 5.7.12; read-only) \u2014 no macOS build of mongosqld 2.14.x exists, the linux-arm64 build runs inside the `mongo:7` arm64 container"
},
"windows_x64": {
"status": "pass",
"detail": "BI Connector mongosqld v2.14.22 over mongo:7, MySQL wire 5.7.12; read-only; MySQL Connector/ODBC 26.7.1 + `NO_SSPS=1`; libssl 1.1 staged per the README) \u2014 **with MySQL Connector/ODBC 26.7.1 the astral check passes**; through 8.4.0 (the earlier Windows measurement) the same entry FAILED at that check only: `h\u00e9llo \ud83d\ude80` stored byte-exact, read back as `h\u00e9llo ???` on every read path including pyodbc, because 8.4.0 substitutes `?` for a non-BMP character before either C type sees it (SQL_C_CHAR and SQL_C_WCHAR both return `3f 3f 3f`; `CHARSET=` makes no difference"
}
},
"quirks": {
"big_rows": 100000,
"bool_type": "int8",
"catalog_cols": [
"b",
"bo",
"d",
"f",
"i",
"n",
"s",
"ts"
],
"ddl": "CREATE TABLE adbc_t (_id VARCHAR(24), i BIGINT, f DOUBLE, s VARCHAR(65535), b VARCHAR(65535), d DATETIME, ts DATETIME, n DECIMAL(65,20), bo BOOLEAN)",
"decimal_type": "string",
"pseudo_columns": [
"_id"
],
"quote": "`",
"read_only": true
},
"notes": "`mongosqld` presents MongoDB collections as SQL tables over the MySQL wire; a query engine on a DRDL schema: `CREATE TABLE`, `INSERT` and `DROP TABLE` answer 1105 `\u2026 requires --writeMode`, `UPDATE`/`DELETE`/`TRUNCATE` are not in the grammar at all, and `--writeMode` is refused together with a `--schema` path \u2014 so the two collections are loaded with mongosh and the entry runs the read side unchanged. Driver quirk handled: `SQLColumns` segfaults inside Connector/ODBC on any table with a `DECIMAL` column -- mongosqld's `information_schema` reports NULL `NUMERIC_PRECISION` and the connector runs `strtol()` on it -- so `GetObjects` describes a zero-row SELECT instead (the existing `no_sql_columns` path the Flight SQL driver takes, keyed on `SQL_DBMS_VER`); server side: the handshake also segfaults without `PLUGIN_DIR=` (`mysql_native_password`), and `COM_STMT_PREPARE` is refused (`NO_SSPS=1`). No binary type; `d` is mapped `timestamp` by the entry's DRDL schema so it arrives as a midnight datetime (mongosqld does have a DATE SQL type: a `SqlType: date` column is described `SQL_TYPE_DATE`), decimals described as `DECIMAL(65,20)` so they read back as exact text, `_id` in every `SELECT *`; fetch 128k rows/s"
},
{
"entry": "db2",
"name": "IBM Db2 12.1",
"driver": "Db2 clidriver (`libdb2.so`)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Db2 12.01.0500, IBM macarm64 clidriver; server amd64 emulated"
},
"windows_x64": {
"status": "pass",
"detail": "Db2 12.01.0500 in the ibmcom/db2 container; IBM clidriver 12.1.4 from the free \"ODBC and CLI\" zip, registered by hand as the short alias `IBM DB2 ODBC DRIVER` \u2192 `clidriver\\bin\\db2cli64.dll` \u2014 the 78-character name `db2cli install -setup` writes gets `IM002` from the Windows driver manager, and the `db2clio.dll` it points at is not in that package). **`Authentication=SERVER` must be in the connection string on Windows**: the default `SERVER_ENCRYPT` negotiation fails `SQL1042C` (sqlerrp `SQLEXSLC`, rc 205, the client-side security-plugin step) from every process but `db2cli.exe`, which carries its own gsk8/ICC manifests \u2014 python and PowerShell P/Invoke fail identically, whatever `DB2CODEPAGE`, cwd or environment. The whole workload passes with that one keyword; fetch 398,715 rows/s, array ingest 81,651"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE, s VARCHAR(50), b VARBINARY(10), d DATE, ts TIMESTAMP(6), n DECIMAL(10,3), bo BOOLEAN)",
"ident": "str.upper",
"text_sortable": true
},
"notes": "32-bit `SQLLEN` (see `adbc.odbc.sqllen_32bit`); ingest DDL spells an Arrow string as the widest `VARCHAR`, not the `LONG VARCHAR` the driver's `SQLGetTypeInfo(SQL_LONGVARCHAR)` names (deprecated; will not sort, group or de-duplicate -- `SQL0134N` on `ORDER BY`, `GROUP BY`, `DISTINCT` and `UNION` -- and has no bulk-insert path: ~7k rows/s whatever the batch size against ~430k for `VARCHAR(32672)` in a 20,000-row test, 30-56x on a warm database and over 200x through `adbc_ingest` on a cold one; an earlier run recorded ~700x)"
},
{
"entry": "informix",
"name": "IBM Informix 15.0.1 (developer edition)",
"driver": "Db2 clidriver (`libdb2.so`, DRDA)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "IDS 12.10, same arm64 clidriver; server amd64 emulated"
},
"windows_x64": {
"status": "pass",
"detail": "IDS 12.10 developer edition over the DRDA listener, same clidriver 12.1.4 alias and `Authentication=SERVER` as db2; `adbc` database created with `dbaccess` per the README) \u2014 **after two Windows-only fixes**: the IDS quirk relied on `wchar_as_utf8`, which the Windows block resets, so the astral parameter still went SQL_C_WCHAR and the INSERT failed `-415 Data conversion error`; `narrow_params` (the Ignite mechanism) keeps it on SQL_C_CHAR, and **`DB2CODEPAGE=1208` in the environment** tells the CLI driver those bytes are UTF-8 (without it the driver reads them as cp1252 and the row stores double-encoded, `h\u00c3\u0192\u00c2\u00a9llo`). Verified first with pyodbc: SQL_C_CHAR + UTF-8 stores and reads `h\u00e9llo \ud83d\ude80` byte-exact, wide UTF-16 gets -415. Fetch 591,439 rows/s, array ingest 91,977"
}
},
"quirks": {
"bool_type": "int16",
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTE, d DATE, ts DATETIME YEAR TO FRACTION(5), n DECIMAL(10,3), bo BOOLEAN)",
"ts_us": [
"123450"
]
},
"notes": "same `libdb2.so` as Db2, so keyed on `SQL_DBMS_NAME` \"IDS\", not the driver name: no usable `SQL_C_WCHAR` params (UTF-8 narrow path instead), `SQL_C_BIT` params break the DRDA stream so booleans go as integers; 32-bit `SQLLEN` as for Db2; `BYTE` described as IBM's own `SQL_BLOB` type code (-98); server side: `GL_USEGLU=1` for 4-byte UTF-8, `DELIMIDENT=y` for the quoted identifiers ingest emits, `DATETIME YEAR TO FRACTION(5)` timestamps; ingest 15.6k rows/s (130k array), fetch 751k rows/s"
},
{
"entry": "monetdb",
"name": "MonetDB 11.55 (Dec2025-SP3)",
"driver": "MonetDBODBClib 11.55",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "MonetDB 11.55.0007, libMonetODBC built from the 11.55 source; Homebrew's bottle has no ODBC driver"
},
"windows_x64": {
"status": "pass",
"detail": "MonetDB 11.55.0007; MonetDB ODBC Installer x86_64-20260615, `MonetDB ODBC Driver`"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE, s VARCHAR(50), b BLOB, d DATE, ts TIMESTAMP(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "driver quirk handled: no usable parameter arrays (executes only the first set); `SQLEndTran` unreliable under pyodbc"
},
{
"entry": "vertica",
"name": "Vertica 25.3 (OpenText Analytics Database)",
"driver": "Vertica ODBC 25.1 (`libverticaodbc.so`, native wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "driver-unavailable",
"detail": "vertica.com's macOS download is vsql only, no ODBC"
},
"windows_x64": {
"status": "pass",
"detail": "`Vertica Database (via ODBC) 25.03.0000`, opentext/vertica-k8s:25.3.0-8-minimal bootstrapped with `fixtures/setup_vertica.sh`; Vertica client 25.1.0 MSI, driver name `Vertica` \u2014 the 25.3 client URL 404s and 25.1 drives the 25.3 server). Windows note: run the `vcluster create_db` step from PowerShell, not Git Bash (path conversion mangles `/opt/vertica/bin/vcluster`), and `--password \"\"` must be passed as an empty argument \u2014 PowerShell 5.1 drops it, leaving dbadmin with a non-empty password to reset"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b VARBINARY(10), d DATE, ts TIMESTAMP(6), n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "first-party driver on 5433 \u2014 the port speaks the PostgreSQL v3 wire (libpq connects, `server_version 14.0`) but the PostgreSQL catalogs are absent, so psqlodbc fails at its connect-time `pg_type` lookup (`Relation \"pg_type\" does not exist`) and cannot drive it. Needs **no tolerance flags at all** (as Cloudberry does not): every workload type is a native Vertica type and the workload round-trips exactly, emoji and microseconds included (`i` is int64 \u2014 Vertica's integer types are all 64-bit aliases). `f` is exact because 1.5 is: the driver passes `DOUBLE PRECISION` through a 15-significant-digit decimal form in both directions (`%1.15e` in `libverticaodbc.so`, `SQLDescribeCol` size 15), so a bound `3.141592653589793` is stored as `3.14159265358979`. The entry points `VERTICAINI` at a `vertica.ini` of its own for the driver's message catalogue (`ErrorMessagesPath`; without it every driver diagnostic degrades to `[Vertica][DSI] ... Could not open error message files`, though the workload still passes) and pins `DriverManagerEncoding = UTF-16` \u2014 client 25.1 detects unixODBC's 2-byte `SQLWCHAR` by itself, but `UTF-32` there corrupts every wide string in both directions with no error (six U+FFFD) and can abort the process. One quirk: its parameter arrays are a native bulk load (`COPY ... FROM LOCAL STDIN NATIVE`) and beat the one-row-per-execute path 7-8\u00d7 (17-20k rows/s \u2192 135-139k), so `prefer_param_arrays` \u2014 the flag `maodbc` already uses. Multi-row `VALUES` is refused on this fixture (`Function public.explode(\"array\") does not exist`: `vcluster create_db --skip-package-install` leaves the `ComplexTypes` package out); with the package installed it parses and runs at ~2.5k rows/s, a column store taking each multi-row `VALUES` as one row-store insert, so the array path stays ahead either way. Server side, `vertica/vertica-ce` no longer exists (the `vertica` Docker Hub namespace is empty and the `opentext` one publishes no CE image), so the entry runs `opentext/vertica-k8s` and builds the database with `vcluster` itself (`fixtures/setup_vertica.sh`); pinned to 25.3 because Vertica 26.1 dropped Community Edition and a 26.x server refuses the licence its own image ships; needs `nofile` 65536; ingest 151k rows/s (array binding), fetch 948k rows/s"
},
{
"entry": "cockroachdb",
"name": "CockroachDB 26",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "CockroachDB, PostgreSQL wire 18.0.0, arm64"
},
"windows_x64": {
"status": "pass",
"detail": "CockroachDB, PostgreSQL wire 18.0.0; psqlodbc 18.00.0002"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER PRIMARY KEY, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n DECIMAL(10,3), bo BOOLEAN)"
},
"notes": "no quirks; declare a PRIMARY KEY or the synthesised hidden `rowid` shows up in `GetObjects`; `version()` is CockroachDB's own, so the PostgreSQL array-ingest form stays off and ingest uses the multi-row `INSERT` (verified); **no `ctid`** (`42703`), so partitioned reads use the key-range split \u2014 measured 5.1x at N=8 and 6.5x at N=16 over a single connection on 1 M rows. **`adbc_driver_postgresql` cannot read this server at all** (it needs binary `COPY TO`, which CockroachDB does not implement), so there is no native ADBC alternative to compare against"
},
{
"entry": "yugabyte",
"name": "YugabyteDB 2026.1 (YSQL)",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "YugabyteDB, PostgreSQL wire 15.12, arm64"
},
"windows_x64": {
"status": "pass",
"detail": "YugabyteDB 2026.1.1.1, PostgreSQL wire 15.12; psqlodbc 18.00.0002"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)"
},
"notes": "no quirks; YSQL is PostgreSQL 15, and its internal row id is a system column so `GetObjects` is unaffected; `version()` carries `-YB-`, so the PostgreSQL array-ingest form stays off (verified); **no `ctid`** (`0A000`), and its default `PRIMARY KEY (id)` is hash-partitioned, so partitioned reads split on `yb_hash_code()` \u2014 2.6x over a single connection, and 2.2x better than a key range, which YugabyteDB would run as a `Seq Scan` with a `Storage Filter`. Against the native driver it reaches 1.18\u20131.22x at 4 M rows, i.e. on the 1.2x bar rather than over it, and 0.94x at 1 M: the server is the bottleneck there, not psqlodbc"
},
{
"entry": "timescaledb",
"name": "TimescaleDB 2.29 (PostgreSQL 16)",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "TimescaleDB, PostgreSQL wire 16.15"
},
"windows_x64": {
"status": "pass",
"detail": "TimescaleDB latest-pg16, PostgreSQL wire 16.15.0; hypertable steps included"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)"
},
"notes": "no quirks; an extension on stock PostgreSQL, so the array-ingest form applies and is used (verified); also ingests into and reads back a `create_hypertable()` hypertable; a plain table with rows in it has a heap and takes the `ctid` split, while a hypertable is an inheritance parent (relkind `r`, chunks through `pg_inherits`, not a declarative `p` parent) whose own heap is empty \u2014 `pg_relation_size` 0, so the block-count probe declines the heap split \u2014 and takes the key-range split only when its primary key leads with an integer NOT NULL column (not timed; TimescaleDB requires the partitioning column in any unique index, so a `(d, a)` key leads with the DATE and gets no split). `SELECT ctid` itself succeeds on an uncompressed hypertable and hands back chunk-local ctids that repeat across chunks, so it is the zero block count and not an error that rules the heap split out"
},
{
"entry": "citus",
"name": "Citus 14.1 (PostgreSQL 18)",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Citus, PostgreSQL wire 18.4; amd64 emulated"
},
"windows_x64": {
"status": "pass",
"detail": "PostgreSQL 18.4 + Citus 14.1.0; distributed-table steps included"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)"
},
"notes": "no quirks; an extension on stock PostgreSQL, so the array-ingest form applies and is used, distributed tables included (verified: the entry also ingests into and reads back a `create_distributed_table()` hash-distributed table); a fresh single node needs no registration -- `create_distributed_table()` registers the coordinator itself with `shouldhaveshards` on; if the coordinator was registered with `citus_set_coordinator_host()` alone, `shouldhaveshards` has to be set too or `create_distributed_table()` fails with `replication_factor (1) exceeds number of worker nodes (0)`; ingest 107k rows/s (array binding), fetch 1.86M rows/s"
},
{
"entry": "cloudberry",
"name": "Apache Cloudberry 2.1.0-incubating (Greenplum fork)",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Apache Cloudberry, PostgreSQL wire 14.4; amd64 emulated"
},
"windows_x64": {
"status": "pass",
"detail": "Apache Cloudberry 2.1.0-incubating, PostgreSQL wire 14.4.0; compose service unchanged, 3 GB / shm 1 GB"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)"
},
"notes": "no driver quirks and no tolerance flags: an MPP cluster of PostgreSQL 14 segments behind one coordinator, driven by the `postgres` entry's types unchanged (and, unlike CockroachDB, needing no `PRIMARY KEY`) \u2014 and since it reports `SQL_DBMS_NAME` \"PostgreSQL\" behind the same `psqlodbcw.so`, no driver-name quirk *could* be correct here without also firing on real PostgreSQL. Cloudberry is named once in the bridge and not as a workaround: `version()` carries `Apache Cloudberry`, a fork marker, so it is excluded from the `unnest` array-ingest form only PostgreSQL proper is claimed to owe (see the array-ingest note above) \u2014 conservative rather than necessary, since the form's own proof query passes and a 5,000-row `unnest` ingest lands on heap, append-optimized row and append-optimized column tables alike, about an order of magnitude faster server-side than the multi-row `INSERT` it keeps (~530k against ~33k rows/s, bare SQL on a shared host); no Apache-published *server* image exists -- `apache/incubator-cloudberry` holds only `cbdb-build-*`/`cbdb-test-*` CI toolchains and `apache/cloudberry-db` does not exist -- so the community `woblerr/cloudberry` image is used, and it needs `--shm-size=1g` or `gpinitsystem` fails; `extra` steps cover what the standard workload cannot tell apart from `postgres`: a `DISTRIBUTED BY` table whose bulk-ingested rows occupy more than one segment plus an aggregate merged on the coordinator, and append-optimized **column-oriented** storage (read from `pg_am` as `ao_column` -- Greenplum 6's `relstorage` is gone in the PostgreSQL 14-based 2.x); ingest 9.7k rows/s (9.8k with array binding), fetch 1.33M rows/s"
},
{
"entry": "materialize",
"name": "Materialize 26.38",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Materialize, PostgreSQL wire 9.5.0"
},
"windows_x64": {
"status": "pass",
"detail": "Materialize v26.38.2, PostgreSQL wire 9.5.0; harness rows need `ADBC_BENCH_AUTOCOMMIT=1`, as everywhere"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)",
"decimal_type": "string"
},
"notes": "streaming warehouse; PostgreSQL SQL layer, so no driver quirks -- but no `SAVEPOINT`, so the entry sets psqlodbc's `Protocol=7.4-0` to stop the driver wrapping the second batch of a large ingest in one (with that setting, a prepared statement freed before a manual commit also lost the transaction's rows through a psqlodbc/Materialize pair of defects: fixed in psqlodbc 18.00.0003 and in Materialize [#38606](https://github.com/MaterializeInc/materialize/pull/38606), merged 2026-09-14, verified on its main image, not yet in a Materialize release); its single 39-digit `NUMERIC` is wider than an Arrow decimal128, so decimals read back as exact strings; also ingests into and reads back an incrementally maintained `MATERIALIZED VIEW`; ingest 23.6k rows/s (23.7k with array binding), fetch 322k rows/s"
},
{
"entry": "opengauss",
"name": "openGauss 6.0",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "server-unavailable",
"detail": "enmotech/opengauss arm64's MOT engine panics at start in the Docker Desktop VM (libnuma / `numa_node_of_cpu(0) => -1` / thread identifiers exhausted \u2192 `Failed to Initialize core services`), with `--cap-add=SYS_NICE --shm-size=1g` and with `--cpuset-cpus=0-7`"
},
"windows_x64": {
"status": "pass",
"detail": "openGauss 6.0.0, PostgreSQL wire 9.2.4; role and database created inside the container as `omm`, as the README says"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)"
},
"notes": "no quirks: a PostgreSQL 9.2 fork, driven by the `postgres` entry's types unchanged; server-side setup only (`CAP_SYS_NICE` for the MOT engine's `mbind()`, `max_process_memory` >= 2 GB, and a role created after start-up because the initial user cannot log in remotely); ingest 220k rows/s (258k with array binding), fetch 1.35M rows/s"
},
{
"entry": "cratedb",
"name": "CrateDB 6.4",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "CrateDB 6.4.3, PostgreSQL wire 14.0.0) \u2014 at 1f35a5c, after the entry accepted psqlodbc 18's decimal128(28, 3"
},
"windows_x64": {
"status": "pass",
"detail": "CrateDB 6.4.3, PostgreSQL wire 14.0.0; `tzdata` PyPI package required on Windows"
}
},
"quirks": {
"binary_text": "\\x0102",
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b TEXT, d TIMESTAMP, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)",
"decimal_type": [
"decimal128(28, 3)",
"decimal128(28, 6)"
],
"ingest_types": "{pa.date32(): pa.timestamp('us'), pa.bool_(): pa.int8()}",
"ts_us": [
"123000"
]
},
"notes": "driver quirk handled: with autocommit off psqlodbc sends `BEGIN;<statement>` as one query string, and CrateDB tags every result of a multi-statement query with the leading text of the whole string (`BEGIN;INSERT 1`, where PostgreSQL sends `BEGIN` then `INSERT 0 1`), so `SQLRowCount` succeeds without writing its out-parameter for any write or DDL inside a transaction \u2014 the same driver and query string against stock PostgreSQL write the count (fixed generically in `OdbcRowCount()`, which now pre-fills *unknown* rather than *none*; reported upstream and fixed in CrateDB 6.4.4 by [crate/crate#20088](https://github.com/crate/crate/pull/20088), the bridge handling stays for earlier versions); `version()` is CrateDB's own, so the PostgreSQL array-ingest form stays off (verified); server side: eventually consistent, so reads follow `REFRESH TABLE`; no binary or `DATE` column type; ingest 50.0k rows/s (45.6k with array binding; one multi-row `INSERT` per batch \u2014 row-at-a-time is ~0.9k rows/s because CrateDB fsyncs its translog once per statement, and psqlodbc sends a parameter array as separate single-row statements), fetch 726k rows/s"
},
{
"entry": "questdb",
"name": "QuestDB 10",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "QuestDB, PostgreSQL wire 11.3"
},
"windows_x64": {
"status": "pass",
"detail": "QuestDB, PostgreSQL wire 11.3.0; psqlodbc 18.00.0002; `ADBC_BENCH_AUTOCOMMIT=1` for the harness rows; shares host port 19000 with the clickhouse service, so the two are never up together"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INT, f DOUBLE, s STRING, b BINARY, d DATE, ts TIMESTAMP, n DECIMAL(10,3), bo BOOLEAN)",
"decimal_type": "decimal128(28, 3)",
"not_null": [
"bo"
]
},
"notes": "own type system behind the PG wire: standard-SQL ingest DDL, `true`/`false` boolean params, parameter arrays off (psqlodbc inlines their values as string literals, which QuestDB will not convert to `BINARY` or `BOOLEAN`); psqlodbc's `SQLColumns` fails here, so `GetObjects` falls back to `SQLDescribeCol` of a zero-row SELECT instead; a `BINARY` value holding a `0x00` byte comes back truncated at the first NUL through psqlodbc (the server keeps the whole value); on a manual-commit connection free a prepared statement only after the commit \u2014 psqlodbc's free sends `SAVEPOINT \u2026; DEALLOCATE \u2026; RELEASE` whatever `Protocol` says, QuestDB has no `SAVEPOINT`, the transaction aborts unreported and `COMMIT` still returns `SQL_SUCCESS` (the psqlodbc path already recorded for Materialize)"
},
{
"entry": "risingwave",
"name": "RisingWave 3.0",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "RisingWave, PostgreSQL wire 13.14) \u2014 compose bind-mount of risingwave.toml refused by Docker Desktop for this account; run with the README's `docker run` and the toml under /private/tmp"
},
"windows_x64": {
"status": "pass",
"detail": "RisingWave 3.0.3, PostgreSQL wire 13.14; the compose bind-mount of risingwave.toml works as-is from PowerShell"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR, b BYTEA, d DATE, ts TIMESTAMP, n NUMERIC, bo BOOLEAN)",
"decimal_type": "decimal128(28, 3)"
},
"notes": "no `adbc.odbc.*` quirks, but the entry sets psqlodbc's `UseServerSidePrepare=0`: psqlodbc names every server-side statement `_PLAN0x<statement-handle address>` and RisingWave keeps the name after the handle is freed, so the next `SQLPrepare` under a reused name fails `XX000 ... Duplicated statement name` -- which the ordinary allocate/prepare/execute/free loop hits on its second query, with or without parameters (concurrently open handles get distinct names and all succeed, re-executing a prepared handle is fine, `SQLExecDirect` never collides); `version()` names RisingWave, so the PostgreSQL array-ingest form stays off (verified); server side: RisingWave's parser takes no length, precision or scale on a column type -- `VARCHAR(50)`/`TIMESTAMP(6)` are parser errors and `NUMERIC(10,3)` an unsupported type, so `VARCHAR` and `NUMERIC` are declared unqualified; `FLOAT(p)` is the one modifier it does take, and honours (1-24 gives `real`, 25-53 `double precision`) -- and a write reaches a scan only at the next barrier (the default 1 s interval here; a COMMIT does not bring it forward), so reads follow `FLUSH`, which forces a barrier straight away (tens of ms), or `SET RW_IMPLICIT_FLUSH = true`, which does that on every write; ingest 31.9k rows/s (35.3k with array binding), fetch 1.00M rows/s"
},
{
"entry": "spanner",
"name": "Google Cloud Spanner (emulator + PGAdapter 0.55)",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "Spanner emulator + PGAdapter, PostgreSQL wire 14.1; 300/2,000 rows as on Linux"
},
"windows_x64": {
"status": "pass",
"detail": "Spanner emulator + PGAdapter 0.55.2, PostgreSQL wire 14.1.0; compat only \u2014 the Python bench is not run against the emulator, see the Windows benchmark file"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i bigint PRIMARY KEY, f double precision, s varchar(50), b bytea, d date, ts timestamptz, n numeric, bo bool)",
"decimal_type": "decimal128(28, 3)",
"ingest_types": "{pa.int32(): pa.int64()}"
},
"notes": "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`). Server side: no 32-bit integer, no `TIMESTAMP WITHOUT TIME ZONE` (so `ts` reads back zone-aware), no modifier on `NUMERIC`, no DDL inside a transaction; also ingests into and reads back an `INTERLEAVE IN PARENT` child 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 at `SQLExecute` (`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"
},
{
"entry": "firebird",
"name": "Firebird 5",
"driver": "Firebird ODBC 3.5.0-rc1",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "driver-unavailable",
"detail": "firebird-odbc-driver v3-0-1 ships Linux and Windows assets only"
},
"windows_x64": {
"status": "pass",
"detail": "Firebird 5.0.4 container; Firebird ODBC 3.0.1.18 `Firebird ODBC Driver` with fbclient.dll from the Firebird 5 zip on PATH) \u2014 **one bridge fix needed**: the driver answers SQL_DRIVER_NAME `FirebirdODBC` on Windows where Linux sees `OdbcFb`, so the quirk block that turns parameter arrays off (the driver accepts SQL_ATTR_PARAMSET_SIZE and executes one set) never fired and the first run stopped with `ODBC driver accepted a parameter array of 2 sets but reported neither SQL_ATTR_PARAMS_PROCESSED_PTR nor SQL_ATTR_PARAM_STATUS_PTR`; matching both names (`src/odbc_driver.c`) makes the whole workload pass, UNION-ALL bulk form included"
}
},
"quirks": {
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s VARCHAR(50), b BLOB SUB_TYPE BINARY, d DATE, ts TIMESTAMP, n NUMERIC(10,3), bo BOOLEAN)",
"decimal_type": "decimal128(18, 3)",
"ident": "str.upper",
"ts_us": [
"123400"
]
},
"notes": "`SQL_C_WCHAR` sized in 4-byte `wchar_t`; parameter arrays are off (`no_param_arrays`): with column-wise binding OdbcFb steps fixed-length C types at the `BufferLength` stride, so every row receives row 1's values with `SQL_SUCCESS` throughout ([firebird-odbc-driver#299](https://github.com/FirebirdSQL/firebird-odbc-driver/issues/299), reproduced in plain ODBC on 3.0.1 and 3.5.0-rc1; fixed upstream in [PR #308](https://github.com/FirebirdSQL/firebird-odbc-driver/pull/308), verified here on its CI build 2026-09-07, so `no_param_arrays` comes off once a release carries it). Firebird's dialect has neither multi-row `VALUES` (-104 `Token unknown` at the second row-group's comma) nor Oracle's `INSERT ALL`, so bulk ingest batches through the third form it does take: `INSERT INTO t (cols) SELECT CAST(? AS <type>), ... FROM RDB$DATABASE UNION ALL SELECT ...` (`multirow_union_from`, probed like every other form and only after the standard one is refused). The `CAST` is not a guess and cannot lose anything -- it names the type ingest itself would create for that Arrow type, so a value too wide for the target column raises `string right truncation` on the INSERT's own assignment exactly as a one-row INSERT would; a column with no such exact type gets no batching. Firebird's 256-context limit caps it at ~250 row-groups. Also handled here: once a NULL has been bound to a `SQL_BIGINT` parameter with `SQL_C_DEFAULT` (whose ODBC default C type is `SQL_C_CHAR`), OdbcFb writes NULL for every later `SQL_C_SBIGINT` rebind of that parameter ([#300](https://github.com/FirebirdSQL/firebird-odbc-driver/issues/300)) -- NULLs are bound `SQL_C_SBIGINT` (`NullParamCType`). `SQL_ATTR_ROWS_FETCHED_PTR` set before `SQLPrepare` is discarded ([#301](https://github.com/FirebirdSQL/firebird-odbc-driver/issues/301)); the bridge sets it after execute and was not affected. Both have upstream fixes from F.D. Castel, [PR #302](https://github.com/FirebirdSQL/firebird-odbc-driver/pull/302) and [PR #303](https://github.com/FirebirdSQL/firebird-odbc-driver/pull/303), verified here on their CI builds on 2026-09-07 (the workarounds stay until a release carries them). Generated ingest DDL used to spell an Arrow string as `BLOB SUB_TYPE TEXT` -- what `SQLGetTypeInfo(SQL_LONGVARCHAR)` names. One run read a 100,000-row table with such a column at 8,256 rows/s against 1,004,277 without it; that slowdown could not be reproduced afterwards (2026-08-28: BLOB and `VARCHAR` columns both read at 300k+ rows/s, plain ODBC and through the bridge, driver 3.0.1 and 3.5.0-rc1), so it is not recorded as a driver property. Strings go in as `VARCHAR(8191)`, legal in any character set (`ddl_string_type_name`), because the column can then be filtered and indexed and reads through ordinary bound columns; the same table ingested at 40,670 rows/s instead of 5,705 in that run. Values over 8,191 characters are refused on insert, as Db2's `VARCHAR(32672)` refuses its last 28 bytes. Ingest 7.9k rows/s (8.4k with array binding, 6.0k with `adbc.odbc.rows_per_insert=1`) on the compat table, fetch 294k rows/s"
},
{
"entry": "virtuoso",
"name": "OpenLink Virtuoso 7.2",
"driver": "Virtuoso ODBC (`virtodbc.so`, ANSI build)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "`OpenLink Virtuoso (via ODBC) 07.20.3243`) through a bridge built against iODBC 3.52.16, once the `virtodbc` quirk stopped forcing the narrow path on a four-byte-`SQLWCHAR` build \u2014 there the driver's narrow charset is single-byte (a `h\u00e9llo` statement literal never matches, and `CHARSET=UTF-8` reads back one byte per unit) while its wide path is correct end to end; Python fetch 252,282 rows/s, ingest 3,128. **Through unixODBC it aborts at the first failing statement**: the driver is iODBC-width and unixODBC's driver manager overflows its own `sqlstate` buffer reading the diagnostic ([lurcher/unixODBC#239](https://github.com/lurcher/unixODBC/issues/239), [openlink/virtuoso-opensource#1469](https://github.com/openlink/virtuoso-opensource/issues/1469"
},
"windows_x64": {
"status": "pass",
"detail": "OpenLink Virtuoso 07.20.3243 container; the Windows Virtuoso Open Source 7.2 installer's `Virtuoso (Open Source)` driver, virtodbc.dll"
}
},
"quirks": {
"bool_type": "int16",
"ddl": "CREATE TABLE adbc_t (i INTEGER, f DOUBLE PRECISION, s NVARCHAR(50), b VARBINARY(10), d DATE, ts DATETIME, n DECIMAL(10,3), bo SMALLINT)"
},
"notes": "ODBC-native server; driver quirks handled: `SQL_C_WCHAR` is a 4-byte `wchar_t` on both the parameter and the fetch side, not unixODBC's 2-byte `SQLWCHAR` (UTF-8 narrow path instead), `SQL_C_SBIGINT` is stored as 0 and read back as 0 without a diagnostic (64-bit ints sent as text declared `SQL_NUMERIC`, the one declaration that converts exactly), and datetime parameter arrays are strided by `ColumnSize` rather than by the C struct \u2014 a `DATE32` bound with `ColumnSize` 0 repeats row 0 (so no parameter arrays); no `BOOLEAN` type. Two more to know: an `SQL_C_SLONG` parameter is read as 8 bytes whatever `BufferLength` says, so two `int32` parameters bound from adjacent slots fail with `SR346: Integer out of range`; and any `Charset=` value but the exact literal `UTF-8` aborts the process inside `SQLDriverConnect` (`GPF: Dkbox.c:638 Double free`). The Unicode build `virtodbcu.so` is built to a 4-byte `SQLWCHAR`, so unixODBC's driver manager aborts reading its first error diagnostic \u2014 [lurcher/unixODBC#239](https://github.com/lurcher/unixODBC/issues/239), see [`UPSTREAM.md`](UPSTREAM.md); ingest 11.9k rows/s, fetch 1.04M rows/s"
},
{
"entry": "flightsql",
"name": "Arrow Flight SQL (sqlflite 1.5.5 / DuckDB 1.1.1)",
"driver": "Flight SQL ODBC 0.9.7 (Dremio, open source)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "`sqlflite (via ODBC) 00.00.0000`, DuckDB 1.1.1; read-only) through a bridge built against iODBC 3.52.16 \u2014 Python fetch 8,260,509 rows/s. **Through unixODBC it aborts at the first failing statement**: the driver is built to iODBC's 4-byte `SQLWCHAR`, and unixODBC 2.3.12's driver manager overflows its own stack buffer (`SQLWCHAR sqlstate[6]` in `extract_diag_error_w`) reading the wide diagnostic on the first `SQL_ERROR` \u2192 `__stack_chk_fail` \u2192 SIGABRT with no message. Not the driver crashing, not the bridge: unixODBC's `isql` dies the same way, and through iODBC the same programs get the proper diagnostic"
},
"windows_x64": {
"status": "pass",
"detail": "sqlflite 1.5.5 / DuckDB 1.1.1; arrow-flight-sql-odbc LATEST-win64, 00.09.0007) \u2014 **with the `text_as_binary` fix in this tree**: the driver's Windows build returns U+1F680 as **U+F680** (the low 16 bits) through SQL_C_WCHAR and `?` through SQL_C_CHAR (the ANSI code page), pyodbc identical, so the first run failed the astral check; its SQL_C_BINARY conversion of a text column hands the server's UTF-8 through byte-exact (probed with pyodbc), and the reader now takes that route for this driver on Windows. It is also the faster route \u2014 5,211,319 rows/s against 2,956,638 wide, the fastest fetch of the Windows column"
}
},
"quirks": {
"big_rows": 100000,
"ddl": "'CREATE TABLE adbc_t ' + FLIGHTSQL_COLS",
"decimal_type": "string",
"params": false,
"read_only": true
},
"notes": "driver has no `SQLBindParameter` at all, so nothing can be written through it; driver quirks handled: `SQLColumns` segfaults on the first `SQLFetch`, so `GetObjects` describes a zero-row SELECT instead (`no_sql_columns`); every `DECIMAL` described as (19,0), so decimals are read as exact text; fetch 1.32M rows/s"
},
{
"entry": "arcadedb",
"name": "ArcadeDB 26.9 (PostgreSQL wire)",
"driver": "psqlodbc 16 (PG wire)",
"results": {
"linux": {
"status": "pass",
"detail": null
},
"macos_arm64": {
"status": "pass",
"detail": "ArcadeDB, PostgreSQL wire 12.0.0; read-only fixture"
},
"windows_x64": {
"status": "pass",