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.
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.
| Surface | What is real |
|---|---|
| Identity | Personal access tokens, this process's own OIDC, and federated JWTs from entra-emulator. Real signed tokens, really verified. |
| Workspace & DBFS | Notebooks and workspace files with the SOURCE / RAW / AUTO formats the API defines, and a Repos surface that really clones with git. |
| Jobs 2.2 | notebook_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 warehouses | The statement API and HiveServer2 Thrift, executing on an attached engine — the dialect is Spark SQL, not Photon, and the docs say so. |
| Compute | Sail (Spark Connect) or JVM Spark, writing genuine Delta bytes that delta-rs reads back independently. |
| Secrets | Scopes that persist, {{secrets/scope/key}} resolved into a task's environment, and live read-through to azure-keyvault-emulator. |
| Unity Catalog | A 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. |
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:
| Kind | What 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.
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.
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.
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.
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.
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.
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.
| Client | What it proves |
|---|---|
databricks-sdk==0.129.0 | Workspace, Jobs, secrets and Unity Catalog through the official Python SDK. |
Databricks CLI v1.12.1 | The published binary, against the same surface. |
databricks/databricks | The real Terraform provider, planning and applying. |
databricks-connect==19.1 | Spark Connect against the attached engine. |
databricks-sql-connector==4.4.0 | HiveServer2 Thrift, the wire a BI tool speaks. |
dbt-databricks==1.12.4 | A 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.
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.
spark_jar_task asks this process to run a Java main class. It says so and stops.python_wheel_task, libraries, instance pools, init scripts — the emulator does not own a cluster, so it will not pretend to configure one.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.