#include <stdlib.h>What’s the tiniest ADBC driver you could build?
As of writing, the ADBC spec lists 52 functions a driver can define but it doesn’t specifically say which you have to define or in what order you should tackle them as you build a driver.
While working with the validation framework for the ADBC Driver Foundry, I got to wondering about this question.
ADBC drivers can be implemented a few ways, and one of the most common is to build them as a shared library that exports symbols from the ADBC C API. This shared library is then loaded by an ADBC Driver Manager and the program calls into the driver through the Driver Manager.
I remembered seeing a comment somewhere that a Driver Manager, when loading a driver, can actually fill in some methods for a driver that doesn’t implement them. This gave me an idea for how to approach my original question.
To figure this out, I’m going to build a driver in C that defines only as many ADBC methods as needed to be loadable by a driver manager. I’ll do it as a literate program (this source file) and test it with the Python adbc-driver-manager which wraps the C++ driver manager.
When all is done, I’ll build and test the driver by running:
$ make
$ uv run pytest
If you have any questions or comments, please feel free to email me at brycemecum@gmail.com.
I’ll walk through what it takes to build a very tiny ADBC driver. For simplicity, the driver isn’t going to connect to any external or in-memory database or do much of anything but it will load successfully with the driver manager.
I’ll include this because I need to allocate at least some memory to uphold the API contract.
#include <stdlib.h>Then I’ll include the ADBC C header. I’ve vendored it in this repo for simplicity.
#include "../vendor/adbc/adbc.h"Earlier I mentioned some 52 functions in the ADBC API. After doing some testing, I narrowed down the minimum set to just 13 functions.
Here I forward declare them so you can see them all together at once and I’ll
actually define their bodies later on. To ensure the driver manager can find
these symbols, each is marked with ADBC_EXPORT.
ADBC_EXPORT AdbcStatusCode AdbcDriverInit(int version, void *raw_driver,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcDatabaseNew(struct AdbcDatabase *database,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcDatabaseSetOption(struct AdbcDatabase *database,
const char *key,
const char *value,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase *database,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcDatabaseRelease(struct AdbcDatabase *database,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcConnectionNew(struct AdbcConnection *connection,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcConnectionInit(struct AdbcConnection *connection,
struct AdbcDatabase *database,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcConnectionRelease(
struct AdbcConnection *connection, struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode
AdbcConnectionSetOption(struct AdbcConnection *connection, const char *key,
const char *value, struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcStatementNew(struct AdbcConnection *connection,
struct AdbcStatement *statement,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcStatementRelease(struct AdbcStatement *statement,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode
AdbcStatementSetSqlQuery(struct AdbcStatement *statement, const char *query,
struct AdbcError *error);
ADBC_EXPORT AdbcStatusCode AdbcStatementExecuteQuery(
struct AdbcStatement *statement, struct ArrowArrayStream *out,
int64_t *rows_affected, struct AdbcError *error);Now on to implementation. I’ll explain a bit about each as I go and also about what the relevant parts of the API contract are.
AdbcDatabaseNew normally allocates an AdbcDatabase after which the
Database would be in a created but uninitialized state. My driver can just
return ADBC_STATUS_OK.
ADBC_EXPORT AdbcStatusCode AdbcDatabaseNew(struct AdbcDatabase *database,
struct AdbcError *error) {
return ADBC_STATUS_OK;
}The driver manager requires drivers to define AdbcDatabaseSetOption but
it’s most correct to return ADBC_STATUS_NOT_IMPLEMENTED to make it clear
that calling this won’t do anything.
ADBC_EXPORT AdbcStatusCode AdbcDatabaseSetOption(struct AdbcDatabase *database,
const char *key,
const char *value,
struct AdbcError *error) {
return ADBC_STATUS_NOT_IMPLEMENTED;
}AdbcDatabaseInit normally does any final prep before initializing the
database with the options that may have been set. Since my driver doesn’t
support options, I just return ADBC_STATUS_OK.
ADBC_EXPORT AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase *database,
struct AdbcError *error) {
return ADBC_STATUS_OK;
}AdbcDatabaseRelease would ensure any open connections are cleaned up
before freeing its own resources but my driver can just mark this as
ADBC_STATUS_NOT_IMPLEMENTED since there are no resources to free in this
driver.
ADBC_EXPORT AdbcStatusCode AdbcDatabaseRelease(struct AdbcDatabase *database,
struct AdbcError *error) {
return ADBC_STATUS_NOT_IMPLEMENTED;
}The driver manager treats any driver that doesn’t set private_data to a
non-null pointer as uninitialized (unusable) so I just do a tiny allocation
here to make it happy.
ADBC_EXPORT AdbcStatusCode AdbcConnectionNew(struct AdbcConnection *connection,
struct AdbcError *error) {
connection->private_data = malloc(1);
if (!connection->private_data) {
return ADBC_STATUS_INVALID_STATE;
}
return ADBC_STATUS_OK;
}Similar to AdbcDatabaseInit, I just return ADBC_STATUS_OK for
AdbcConnectionInit.
ADBC_EXPORT AdbcStatusCode AdbcConnectionInit(struct AdbcConnection *connection,
struct AdbcDatabase *database,
struct AdbcError *error) {
return ADBC_STATUS_OK;
}Because AdbcConnectionNew allocated private_data and, as part of the API
contract, AdbcConnectionRelease needs to release private_data.
ADBC_EXPORT AdbcStatusCode AdbcConnectionRelease(
struct AdbcConnection *connection, struct AdbcError *error) {
if (connection->private_data) {
free(connection->private_data);
connection->private_data = NULL;
}
return ADBC_STATUS_OK;
}I can leave this as ADBC_STATUS_NOT_IMPLEMENTED just like
AdbcDatabaseSetOption since it won’t do anything.
ADBC_EXPORT AdbcStatusCode
AdbcConnectionSetOption(struct AdbcConnection *connection, const char *key,
const char *value, struct AdbcError *error) {
return ADBC_STATUS_NOT_IMPLEMENTED;
}The next three functions can just return ADBC_STATUS_OK. Normally, these
would allocate, modify, and release private_data related to a statement
that doesn’t apply since this driver doesn’t execute any queries.
ADBC_EXPORT AdbcStatusCode AdbcStatementNew(struct AdbcConnection *connection,
struct AdbcStatement *statement,
struct AdbcError *error) {
return ADBC_STATUS_OK;
}Because AdbcStatementNew didn’t do anything, AdbcStatementRelease doesn’t
need to do anything either.
ADBC_EXPORT AdbcStatusCode AdbcStatementRelease(struct AdbcStatement *statement,
struct AdbcError *error) {
return ADBC_STATUS_NOT_IMPLEMENTED;
}Same for AdbcStatementSetSqlQuery.
ADBC_EXPORT AdbcStatusCode
AdbcStatementSetSqlQuery(struct AdbcStatement *statement, const char *query,
struct AdbcError *error) {
return ADBC_STATUS_NOT_IMPLEMENTED;
}and AdbcStatementExecuteQuery.
ADBC_EXPORT AdbcStatusCode AdbcStatementExecuteQuery(
struct AdbcStatement *statement, struct ArrowArrayStream *out,
int64_t *rows_affected, struct AdbcError *error) {
return ADBC_STATUS_NOT_IMPLEMENTED;
}This isn’t part of the ADBC API but the driver manager will need this to be defined.
AdbcStatusCode AdbcDriverRelease(struct AdbcDriver *driver,
struct AdbcError *error) {
return ADBC_STATUS_NOT_IMPLEMENTED;
}Last is AdbcDriverInit. When the driver manager loads a driver, it calls
this function to set up its mappings to the functions in it.
ADBC_EXPORT
AdbcStatusCode AdbcDriverInit(int version, void *raw_driver,
struct AdbcError *error) {
struct AdbcDriver *driver = (struct AdbcDriver *)raw_driver;
driver->release = AdbcDriverRelease;
driver->DatabaseNew = AdbcDatabaseNew;
driver->DatabaseInit = AdbcDatabaseInit;
driver->DatabaseRelease = AdbcDatabaseRelease;
driver->DatabaseSetOption = AdbcDatabaseSetOption;
driver->ConnectionNew = AdbcConnectionNew;
driver->ConnectionInit = AdbcConnectionInit;
driver->ConnectionRelease = AdbcConnectionRelease;
driver->ConnectionSetOption = AdbcConnectionSetOption;
driver->StatementNew = AdbcStatementNew;
driver->StatementRelease = AdbcStatementRelease;
driver->StatementSetSqlQuery = AdbcStatementSetSqlQuery;
driver->StatementExecuteQuery = AdbcStatementExecuteQuery;
return ADBC_STATUS_OK;
}And that’s it.
The driver manager should now be able to load this driver once it’s built.
To build the driver, run:
$ make build
I tried to make this work in a cross-platform way so hopefully this will work for you.
To test the driver, open up a Python REPL with the adbc_driver_manager
available:
uv run --with "adbc_driver_manager" python
and then execute:
from adbc_driver_manager import dbapi
with dbapi.connect(driver="libtiny.so") as conn:
with conn.cursor() as cursor:
pass
Alternatively, run the automated tests with uv run pytest.