
Apache Arrow Database Connectivity (ADBC) provides a standard interface for exchanging data between applications and databases using Apache Arrow.
You can think of it as an Arrow-oriented alternative to traditional database APIs such as ODBC and JDBC. The important difference is that ADBC is designed around columnar data. Query results are returned as streams of Arrow record batches, and Arrow data can also be sent back to the database for parameter binding and bulk ingestion.
ADBC is especially interesting for analytical applications, data science tools, and query engines, where converting millions of individual database rows into another in-memory representation can become expensive.
Initially, to work with DuckDB, I have been working on a dedicated ADBC driver for MariaDB:
https://github.com/lefred/adbc-driver-mariadb
This post describes what already works, how it compares with the existing MySQL ADBC driver, and which parts of the ADBC specification still need work.
What the driver already supports
The MariaDB driver is already well beyond a proof of concept. It supports the main lifecycle expected from a useful ADBC SQL driver:
- opening databases and connections;
- executing SQL queries;
- returning query results as Arrow record batches;
- executing statements that do not return a result set;
- prepared statements;
- positional parameter binding;
- Arrow-based bulk ingestion;
- transactions;
- database metadata;
- table and column metadata;
- table schemas;
- table and index statistics;
- ADBC driver-manager manifests.
The implementation uses the Go ADBC interfaces and the shared Go driver framework. As a result, it can be used directly by Go applications or compiled into a shared ADBC driver for applications that use the ADBC driver manager, which is the case with DuckDB.
ADBC defines databases, connections, and statements as its main abstractions. It also standardizes metadata discovery, bulk ingestion, cancellation, statistics, and partitioned result sets.
Query execution and Arrow results
Regular SQL queries are executed against MariaDB and exposed to the caller as Arrow data.
Instead of iterating through a traditional row-oriented result set, an ADBC application receives a stream of Arrow record batches. This is particularly useful when the next consumer is already Arrow-aware, such as:
- DuckDB;
- Polars;
- PyArrow;
- DataFusion;
- analytical Python or R applications;
- federated query engines.
This does not mean that the MariaDB wire protocol suddenly becomes columnar. Internally, MariaDB still returns rows through its normal client protocol. The driver converts those rows into Arrow arrays and batches.
The benefit is at the application boundary: once the data has entered the Arrow ecosystem, it can be transferred between compatible tools without repeatedly converting it into different object or row representations.
Comparison with the MySQL ADBC driver
There is already an ADBC Driver Foundry driver for MySQL. Despite its name, its documentation states that it supports both MySQL and MariaDB. The latest documented release at the time of writing is version 0.5.0, released on July 10, 2026.
The two drivers share a similar general architecture. Both are Go-based drivers built around the ADBC Go driver framework and Go’s MySQL client ecosystem.
However, the dedicated MariaDB driver is not simply the MySQL driver with a different name. It is becoming MariaDB-aware in areas where product behavior and metadata differ.
Here is the practical comparison as of July 2026:
| Feature | MariaDB driver | MySQL ADBC driver 0.5.0 |
|---|---|---|
| SQL query execution | Yes | Yes |
| Arrow result batches | Yes | Yes |
| Prepared statements | Yes | Yes |
| Parameter schema | Partial: parameter count with unknown types | Documented as unsupported |
| Bulk ingest: create | Yes | Yes |
| Bulk ingest: append | Yes | Yes |
| Bulk ingest: create or append | Yes | Yes |
| Bulk ingest: replace | Yes | Yes |
| Temporary-table ingestion | Yes | Yes |
| Target catalog for ingestion | Not yet | No |
| Target schema for ingestion | Not applicable/not yet | No |
GetObjects catalogs | Yes | Yes |
GetObjects tables | Yes | Yes |
GetObjects columns | Yes | Yes |
GetTableSchema | Yes | Yes |
| Prepared transactions | Not applicable | |
| Transactions | Yes | Documented as unsupported |
| Current catalog get/set | Yes | Not clearly documented |
| Statistics | Yes | Not listed in the feature table |
| GeoArrow WKB ingestion | Yes | Not documented |
| Partitioned result sets | Not yet | Not documented |
| Substrait plans | Not yet | Not documented |
The MySQL driver has one major advantage today: packaging and distribution.
It has published releases, documentation and prebuilt binaries that can be installed with:
dbc install mysql
It also provides usage examples for Go, Python, R and Rust through the ADBC driver manager.
The dedicated MariaDB driver still needs the same level of release engineering and user-facing documentation.
Where the MariaDB driver is ahead
The dedicated MariaDB implementation currently appears ahead in several areas.
I’ve also created some scripts using both drivers (mysql and mariadb) to compare them.
The first script is dedicated to comparing data type handling. There is a distinction between what the protocol sends (query) and how the metadata is retrieved.
$ python compare_adbc_mysql_mariadb.py
MariaDB server: 127.0.0.1:13100/test
The password is intentionally not printed.
=== UUID (protocol reports CHAR) ===
mysql :
Arrow type: string
value: '019f6e5e-f3d3-71e4-bf59-917f4080b419'
metadata:
sql.column_name
= id
sql.database_type_name
= CHAR
mariadb:
Arrow type: string
value: '019f6e5e-f418-7d14-85eb-5b08881fc905'
metadata:
sql.column_name
= id
sql.database_type_name
= CHAR
=== VECTOR (protocol reports VARBINARY) ===
mysql :
Arrow type: binary
value: 0x0000a03f000020c000007040
metadata:
sql.column_name
= embedding
sql.database_type_name
= VARBINARY
mariadb:
Arrow type: binary
value: 0x0000a03f000020c000007040
metadata:
sql.column_name
= embedding
sql.database_type_name
= VARBINARY
=== INET4 (protocol reports CHAR) ===
mysql :
Arrow type: string
value: '192.168.1.10'
metadata:
sql.column_name
= ipv4
sql.database_type_name
= CHAR
mariadb:
Arrow type: string
value: '192.168.1.10'
metadata:
sql.column_name
= ipv4
sql.database_type_name
= CHAR
=== MariaDB geometry ===
mysql :
Arrow type: binary
value: 0x0000000001010000006666666666661140cdcccccccc6c4940
metadata:
mysql.is_spatial
= true
sql.column_name
= location
sql.database_type_name
= GEOMETRY
mariadb:
Arrow type: binary
value: 0x01010000006666666666661140cdcccccccc6c4940
metadata:
mariadb.geometry_encoding
= internal_srid_wkb
mariadb.is_spatial
= true
mariadb.logical_arrow_type
= geoarrow.wkb
sql.column_name
= location
sql.database_type_name
= GEOMETRY
=== 65-digit DECIMAL ===
mysql :
Arrow type: decimal256(65, 30)
value: Decimal('12345678901234567890123456789012345.123456789012345678901234567890')
metadata:
sql.column_name
= precise_value
sql.database_type_name
= DECIMAL
sql.precision
= 65
sql.scale
= 30
mariadb:
Arrow type: string
value: '12345678901234567890123456789012345.123456789012345678901234567890'
metadata:
mariadb.arrow_fallback
= string
mariadb.logical_arrow_type
= decimal256(65,30)
sql.column_name
= precise_value
sql.database_type_name
= DECIMAL
sql.precision
= 65
sql.scale
= 30
=== negative TIME duration ===
mysql :
ERROR: UNKNOWN: [mysql] failed to append value to column 0: cannot convert larger than microsecond precision to time64us
mariadb:
Arrow type: duration[us]
value: datetime.timedelta(days=-35, seconds=3600, microseconds=876544)
metadata:
mariadb.native_type
= time_duration
sql.column_name
= elapsed
sql.database_type_name
= TIME
sql.fractional_seconds_precision
= 6
=== YEAR ===
mysql :
Arrow type: extension<arrow.opaque[storage_type=string, type_name=YEAR, vendor_name=MySQL]>
value: '2026'
metadata:
sql.column_name
= event_year
sql.database_type_name
= YEAR
mariadb:
Arrow type: int16
value: 2026
metadata:
mariadb.native_type
= year
sql.column_name
= event_year
sql.database_type_name
= YEAR
=== JSON query result (protocol reports LONGTEXT) ===
mysql :
Arrow type: string
value: '{"name":"MariaDB","valid":true}'
metadata:
sql.column_name
= document
sql.database_type_name
= TEXT
mariadb:
Arrow type: string
value: '{"name":"MariaDB","valid":true}'
metadata:
sql.column_name
= document
sql.database_type_name
= TEXT
=== ADBC metadata discovery ===
mysql :
invisible remarks: 'secret'
generated flag: None
JSON Arrow type: string
JSON metadata:
sql.column_name
= document
sql.database_type_name
= longtext
sql.length
= 4294967295
Native schema types:
native_uuid
= opaque(UUID)
embedding
= opaque(VECTOR)
address
= opaque(INET4)
ENUM/SET allowed values:
ENUM = (not reported)
SET = (not reported)
Constraints: (not reported)
mariadb:
invisible remarks: 'secret [MariaDB: INVISIBLE]'
generated flag: True
JSON Arrow type: extension<arrow.json>
JSON metadata:
mariadb.character_set
= utf8mb4
mariadb.collation
= utf8mb4_bin
mariadb.column_type
= longtext
mariadb.native_type
= json
sql.column_name
= document
sql.database_type_name
= JSON
Native schema types:
native_uuid
= extension<arrow.uuid>
embedding
= fixed_list<float32>[3]
address
= string
ENUM/SET allowed values:
ENUM = ["new","active","disabled"]
SET = ["read","write","admin"]
Constraints: CHECK, PRIMARY KEY
We can notice a huge difference in the metadata discovery.
Transactions
The MariaDB driver supports autocommit, commit, rollback and supported isolation levels. The MySQL driver’s current feature table says transactions are not supported.
Once again, I’ve created a small script comparing both drivers when using transactions:
$ python compare_adbc_transactions.py
MariaDB server: 127.0.0.1:13100/test
Requested DB-API mode: autocommit=False
=== mysql ADBC driver ===
server @@autocommit: 1
SELECT * snapshots:
initial table: []
after INSERT 1: [1]
after ROLLBACK: [1]
after INSERT 2: [1, 2]
after COMMIT: [1, 2]
warnings:
Cannot disable autocommit; conn will not be DB-API 2.0 compliant
result: rollback was not effective
=== mariadb ADBC driver ===
server @@autocommit: 0
SELECT * snapshots:
initial table: []
after INSERT 1: [1]
after ROLLBACK: []
after INSERT 2: [2]
after COMMIT: [2]
warnings: none
result: transactions work
Note that the DuckDB ADBC implementation does not yet support transactions.
$ python mariadb_transaction_isolation.py \
--uri mariadb://msandbox:msandbox@127.0.0.1:13100
Connecting with MariaDB ADBC driver: mariadb
Transaction isolation levels
read uncommitted ADBC=adbc.connection.transaction.isolation.read_uncommitted
MariaDB=READ-UNCOMMITTED
read committed ADBC=adbc.connection.transaction.isolation.read_committed
MariaDB=READ-COMMITTED
repeatable read ADBC=adbc.connection.transaction.isolation.repeatable_read
MariaDB=REPEATABLE-READ
serializable ADBC=adbc.connection.transaction.isolation.serializable
MariaDB=SERIALIZABLE
Parameter schema
The MariaDB driver returns a useful parameter-count schema, even though the actual types must remain unknown because of the database/sql limitation.
$ python mariadb_parameters.py \
--uri mariadb://msandbox:msandbox@127.0.0.1:13100
Connecting with MariaDB ADBC driver: mariadb
Parameter schema discovery
SQL: SELECT CAST(? AS SIGNED) AS identifier, CAST(? AS CHAR) AS label
Parameters: 2
$1: name='', type=null, nullable=True
$2: name='', type=null, nullable=True
Bound execution result:
pyarrow.Table
identifier: int64
label: string
----
identifier: [[42]]
label: [["MariaDB ADBC"]]
What is important here and not totally obvious when we check the execution of the script is this part of the code:
cursor.execute(query, [42, "MariaDB ADBC"])
Statistics
The MariaDB driver implements both standard and MariaDB-specific statistics discovery.
This is what it looks like when using connection.adbc_get_statistic_names().read_all():
$ python compare_adbc_statistics.py
MariaDB server: 127.0.0.1:13100/test
Demo rows: (1, alpha), (2, alpha), (3, beta)
=== mysql ADBC driver ===
statistics unavailable: NOT_IMPLEMENTED: [MySQL] GetStatisticNames
=== mariadb ADBC driver ===
custom statistic names:
1024: mariadb.statistic.data_length
1025: mariadb.statistic.index_length
1026: mariadb.statistic.avg_row_length
statistics:
adbc.statistic.row_count
scope = <table>
value = 3 (approx)
mariadb.statistic.data_length
scope = <table>
value = 16384 (approx)
mariadb.statistic.index_length
scope = <table>
value = 16384 (approx)
mariadb.statistic.avg_row_length
scope = <table>
value = 5461 (approx)
adbc.statistic.distinct_count
scope = category
value = 3 (approx)
adbc.statistic.distinct_count
scope = id
value = 3 (approx)
Spatial ingestion
GeoArrow WKB fields are recognized and passed through ST_GeomFromWKB.
python mariadb_spatial_ingestion.py
Connecting with MariaDB ADBC driver: mariadb
Input Arrow schema:
id: int64 not null
location: binary
-- field metadata --
mariadb.logical_arrow_type: 'geoarrow.wkb'
Rows ingested: 3
Stored MariaDB geometries:
id=1 WKT='POINT(4.3517 50.8503)'
WKB=01010000004703780B24681140F7E461A1D66C4940
id=2 WKT='POINT(5.5797 50.6326)'
WKB=0101000000FDF675E09C5116408CB96B09F9504940
id=3 WKT=None
WKB=None
Where the MySQL driver is ahead
The MySQL driver is currently ahead in ecosystem maturity.
Releases
It has versioned releases and precompiled artifacts for multiple architectures.
Installation
It can be installed through the dbc package manager.
Documentation
It has a public compatibility matrix, connection-string documentation, type mappings and examples.
Multi-language consumption
Although both drivers can theoretically be exposed through the C driver interface, the MySQL driver’s Python, R and Rust usage is already documented and packaged.
For the MariaDB driver, these are less about missing database functionality and more about making the driver straightforward to obtain and use.
What is still missing from the ADBC specification
The driver covers a substantial part of the ADBC interface, but it does not yet implement every optional or advanced capability.
Partitioned result sets
ADBC statements can expose a result as multiple independent partitions. Clients can then process those partitions using different threads, processes or machines.
This is represented by ExecutePartitions and ReadPartition. ADBC 1.1 also supports incremental partition discovery.
The MariaDB driver does not currently implement this.
MariaDB’s normal client protocol returns a single result stream, so implementing meaningful partitions would require a strategy such as:
- splitting a query by primary-key ranges;
- using application-supplied partitioning expressions;
- creating temporary tables;
- using parallel connections;
- integrating with a MariaDB feature capable of producing independent result fragments.
Returning one artificial partition would satisfy only part of the interface and would not provide the intended parallelism.
Substrait plans
ADBC statements can optionally accept serialized Substrait plans instead of SQL.
The MariaDB driver currently accepts SQL only.
MariaDB does not natively execute Substrait, so this would require a Substrait-to-SQL translation layer. It is therefore not a small driver feature and may be better implemented by an external query engine.
Native query cancellation
The Go interfaces use context.Context, allowing deadlines and cancellations to be propagated through the driver framework. However, full server-side cancellation deserves additional work and testing.
Ideally, canceling a running query should reliably interrupt the query on the MariaDB server, rather than merely stopping the client from waiting for results.
A robust implementation could use:
- connector-level context cancellation;
- MariaDB query interruption;
- a second administrative connection issuing
KILL QUERY; - clear ADBC status mapping for cancelled operations.
Rich error metadata
The driver already maps database errors into ADBC errors, including vendor codes and SQLSTATE where available.
ADBC 1.1 also permits structured rich error metadata.
The MariaDB driver does not yet expose extensive MariaDB-specific structured details. Possible additions include:
- constraint names;
- conflicting key values;
- server host information;
- replication-related error context;
- deadlock diagnostics;
- warnings returned by the server.
Roadmap
For practical use, the most important capabilities are already present:
- connections;
- SQL execution;
- Arrow result streams;
- prepared statements;
- parameter binding;
- bulk ingestion;
- transactions;
- table metadata;
- table schemas;
- predictable type conversion.
The remaining work is mainly advanced functionality, performance optimization, broader type coverage and packaging.
My priorities for the driver are:
- improve the documentation and publish a detailed feature matrix;
- produce reproducible binary releases and a driver manifest;
- investigate
LOAD DATA LOCAL INFILEfor faster ingestion; - improve rich MariaDB error details;
- evaluate useful approaches to partitioned execution.
Conclusion
The MariaDB ADBC driver is already usable for real SQL and Arrow workflows.
It supports queries, prepared statements, bulk ingestion, transactions, metadata, schemas and statistics. In some areas, particularly transactions, parameter-schema discovery, and statistics, it currently goes beyond the published feature set of the existing MySQL ADBC driver.
The MySQL driver remains more mature in terms of packaging and distribution. It has releases, documentation, and straightforward installation through dbc.
The next stage for the MariaDB driver is therefore not only to add code. It is also to improve conformance reporting, packaging, documentation, and integration with the wider ADBC ecosystem.
The source code is available here:
https://github.com/lefred/adbc-driver-mariadb
Contributions, testing, and feedback are welcome.