Adapters

The core knows nothing about DuckDB or Databricks. Adapters are separate packages, discovered through the ontologiq.adapters entry point group, so installing one is all it takes to use it.

pip install ontologiq-duckdb
# ontologiq.yml
adapter: duckdb
connection:
  path: ecommerce.duckdb

Available adapters

PackageStatus
ontologiq-duckdbOn PyPI. Working, exercised end-to-end in CI. Powers ontologiq init — zero credentials, zero setup.
ontologiq-postgresImplemented; reaches PyPI with the next release. The connection block goes to psycopg verbatim, so anything libpq understands works; credentials stay in PGPASSWORD / ~/.pgpass. Driven against a live postgres:16 in CI through the whole loop.
ontologiq-mysqlImplemented, via PyMySQL; reaches PyPI with the next release. Driven against a live mysql:8 in CI through the whole loop.
ontologiq-databricksOn PyPI. Prefers a Databricks CLI profile (connection.profile) — the SDK resolves host and OAuth from ~/.databrickscfg — or server_hostname + env-interpolated access_token. Verified end to end against a live workspace (serverless SQL warehouse, Unity Catalog) with the same smoke; CI cannot hold workspace credentials, so that run stays local. The only adapter supporting read_policy today: the row predicate compiles into the deployed view via Unity Catalog’s identity functions.

“The whole loop” is scripts/live_smoke.py in the repository: build, computed state on live rows, a proposal, the record moving underneath it, approval refusing because the precondition no longer holds, then a fresh proposal executing its webhook — against a real server, not an intercepted driver. If you run it against your own warehouse, an issue saying “it worked” is as valuable as one saying it did not.

One project uses one adapter. source.adapter is reserved in the format for per-object overrides, but a value differing from the project adapter is a validate error today rather than a half-working feature.

Connection settings

The connection block is adapter-specific and its values interpolate {{ env.VAR }} at load time, so credentials never belong in git:

connection:
  host: "{{ env.DATABRICKS_HOST }}"
  token: "{{ env.DATABRICKS_TOKEN }}"

A referenced variable that is not set is an error naming the variable, not a silent empty string.

The word is connection, not profile, on purpose: to a dbt user “profile” means an entry in ~/.dbt/profiles.yml, which this is not. Multi-target (dev/prod) configuration is reserved for a later version.

Writing an adapter

If your warehouse’s driver speaks DBAPI 2.0 — and nearly every one does — subclass the base and implement a single hook:

from pathlib import Path

import mydriver

from ontologiq.adapters import DBAPIAdapter
from ontologiq.adapters.dbapi import DBAPIConnection


class MyAdapter(DBAPIAdapter):
    name = "mywarehouse"
    sqlglot_dialect = "postgres"   # any dialect sqlglot knows

    def _connect(self, connection: dict[str, str], project_root: Path) -> DBAPIConnection:
        return mydriver.connect(autocommit=True, **connection)

The base does the rest: cursor handling, rows-as-dicts, and view DDL assembled with sqlglot so identifier quoting follows your dialect. Two contracts to honour: the connection must behave as if in autocommit (seeds are plain INSERTs and views are DDL; a driver-managed transaction would leave both invisible), and replace_views = False makes the base DROP + CREATE instead of CREATE OR REPLACE — set it when your engine lacks the statement or restricts it: Postgres, for instance, has it but refuses to drop, rename or reorder view columns, so the Postgres adapter uses DROP + CREATE or almost any ontology edit would brick the next run. This is exactly how the DuckDB, Postgres, MySQL and Databricks adapters are built — each is only its connection policy.

For a driver that does not speak DBAPI, implement the three-method protocol from ontologiq.adapters.base directly:

from pathlib import Path
from typing import Any


class MyAdapter:
    name = "mywarehouse"
    sqlglot_dialect = "postgres"

    def connect(self, connection: dict[str, str], project_root: Path) -> None:
        """Open a connection from the resolved `connection` block."""

    def execute(self, sql: str) -> list[dict[str, Any]]:
        """Run a query, return rows as dicts."""

    def create_view(self, name: str, sql: str) -> None:
        """Create or replace a view in the target catalog."""

Register it in your package’s pyproject.toml:

[project.entry-points."ontologiq.adapters"]
mywarehouse = "ontologiq_mywarehouse:MyAdapter"

That is the whole contract. sqlglot_dialect is what the compiler uses to render SQL, so the generated views come out in your dialect for free.

Things an adapter should know

If you write one, please open an issue — and if you run an adapter against your own warehouse, tell us how it went. The loop is proven live against DuckDB, Postgres, MySQL and a Databricks serverless warehouse; field reports from real schemas, real types and real permissions are what turn “the smoke passes” into “this is dependable”.