Skip to main content

Database drivers

A sources.db instance names an engine, and the engine decides what has to be in the image: odbc speaks to any SQL engine through unixODBC and that engine's own ODBC driver, clickhouse and mongodb are pure Rust clients with nothing to install. The table below is one row per engine: the driver, its licence, whether the runtime image ships it or the operator adds it in a driver layer, the connection form, what a tail needs of its key, and what the tail commits. The grammar every row shares (stores, batch, filter, accumulate) is annotated in config.example.yaml; the code is crates/db/src/.

What the image ships​

The release binary is built with db-odbc, and the runtime image the deployment contract generates (Dockerfile, from dfe-fetcher emit-dockerfile) installs the unixODBC driver manager (libodbc2) and the three open-licence drivers as Debian packages: psqlodbc (odbc-postgresql), MariaDB Connector/ODBC (odbc-mariadb) and sqliteodbc (libsqliteodbc). Each registers itself in /etc/odbcinst.ini on install, so odbcinst -q -d in the image lists PostgreSQL Unicode, MariaDB Unicode and SQLite3. db-clickhouse and db-mongodb need nothing from the image.

unixODBC is the one C library the binary links dynamically: odbc-sys has no load-on-demand mode, so the binary needs libodbc.so.2 at start, and the driver manager loads each engine's driver by name from odbcinst.ini at connect time.

The Microsoft ODBC Driver 18 for SQL Server ships in the image once the deployment contract can install it under Microsoft's licence. Until the operator accepts that licence (https://aka.ms/odbc18eula) with odbc.mssql.accept_eula: true (env DFE_FETCHER__ODBC__MSSQL__ACCEPT_EULA=true), a mssql instance is refused at load. Microsoft's install steps are at https://learn.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server.

Oracle, Snowflake, Databricks and BigQuery drivers are click-through downloads the operator installs under their own licences, in a driver layer built FROM our image. The vendor's installer registers the driver in odbcinst.ini, or the layer appends the entry itself:

FROM ghcr.io/hyperi-io/dfe-fetcher:<version>
USER root
COPY snowflake-odbc.deb /tmp/
RUN apt-get update && apt-get install -y /tmp/snowflake-odbc.deb && rm -rf /var/lib/apt/lists/* /tmp/*.deb
USER 1000

The matrix​

Engineengine / dialectDriverLicenceIn the imageConnection formTail keyCheckpoint
PostgreSQL, and the PostgreSQL wire (Redshift, CockroachDB, Cloud SQL, RDS)odbc / postgrespsqlodbc (odbc-postgresql, PostgreSQL Unicode)LGPL-2.1ShippedDriver={PostgreSQL Unicode};Server=..;Port=..;Database=..;Uid=..;Pwd=..;UseDeclareFetch=1;BoolsAsChar=0 -- UseDeclareFetch=1 is required or the driver buffers the whole result set; BoolsAsChar=0 makes booleans JSON booleansNOT NULL columns of the query's result; a plain name folds to lower case, "Name" keeps its caseKey tuple of the last acknowledged row
MariaDB, MySQL, and their managed formsodbc / mysqlMariaDB Connector/ODBC (odbc-mariadb, MariaDB Unicode); works against MySQL serversLGPL-2.1 (MySQL Connector/ODBC is GPL and cannot ship)ShippedDriver={MariaDB Unicode};Server=..;Port=..;Database=..;User=..;Password=..;NO_CACHE=1;FORWARDONLY=1 -- both knobs required or the driver buffers the result setNOT NULL columns; names keep their case, `name` delimitsKey tuple of the last acknowledged row
ClickHouse, ClickHouse CloudclickhouseThe official HTTP client crate plus dfe-clickhouse (pure Rust)Apache-2.0Nothing to installhttp[s]://user:password@host:8123/databaseColumns of the query's result; their types are read from DESCRIBE (query) at the first tail and bound as {k<i>:Type} placeholdersKey tuple of the last acknowledged row
SQL Server, Azure SQL, Synapseodbc / mssqlMicrosoft ODBC Driver 18 (msodbcsql18) from packages.microsoft.comMicrosoft EULA, click-through, redistributable under its termsShips once the contract can install it; refused at load until odbc.mssql.accept_eula: trueDriver={ODBC Driver 18 for SQL Server};Server=host,1433;Database=..;Uid=..;Pwd=..;Encrypt=yesNOT NULL columns; names keep their case, [name] delimitsKey tuple of the last acknowledged row
Oracle, Autonomous, RDS Oracleodbc / oracleOracle Instant Client ODBCOTN licence, click-throughOperator driver layerDriver={Oracle ODBC driver};DBQ=host:1521/service;UID=..;PWD=..NOT NULL columns; a plain name folds to UPPER case, "Name" keeps its case; 12c Release 1 or laterKey tuple of the last acknowledged row
SQLiteodbc / sqlitesqliteodbc (libsqliteodbc, SQLite3)Permissive (the driver's own BSD-style licence)ShippedDriver={SQLite3};Database=/path/to/file.dbNOT NULL columns; names keep their caseKey tuple of the last acknowledged row
Snowflakeodbc / snowflakeSnowflake ODBC driverSnowflake client licence, click-throughOperator driver layerDriver={SnowflakeDSIIDriver};Server=<account>.snowflakecomputing.com;Database=..;Schema=..;Warehouse=..;Uid=..;Pwd=..NOT NULL columns; a plain name folds to UPPER case, "Name" keeps its caseKey tuple of the last acknowledged row
Databricksodbc / databricksSimba Spark ODBC (Databricks-distributed)Proprietary, click-throughOperator driver layerDriver={Simba Spark ODBC Driver};Host=..;Port=443;HTTPPath=..;SSL=1;ThriftTransport=2;AuthMech=3;UID=token;PWD=<token>NOT NULL columns; names keep their case, `name` delimitsKey tuple of the last acknowledged row
BigQueryodbc / bigquerySimba Google BigQuery ODBC Connector (Google-distributed)Proprietary, click-throughOperator driver layerDriver={Simba Google BigQuery ODBC Connector};Catalog=<project>;OAuthMechanism=0;Email=<service account>;KeyFilePath=<key.json>NOT NULL columns; names keep their case, `name` delimitsKey tuple of the last acknowledged row
Redshift with Amazon's driverodbc / postgresAmazon Redshift ODBC driver, or psqlodbc over the PostgreSQL wireApache-2.0 (Amazon's), LGPL-2.1 (psqlodbc)Operator driver layer (Amazon's), shipped (psqlodbc)As PostgreSQL, with Amazon's driver name when usedAs PostgreSQLKey tuple of the last acknowledged row
MongoDB, Atlas, and the MongoDB wire (DocumentDB, Cosmos DB for MongoDB)mongodbThe official mongodb crate and bson (pure Rust)Apache-2.0 (driver), MIT (bson)Nothing to installmongodb://user:password@host:27017/?authSource=admin, or mongodb+srv://... for AtlasChange stream (the default): none, the server orders events, but the deployment must be a replica set; keyset: _idChange stream: the resume token of the last acknowledged event; keyset: the _id of the last acknowledged document
DynamoDB, Cosmos DB SQL API, Elasticsearch, OpenSearchnot a sources.db engine------A REST profile under sources.rest (see docs/reference/profile-grammar.md)----

The driver name in a connection string is whatever odbcinst -q -d lists on the host; the names above are the packages' defaults.

Authentication​

connection_string is ONE string, resolved once when the store first connects. So an auth form works here only if the whole credential fits in that string, or in files the container already carries. A form that needs a short-lived token minted per connection does not work, because nothing re-resolves the string between connects; those are listed as unsupported below rather than left to fail at connect time.

Two limits apply to every row. The string is a credential spec, so it may be vault:, bao:, openbao:, env: or file: and the plaintext never sits in config; an aws: prefix needs a secrets feature the fetcher does not build and is refused at load naming the prefix rather than reaching the driver as literal text. And a credential the driver reads from disk (a key file, a wallet, a certificate) has to be mounted into the image; the spec mechanism does not fetch it.

EngineExpressible in the connection stringNeeds a file mountedNot expressible here
PostgreSQLUser and password (Uid, Pwd). TLS client certificate through psqlodbc's pqopt, which forwards libpq's sslmode, sslcert, sslkey, sslrootcertThe certificate, key and root CAKerberos and GSSAPI: libpq has krbsrvname, gssencmode and gsslib, but psqlodbc documents no keyword for them and only demonstrates pqopt with the TLS set, so treat it as undocumented rather than available. Azure Database for PostgreSQL Entra auth -- see below
MariaDB, MySQLUser and password. TLS client certificate (SSLCERT, SSLKEY, SSLCA, SSLVERIFY=1); absolute paths are requiredThe certificate, key and CA--
ClickHouseUser and password in the URL (http[s]://user:password@host/db)----
SQL Server, Azure SQL, SynapseUser and password. Entra service principal (Authentication=ActiveDirectoryServicePrincipal, client id in UID, secret in PWD). Managed identity (Authentication=ActiveDirectoryMsi, with UID for a user-assigned identity). Kerberos (Trusted_Connection=yes)Nothing for the Entra forms. Kerberos needs an external ticket cache the driver does not createAn Entra ACCESS TOKEN. Microsoft documents no DSN or connection-string keyword for it: it is set through a connection attribute whose buffer must outlive the connection handle, so a config-driven caller cannot supply it. Client certificates are documented only for loopback connections on SQL Server on Linux, not as a general login
OracleUser and password. Nothing else: the ODBC keyword set has no auth parameter beyond theseWallet and TCPS, and Kerberos, are Oracle Net configuration in tnsnames.ora and sqlnet.ora found through TNS_ADMIN, so the credential is entirely out of band and the string carries none of it--
SnowflakeUser and password. Key-pair JWT (AUTHENTICATOR=SNOWFLAKE_JWT with PRIV_KEY_FILE, plus PRIV_KEY_FILE_PWD for an encrypted key) -- the driver signs the assertion itself. OAuth client credentials (AUTHENTICATOR=oauth_client_credentials with OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_TOKEN_REQUEST_URL) -- the driver performs the exchange. Workload identity (AUTHENTICATOR=workload_identity with WORKLOAD_IDENTITY_PROVIDER), which needs no key file at all. A programmatic access token, which the driver's parameter list takes as AUTHENTICATOR=programmatic_access_token with token while the feature's own guide describes putting it in the passwordThe private key for the key-pair formAn OAuth token minted elsewhere (AUTHENTICATOR=oauth with token) -- that is a caller-supplied token, not a static credential
DatabricksA personal access token (AuthMech=3, UID=token, the token in PWD). OAuth machine-to-machine (AuthMech=11, Auth_Flow=1, with Auth_Client_ID, Auth_Client_Secret and Auth_Scope=all-apis), where the driver performs the exchange--Token pass-through (Auth_Flow=0 with Auth_AccessToken), where the caller supplies an OAuth token
BigQueryA service-account key (OAuthMechanism=0 with Email and KeyFilePath, or KeyFile for the key inline). Application Default Credentials (OAuthMechanism=3), which falls back to the metadata server's default service account and so needs no key at all. Workload identity federation (OAuthMechanism=4). Service-account impersonation (SAI_Email, SAI_Lifetime, SAI_Scopes)The key file, unless KeyFile carries it inline or OAuthMechanism=3 is used--
RedshiftIAM with explicit keys (IAM=1 with AccessKeyID, SecretAccessKey, optional SessionToken), a shared profile (Profile), or the instance role (InstanceProfile=1). The driver calls STS and requests cluster credentials itselfProfile reads a shared credentials fileThe IdP token plugin, which the vendor states the calling application must generate
SQLiteNothing: the file is the database and the driver takes no credentialThe database file itself--
MongoDBSCRAM (authMechanism=SCRAM-SHA-256 or SCRAM-SHA-1, with authSource), which is the on-prem form and the one this engine is for. Workload identity (authMechanism=MONGODB-OIDC with authMechanismProperties=ENVIRONMENT:k8s, azure or gcp, the latter two with TOKEN_RESOURCE), where the driver mints the token from that environment itselfThe projected service-account token for the k8s form, which Kubernetes mounts on its ownMONGODB-AWS and GSSAPI: driver build features this binary does not enable, the first because it links the AWS SDK's credential stack. MONGODB-OIDC with no ENVIRONMENT, which needs a token callback the driver expects the application to supply. Each is refused at load naming the reason. MONGODB-X509 is URI-shaped and needs no auth feature, but the driver's own documentation demonstrates X.509 only through a programmatic credential, so it is unproven here rather than supported

Forms that need a token minted per connection​

These are unsupported, and the reason is the same in each case: the credential is a token valid for minutes to an hour, and connection_string resolves once. Supporting them means re-resolving the string per connect.

  • Azure Database for PostgreSQL with Entra. The user name is the Entra principal and the PASSWORD is an access token, obtained out of band and valid for a matter of minutes.
  • Google Cloud SQL with IAM database authentication. The password is a login token valid for about an hour; Google's own guidance is that long-lived or pooled processes use a connector rather than a pasted token, and the token can exceed a client's password field.
  • Caller-supplied OAuth tokens on Snowflake, Databricks and Redshift, listed per engine above. The driver accepts a token but will not obtain one, so something outside the fetcher has to mint and rotate it.

Everything else in the matrix either fits in the string or sits in a file, which is why the supported set is wider than user and password while still being a static configuration.

Dialect notes​

The keyset SQL a tail runs is built per dialect in crates/db/src/keyset.rs, and the unit tests there pin each of these:

  • The predicate. Engines with row-value comparison take (a, b) > (?, ?); SQL Server and Oracle do not, and take the expanded a > ? OR (a = ? AND b > ?) with the leading values bound once per disjunct. Values are always bound as ODBC ? parameters, never written into the SQL text.
  • The cap. LIMIT n everywhere except SQL Server, whose OFFSET 0 ROWS FETCH NEXT n ROWS ONLY is a sub-clause of ORDER BY (so the no-key form is TOP (n)), and Oracle's FETCH FIRST n ROWS ONLY, which needs 12c Release 1 or later.
  • The alias. The operator's query becomes a derived table, (query) AS dfe_tail; Oracle refuses AS before a table alias (ORA-00933) and gets (query) dfe_tail.
  • Delimiting. A key that is not a plain identifier (a space, a dash, a dot) is delimited in the dialect's form: [name] on SQL Server, `name` on MariaDB, MySQL, ClickHouse, BigQuery and Databricks, "name" elsewhere. A key the operator already delimited is written as given, and its bare name is what the row JSON is read under.
  • Case. A plain (undelimited) key is written as given and the engine folds it: PostgreSQL to lower case, Oracle and Snowflake to upper, the rest keep it. The row JSON carries the column under the folded name, which is where the tail reads its checkpoint value; write a case-sensitive column delimited.
  • Timestamps. Every engine reports timestamps as RFC 3339 UTC (the session is pinned to UTC on PostgreSQL and MariaDB) and the checkpoint binds that text back. ClickHouse's parameter parser takes the ISO form without the Z, so the store binds a DateTime key without it under session_timezone=UTC, whatever zone the column renders in.

What the tail commits​

A tail row carries its key as a mark; the driver folds the marks of a flush and commits the last one only after the transport has acknowledged the batch, under the cursor key {instance}.{connection}.{unit} in the cursor store. The next tick binds the committed values back: as the keyset predicate's parameters on a SQL engine, as start_after on a MongoDB change stream, as _id > value on a MongoDB keyset. A tick that fails commits nothing and re-fetches from the previous checkpoint, so a row is never lost and may be seen twice after an aborted tick. A change stream sees every insert, update, replace and delete as an event with the current document; a keyset tail, on any engine, sees rows whose key sorts after the checkpoint, which is inserts only unless the key is a change timestamp.

One tick reads pages of limit rows, each resuming past the last row of the page before, and ends on the first short page or at max_pages_per_tick: a store that is behind catches up over several ticks instead of holding one open, and tail_pages_full_total counts the pages that came back full.

A change stream that saw no event still commits. The tick ends with the stream's post-batch resume token, so an idle collection does not leave the committed token trailing the oplog's window; once the oplog no longer reaches that token the server answers code 286, which the tail reports naming the unit whose checkpoint has to be cleared to restart from now. The stream binds its token with start_after rather than resume_after, which is what lets it resume past the invalidate a dropped collection ends a stream with.

What is proven where​

The integration tests under crates/fetcher/tests/integration/ start their own database container per test and run the dump and the tail through the driver: PostgreSQL and MariaDB through their ODBC drivers, ClickHouse over HTTP, MongoDB as a standalone server for the dump and the keyset tail and as a one-member replica set for the change stream. Each tail proof inserts rows, ticks, stops, ticks again on a fresh driver over the same cursor store, and checks that every row landed once in key order and that the checkpoint after the first tick was the last acknowledged row's. SQL Server and Oracle have the same proof, gated on their operator-supplied driver being registered with the host's driver manager: absent the driver the test skips saying so, in CI too, because the driver is a click-through licence no shared runner carries. The dialect each needs is pinned by unit test regardless. The PostgreSQL and MariaDB ODBC proofs run by hand: CI and the release gate do not build the db-odbc feature.