catalog.schema.table (three-level). Cost paradigm: warehouse-seconds
(DBUs shown alongside). Read-only against data, enforced in depth.
Authentication: discover, don’t ask
The engine discovers a connection at runtime through the Databricks SDK’s unified auth chain and never prompts for or persists a token. Discovery order:databricks.profilein.dex/config.yml, naming a~/.databrickscfgentry (the storedatabricks auth loginwrites)DATABRICKS_*environment variables (DATABRICKS_HOSTplus a credential:DATABRICKS_TOKEN, OAuth M2M client variables, or the GitHub OIDC federation CI exchanges into a token)- the default
~/.databrickscfgprofile (a[DEFAULT]section, or the newer CLI’sdefault_profilepointer) - the
hostof atype: databrickstarget in a discovered dbtprofiles.yml
default_profile:oauth_user) is ever surfaced. Identities and token values
never cross the envelope. Every discovery failure names the fix.
Config
databricks.warehouse (a SQL warehouse ID or its
/sql/1.0/warehouses/... HTTP path); an unpinned or unresolvable warehouse
refuses with the fix named. Pin the smallest warehouse that works (a 2X-Small
serverless with the minimum auto-stop is the intended shape;
scripts/setup_databricks_ci.sh provisions exactly that).
transform init content-checks every schema the new project would build into
through Unity Catalog REST only, so the pinned warehouse is never woken: the
base dev_catalog.dev_schema, plus the sibling layer schemas (staging_dev,
intermediate_dev, marts_dev on the dev target) when --layered-schemas
is on. A schema that already holds tables or views is warned about, never
refused.
Cost model: two clients, one guarded quantity
Metadata is free and scans cost warehouse runtime, so dex guards warehouse-seconds, not bytes, and splits its clients to keep the free paths provably free:- Free (SDK REST, no SQL session):
connect test,explore inventory, all catalog/schema/table/column metadata, warehouse facts, estimation, andmaintain check(drift detection reads metadata only). The SQL session is opened lazily by the first billed statement, because opening one lands on the warehouse and can wake it; free commands leave a STOPPED warehouse stopped. - Billed (SQL session on the pinned warehouse): profiling aggregates,
explore query, relationship verification probes, DESCRIBE DETAIL size probes, andtransform build.
explore query and explore cluster bill an auto-profile of an object this connection has that the .dex/ cache cannot adjudicate, priced into the same handshake as the statements: see �3. Pass --no-auto-profile (or set auto_profile: false in .dex/config.yml) to be refused instead.
Budgets (budget.ceiling, --budget, budget.session_ceiling) are
warehouse-seconds: the number you budget is the number the server enforces.
Every cost surface also carries the translation to DBUs
(seconds x dbu_per_hour / 3600, rate from the warehouse size, marked
approximate: published rates move and differ by type and cloud), and to
dollars when databricks.dbu_price_usd is set. dex never guesses a dollar
price: it varies by cloud, tier, and contract.
Estimates start as an honest floor and refine in-budget. Databricks has
no dry-run and Unity Catalog exposes no free row or byte counts, so the first
handshake quotes a conservative per-statement floor, labeled
estimate_quality: "low", plus a startup floor when the pinned warehouse is
not running (seconds for serverless, minutes for classic). Once you confirm a
budget, the adapter runs an engine-built DESCRIBE DETAIL per table, charged
inside the confirmed budget, never before it, to learn sizeInBytes: that
sharpens later estimates, drives the sampling decision, and is noted per
table when unavailable (some shared tables refuse it). The same estimator
prices transform build: each compiled model, snapshot, and test is estimated
and summed into the build’s upfront cost, labeled estimate_quality: "low"
like any other Databricks estimate.
transform build --verify costs only where the warehouse keeps no row
count, so a table’s count is free metadata; how the counts are priced into
the build’s own estimate is in
�1.
transform test --mutate prices its whole batch as one number and confirms it
once, then runs one dbt invocation per mutant. Nothing is materialized: a mutant
builds as an ephemeral model, so the dev namespace holds exactly the relations it
held before. A budget that runs out partway stops the run, and the remaining
mutants are reported not_run rather than the budget being exceeded. Survivors
are not yet compared with the model on the dev data on this connector, so each
reports equivalence.status: not_checked with the reason, and nothing is
priced or run for it.
--verify also reads the dev schema (databricks.dev_catalog /
databricks.dev_schema) for the length of that one command, because dbt writes
the relations it is judging there and that schema is refused as a source
everywhere else. The schema need not exist yet: dbt creates it on a first
build, and dex lists it after dbt has run. How that read is bounded is under
Read scope in �3.
The budget is hard-enforced regardless of estimate quality. Before every
billed statement the session’s STATEMENT_TIMEOUT is set to the remaining
budget, so a weak floor cannot overrun the ceiling: Databricks kills the
statement and dex reports the over-ceiling refusal. Actual spend is
wall-clock seconds per statement (including any wake the statement caused),
recorded as billed_seconds in the ledger. Connections identify themselves with a dex user-agent for
attribution.
The estimate carries its DBU translation; the handshake itself is in
�0.
Read-only, enforced in depth
- Every data statement passes the SELECT-only guard parsed in the databricks
dialect through one execution door (DML, DDL,
MERGE,COPY INTO,OPTIMIZE,VACUUM, and multi-statement forms all refuse). - The adapter issues no mutating calls: Unity Catalog REST reads,
SET STATEMENT_TIMEOUT,DESCRIBE DETAIL, and guarded SELECTs only. - The documented grant shape is least-privilege:
CAN USEon the pinned warehouse,USE CATALOG+USE SCHEMA+SELECTon source catalogs, and write only on the dedicated dev catalog (scripts/setup_databricks_ci.shcreates this shape). - dbt dev builds write to
dev_catalog.dev_schema, which is refused as a source, and the generated profile pins the warehouse’s HTTP path and one thread. A user OAuth connection renders dbt’s own OAuth flow; a token connection renders aDATABRICKS_TOKENenv reference, never a value.
Profiling behavior
Profiling is batched aggregates (COUNT, APPROX_COUNT_DISTINCT, min/max only for engine-cleared safe columns), never row values. Nested and semi-structured columns (ARRAY, MAP, STRUCT, VARIANT, BINARY, GEOGRAPHY, GEOMETRY) degrade to non-null counts. Tables abovedatabricks.max_full_profile_bytes are profiled from a sample
(TABLESAMPLE), noted, with uniqueness not judged; the threshold binds once
DESCRIBE DETAIL has learned a size. Exact distinct-count escalation spends
only inside the confirmed budget and degrades to approximate verdicts with a
note when the remainder cannot cover it.
Testing
Offline: a stateful fake pair (tests/fakes/databricks.py): a Unity Catalog
workspace client serving metadata and a DBAPI connection that records
statement ordering, simulates timing on an injected clock, and enforces
STATEMENT_TIMEOUT with the connector’s real error types, behind a counting
factory that proves free paths never open a SQL session. Live: an env-gated
integration suite (DEX_TEST_DATABRICKS_*) reading the samples catalog and
writing only to the scratch catalog, run in CI on a schedule via an OIDC
federation policy (no stored keys), never as a merge or release gate.
Source: references/databricks.md (edit on main). This page is generated; change the source, not the site.