A Native ADBC Driver for MariaDB

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:

FeatureMariaDB driverMySQL ADBC driver 0.5.0
SQL query executionYesYes
Arrow result batchesYesYes
Prepared statementsYesYes
Parameter schemaPartial: parameter count with unknown typesDocumented as unsupported
Bulk ingest: createYesYes
Bulk ingest: appendYesYes
Bulk ingest: create or appendYesYes
Bulk ingest: replaceYesYes
Temporary-table ingestionYesYes
Target catalog for ingestionNot yetNo
Target schema for ingestionNot applicable/not yetNo
GetObjects catalogsYesYes
GetObjects tablesYesYes
GetObjects columnsYesYes
GetTableSchemaYesYes
Prepared transactionsNot applicable
TransactionsYesDocumented as unsupported
Current catalog get/setYesNot clearly documented
StatisticsYesNot listed in the feature table
GeoArrow WKB ingestionYesNot documented
Partitioned result setsNot yetNot documented
Substrait plansNot yetNot 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:

  1. connections;
  2. SQL execution;
  3. Arrow result streams;
  4. prepared statements;
  5. parameter binding;
  6. bulk ingestion;
  7. transactions;
  8. table metadata;
  9. table schemas;
  10. predictable type conversion.

The remaining work is mainly advanced functionality, performance optimization, broader type coverage and packaging.

My priorities for the driver are:

  1. improve the documentation and publish a detailed feature matrix;
  2. produce reproducible binary releases and a driver manifest;
  3. investigate LOAD DATA LOCAL INFILE for faster ingestion;
  4. improve rich MariaDB error details;
  5. 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.

Subscribe to Blog via Email

Enter your email address to subscribe to this blog and receive notifications of new posts by email.

Leave a Reply

Your email address will not be published. Required fields are marked *