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
| Package | Status |
|---|---|
ontologiq-duckdb | On PyPI. Working, exercised end-to-end in CI. Powers ontologiq init — zero credentials, zero setup. |
ontologiq-postgres | Implemented; 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-mysql | Implemented, via PyMySQL; reaches PyPI with the next release. Driven against a live mysql:8 in CI through the whole loop. |
ontologiq-databricks | On 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
- Quoting is not your problem. The compiler emits every identifier
quoted, because objects get called
order. servenever writes. The serving runtime wraps your adapter in a read-only proxy whosecreate_viewraises. Do not rely on being able to write from a query path.- Print nothing to stdout. During
serve, stdout is the JSON-RPC channel. The runtime redirects it around adapter calls, but a driver that spawns threads can still escape that. - Types are not introspected yet. Ontologiq does not know a column’s
type at compile time, so identity values are compared with a conservative
heuristic. Schema introspection (
validate --with-db) is on the roadmap and is where an adapter would contribute adescribe_table.
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”.