v0.2.9 dbt_task reaches the warehouse the agent can actually see →

A Databricks workspace on your laptop, that refuses what it cannot compute.

Clean-room, local, single Go binary. It terminates the real Databricks REST API, attaches a real engine to do the work, and returns an honest error for everything else — because an emulator that fakes a result is worse than one that says no.

31parity claims, every one witnessed
31of those proved by an outside client
6unmodified clients driving it
26surfaces it openly does not implement

The problem this solves

A Databricks workspace is not something you can stand up to test against. So the code that talks to one gets tested against a mock you wrote, which agrees with you by construction, or against a shared dev workspace that is slow, costs money, and is never in a known state.

Both failure modes are the same failure: the thing you test against is not the thing you ship against. A mock returns what you told it to. A shared workspace returns what the last person left behind.

This runs the real API surface locally, and is deliberate about the line between what it really does and what it only appears to do. That line is published as a parity ledger rather than described in a README, and every row on the green side of it names the evidence.

What it actually does

SurfaceWhat is real
IdentityPersonal access tokens, this process's own OIDC, and federated JWTs from entra-emulator. Real signed tokens, really verified.
Workspace & DBFSNotebooks and workspace files with the SOURCE / RAW / AUTO formats the API defines, and a Repos surface that really clones with git.
Jobs 2.2notebook_task, spark_python_task, sql_task.file, condition_task, dbt_task, run_job_task and for_each_task, dispatched with real dependency ordering and real concurrency.
SQL warehousesThe statement API and HiveServer2 Thrift, executing on an attached engine — the dialect is Spark SQL, not Photon, and the docs say so.
ComputeSail (Spark Connect) or JVM Spark, writing genuine Delta bytes that delta-rs reads back independently.
SecretsScopes that persist, {{secrets/scope/key}} resolved into a task's environment, and live read-through to azure-keyvault-emulator.
Unity CatalogA proxy in front of the real Unity Catalog OSS server. Grants stay a 501, because that part is not implemented and pretending otherwise would be the lie.

How a claim gets to be green

Every green row in the parity ledger names its evidence in witnesses.json, and CI fails if a row claims something no witness backs. The kinds are not equal, and are not presented as though they were:

KindWhat it is worth
ci:An unmodified third-party client drives the emulator over a real network in CI. The strongest thing on offer: the client was not written by this project and does not know it is talking to an emulator.
go:This project's own Go tests. They prove routing and refusal. They cannot prove interoperability, because they are the emulator agreeing with itself.

All 31 claims carry a ci: witness. That was not true a few months ago, and closing the gap is what found most of the defects below.

Defects our own tests could not see

Each of these passed a green Go suite and broke a real client. They are the argument for the witness rule, and they are why the rule is enforced by CI rather than by intention.

A dbt task that could never reach its warehouse

The generated dbt profile carried the origin the client uses. Inside the compute container that address resolves to the container itself, so dbt dialled itself and hung. It passed the end-to-end gate because that gate handed the emulator one address that happened to work from both sides — the two names were the same string in the test and nowhere else.

A failing dbt test that came back as a network error

When dbt failed, the generated code raised SystemExit. The statement agent answers that by closing the connection without replying, so the caller got Post /statements: EOF and lost the run_results.json that had already been printed and would have said which test failed. The failure now travels as data, beside the evidence, where nothing between the agent and the caller can separate them.

Secrets that were fetched and then dropped

A task's spark_env_vars and spark_conf were sent to the agent in fields the agent does not read. The emulator really did resolve {{secrets/scope/key}}, and then put the value nowhere — and the run reported SUCCESS. The tests passed because they asserted the secret had been resolved, one level away from whether the task ever saw it.

Two concurrent tasks reading each other's parameters

Task parameters arrive by assigning sys.argv, which is one object per interpreter. Two tasks in the same wave overwrote each other and each read whichever landed last. Both reported SUCCESS. A loop writing one output per input silently produced fewer outputs than it had inputs, and the run was green.

The pattern. Not one of these was a crash. Every one of them was a green run that had quietly done the wrong thing, and in each case a test existed that was asserting something one step to the side of the property that mattered.

Who drives it in CI

Six unmodified clients, pinned to exact versions in the lockfile. A floating install is not a witness — it is whatever the registry shipped the morning CI ran.

ClientWhat it proves
databricks-sdk==0.129.0Workspace, Jobs, secrets and Unity Catalog through the official Python SDK.
Databricks CLI v1.12.1The published binary, against the same surface.
databricks/databricksThe real Terraform provider, planning and applying.
databricks-connect==19.1Spark Connect against the attached engine.
databricks-sql-connector==4.4.0HiveServer2 Thrift, the wire a BI tool speaks.
dbt-databricks==1.12.4A real dbt project, run both from the host and as a Jobs dbt_task.

The engine that writes is never the engine that confirms: models land as Delta on disk and delta-rs — which never spoke to dbt, the agent, or Spark — reads the log and the rows back.

What it refuses to do

Twenty-six surfaces are marked not-implemented in the ledger, on purpose and by name. A refusal that says what is missing is a feature; a plausible empty response is a bug you find in production.

Run it

docker run --rm -p 8443:8443 ghcr.io/calvinchengx/databricks-emulator:latest

# the admin PAT is written to the data directory on first start
export DATABRICKS_HOST=https://localhost:8443
export DATABRICKS_TOKEN="$(cat data/admin.pat)"
databricks workspace list /

It is one binary with no cgo. Jobs need an engine attached; with none, a run fails naming the missing engine and never reports SUCCESS. The quick start attaches one in a single compose file.

Local development only. Intentionally insecure: an unauthenticated admin surface, a well-known seeded token, and a self-signed certificate. Never expose it to a network you do not control.