Skip to content

Add dbt-factory bundle template and example to contrib - #163

Open
mwojtyczka wants to merge 17 commits into
databricks:mainfrom
mwojtyczka:dbt-factory-contrib
Open

Add dbt-factory bundle template and example to contrib#163
mwojtyczka wants to merge 17 commits into
databricks:mainfrom
mwojtyczka:dbt-factory-contrib

Conversation

@mwojtyczka

@mwojtyczka mwojtyczka commented Jul 2, 2026

Copy link
Copy Markdown

What this adds

By default, running dbt on Databricks executes your whole dbt project as a single, opaque Workflow task — one green or red box. You can't see which model failed, you can't rerun just the failed models, and independent models don't run in parallel.

This PR provides a template that turns a dbt project into a Databricks Workflow with one task per dbt object
(model, seed, snapshot, test), with dependencies wired to match your dbt DAG. That gives you:

  • Faster execution — independent models run in parallel, and the notebook task type keeps
    dbt's dependencies pre-cached in the serverless environment, avoiding a cold start on every task.
  • Visibility & simplified troubleshooting — pinpoint failures at the model level in the UI.
  • Enhanced logging & notifications — per-task logs and precise, model-level error alerts.
  • Improved retriability — retry only the failed model tasks without rerunning the whole project.
  • Seamless testing — dbt data tests run as their own tasks right after each model finishes.

It ships two things:

  • contrib/dbt_factory/ — a complete, deployable example you can clone and run.
  • contrib/templates/dbt-factory/ — a databricks bundle init template that scaffolds a new dbt project already wired up this way (bring your own models, or migrate an existing project).

How it works

flowchart TD
    subgraph setup["One-time setup"]
      T["dbt-factory bundle template"] -->|databricks bundle init| B["Scaffolded project:<br/>dbt project + PyDABs hook + factory code"]
      X["Existing dbt project<br/>(optional)"] -.->|move models/seeds/... into src/| B
    end
    subgraph deploy["Every deploy"]
      C["make manifest<br/>(dbt parse)"] --> D["target/manifest.json"]
      D --> E["databricks bundle deploy"]
      E --> F["PyDABs load_resources reads the<br/>manifest and generates the job"]
    end
    subgraph runtime["At run time — serverless"]
      G["Databricks Workflow:<br/>one task per model / seed / snapshot / test"] --> H["Each task triggers dbt<br/>via the runner notebook"]
      H --> I[("SQL warehouse")]
    end
    B --> C
    F --> G

    classDef optional stroke:#999,stroke-dasharray:5 4,color:#888;
    class X optional;
Loading

The job is not written as YAML. It's generated at databricks bundle deploy time from the dbt manifest via the PyDABs
python.resources hook: resources/__init__.py's load_resources reads target/manifest.json and builds one Databricks task per dbt node, wiring up dependencies. No per-model YAML is checked in — the task graph tracks the dbt DAG automatically.

The generation logic comes from the databricks-dbt-factory library, whose source is included under src/databricks_dbt_factory/ (adapted from commit 29866ed, v0.3.5, MIT — see NOTICE for attribution). The only integration code is the small resources/__init__.py.

Design choices

  • Serverless compute and the notebook task type (dbt runs via a small runner notebook using dbtRunner) for the fastest task start times.
  • A minimal set of exposed options: bundle_tests, environment_key, extra_dbt_command_options. Target / warehouse / catalog / schema live in dbt_profiles/profiles.yml.
  • The example commits a target/manifest.json so it deploys out of the box; make manifest regenerates it. The README covers migrating an existing dbt project (init, then move your dbt files into the generated project).
  • Ships both an example and a template
  • Use notebook task (not dbt task):
    • Performance improvements
      • Utilisation of base environments > faster task init, especially on multi layer dag's
      • pre-built partial_parse.msgpack and injecting it via dbtRunner(manifest=) > skips many file reads on shared WS, especially impactful on large production dbt setups with 1000's of models & tests
      • local write targets
    • more flexibility for customers to add functionality of their own

Testing

  • Deployed and ran end-to-end on a serverless SQL warehouse: databricks bundle deploy + databricks bundle run completes SUCCESS with all model and test tasks passing.
  • Offline test suite (make test) passes — including an integration test that exercises load_resources against the committed manifest with no workspace.
  • databricks bundle init on the template renders cleanly; the generated project's resources/__init__.py compiles and its bundle validates.
  • Passes ruff format --check (the repo's fmt CI check).

Benchmarks

  • 17min single dbt task
  • 29min dbt task type with dbt factory task level split
  • 19min notebook task type with dbt factory task level split

Note for reviewers

The databricks_dbt_factory/ core (and its tests) appear twice: once in the example (contrib/dbt_factory/) and once in the template (contrib/templates/dbt-factory/template/{{.project_name}}/). This is intentional, not an
oversight:

  • Each artifact must be self-contained. The example is meant to be cloned and run as-is, and databricks bundle init can only stamp out files that live under the template's template/ directory — a template file can't reference code outside it. So both need their own copy.
  • Consistent with repo precedent. This mirrors the existing contrib/templates/data-engineering + contrib/data_engineering pairing, which likewise duplicates its shared files (e.g. scripts/, conftest.py) between template and example.

The code is owned by this repo (bundle-examples) — the NOTICE files credit the original databricks-dbt-factory source for attribution and it passes ruff format --check.

Vendored-code fidelity. The six core files (dbt_factory.py, dbt_task.py, task_factory.py, utils.py, __version__.py, notebook/run_dbt_command.py) are byte-for-byte identical between the example (contrib/dbt_factory/) and the template (contrib/templates/dbt-factory/template/{{.project_name}}/) — verified with diff. Relative to the upstream databricks-dbt-factory v0.3.5 (commit 29866ed) they are AST-identical (semantically the same); the only textual differences from upstream are cosmetic — quote style normalized to double quotes and this repo's formatting. Because the template's ruff config lives in pyproject.toml.tmpl (which ruff can't discover), a small .ruff.toml at the template root (a sibling of databricks_template_schema.json, outside template/ so bundle init never renders it) sets line-length = 120 so the root ruff format --check validates the template sources at the same width as the example.

Slight downside: a future update to the core means editing it in both places (re-synced from the pinned upstream commit). Low-cost in practice — it's stable code touched only on version bumps, not something maintained in-repo day to day.

Live testing

Example project from the template:
Screenshot 2026-08-14 at 13 35 10

More complex example:
Screenshot 2026-08-14 at 13 44 40

mwojtyczka and others added 5 commits July 2, 2026 20:06
Runs a dbt project on Databricks as a Workflow with one task per dbt object
(model/seed/snapshot/test) on serverless. The job is generated at deploy time
from the dbt manifest via the PyDABs `load_resources` hook, which calls the
bundled databricks-dbt-factory core -- no per-model YAML is checked in.

The core under src/databricks_dbt_factory/ is adapted from
mwojtyczka/databricks-dbt-factory@e767a9d (v0.2.1, MIT), reformatted to this
repo's style; see NOTICE for attribution. The only integration code is
resources/__init__.py. A committed target/manifest.json lets the bundle deploy
out of the box; `make manifest` regenerates it. The README lists the benefits,
includes an end-to-end Mermaid diagram (greenfield + existing-project paths),
and covers migrating an existing dbt project.

Verified end-to-end on a serverless SQL warehouse: bundle deploy + run
completes SUCCESS with all model and test tasks passing. Passes
`ruff format --check`.

Co-authored-by: Isaac
`databricks bundle init` template that scaffolds a dbt project wired to the
PyDABs load_resources hook, derived from the contrib/dbt_factory example.
Prompts expose the project name, catalog/schema, warehouse HTTP path, and the
factory options (bundle_tests, environment_key, extra_dbt_command_options).
The extra-options value is rendered with printf %q so quoted dbt args (e.g.
--vars) stay valid Python. The README lists the benefits and includes an
end-to-end Mermaid diagram of the template flow, including the optional
existing-project migration path. The manifest is generated post-init via
`make manifest`.

Co-authored-by: Isaac
Builds on the initial dbt-factory example + template: reduces the vendored
factory to the single supported path (serverless + notebook tasks), locks in
fast/reproducible defaults, and adds a real end-to-end test. Mirrored in both
the example (contrib/dbt_factory) and the template (contrib/templates/dbt-factory).

- Slim the vendored factory to one path: drop the CLI, native dbt-task
  rendering, and the cluster/warehouse/catalog/schema/dbt-deps knobs plus
  now-dead code (~6.5k lines, incl. obsolete test fixtures). Why: a small,
  easy-to-use API with optimal defaults.
- Minimal API: a few constants in resources/__init__.py; warehouse/catalog/
  schema come from the dbt profile. Why: fewer knobs to learn.
- Faster, reproducible runtime: pin dbt-databricks to the version installed in
  the venv (single source of truth = pyproject), ship it as a pre-built
  serverless base_environment, and sync dbt's parse cache so each task skips
  parsing. Why: dev/prod parity + fast task startup, no per-task installs.
- Clearer job: readable one-task-per-object keys from dbt names, and per-task
  artifact isolation so parallel tasks don't collide.
- Add `make test-e2e`: a developer-run test that generates a project from the
  template, deploys + runs it on your workspace, verifies the output tables, and
  tears everything down. Uses a richer fixture dbt project (tests/e2e/) to
  exercise real dbt features. Why: confidence that factory changes don't break
  actual dbt execution.
- Guard generated output: unit tests reworked to the trimmed surface + a saved
  expected_tasks.json check (refresh with `make test-update-expected-tasks`).
- Fix runner host handling: strip the scheme from DBT_HOST so dbt's `host` is a
  bare hostname (the adapter re-adds https://, so a full URL produced a
  doubled-scheme https://https:// discovery probe). Verified by re-running e2e.
- Housekeeping: tidy the runner notebook, gitignore the e2e .user.yml, prefix
  test-only Make targets with `test-`.

Co-authored-by: Isaac
- Resolve task-key collisions instead of failing deploy: keys are assigned
  through a per-manifest map; only colliding nodes get dbt's test hash or the
  package name folded in, everything else keeps its clean Cosmos-style key
- Make repeated bundle validate side-effect free: the serverless env file is
  only written when its content changed, and a failed write raises a clear error
- Reject local/dev dbt-databricks builds at deploy time; PyPI cannot resolve
  them, which would otherwise fail every task at runtime
- Sync dbt_packages/ with the bundle so projects using dbt packages work at
  runtime without a per-task dbt deps
- Harden the e2e harness: SQL statements poll until terminal (the API caps its
  synchronous wait at 50s) and the teardown schema drop retries
- Fold the one-method SpecsHandler class into Utils.read_dbt_manifest
- Fix README drift: msgpack note direction, rewritten Tests section, document
  make test-e2e and make test-update-expected-tasks, dbt packages note
- Add unit tests for key collisions and the new guards; mirror all changes to
  the template

Co-authored-by: Isaac
Additional simplifications, clean-up & testing setup
@MaximHammer

Copy link
Copy Markdown

Quick Screen capture of usage https://drive.google.com/file/d/1VcEkn6Hc3JOUYyOGFdUGQA2mLSJx4sg8

Comment thread contrib/templates/dbt-factory/template/{{.project_name}}/pyproject.toml.tmpl Outdated
@MaximHammer

Copy link
Copy Markdown

Additional Context on why not native dbt task type but rather a notebook task type for optimal speed:

  • utilisation of base environments > faster task init, especially on multi layer dag's
  • pre-built partial_parse.msgpack and injecting it via dbtRunner(manifest=) > skips many file reads on shared WS, especially impactful on large production dbt setups with 1000's of models & tests
  • local write targets

2 not speed related reasons:

  • notebook task type also provides extreme flexibility for customers to add functionality of their own
  • we can easily support both task types just as the original dbt factory code - but do not see a clear value add for the extra complexity

Update the pinned PyDABs library from 0.295.0 to 1.8.0 in both the example
(contrib/dbt_factory) and the template. Verified the integration against the
1.x line (databricks-bundles 1.7.0, the newest reachable here): the unit suite
passes (36 tests), the PyDABs `load_resources` build phase still generates the
job correctly (5 tasks, base_environment env spec intact), and
`ruff format --check` is clean. 0.x -> 1.x is the only major jump; 1.7.0 -> 1.8.0
is a minor bump.

Co-authored-by: Isaac
… and template

Brings the vendored core (example + template) up to upstream v0.3.2, hand-ported:
- Readable, collision-safe task keys with a noun (_model) suffix.
- Full-FQN --select, fixing duplicate-name and subdirectory-model selection.
- dbt unit-test handling (emits a task per unit test).
- Notebook-runner host parsing and per-task target/log isolation.
- Notebook is the default task type; glue passes task_type=TaskType.NOTEBOOK.

Tests and the expected-tasks snapshot are updated to the v0.3.2 output; NOTICE and
the version pin now reference v0.3.2. Example suite: 36 passing.
park-peter and others added 5 commits August 12, 2026 10:06
Replace the vendored core in both the example and the bundle-init template with upstream v0.3.3 (commit edbbd7c), keeping each copy at its own line-length (example 120, template 88). All 14 vendored files are byte-identical to reformatted upstream.

v0.3.3 rewrites resource selection: every task now emits an exact compound selector (fqn:...,package:...,resource_type:...) and test tasks pin --indirect-selection empty/cautious. It also drops severity-based gating (warn tests now gate downstream too) and refuses tests that share a name and fqn. Update the repo's own tests and expected_tasks.json to match, and switch the EXTRA_DBT_COMMAND_OPTIONS example and template schema off --vars, which v0.3.3 now rejects as a parse-context option.
The template copy of the databricks-dbt-factory core was wrapped to a
narrower width than the example copy, so the two hand-maintained copies
diverged and the template failed its own ruff line-length=120 config.
Copy the example's core files over the template's so both trees are
byte-identical and stay AST-identical to upstream v0.3.3.

Co-authored-by: Isaac
Port databricks-dbt-factory v0.3.3 fixes into the dbt-factory example and template
The root ruff format check has no line-length set, so it defaults to 88
cols. The dbt-factory template's vendored core is a byte-for-byte copy of
the example (which is formatted at line-length=120 via its own
pyproject.toml), and the template's config lives in pyproject.toml.tmpl,
which ruff cannot discover. Add the template subtree to the format
exclude list, matching the existing pattern for every other template
("Templates are responsible for formatting at the source"). The example
copy stays under the check, so the shared core is still validated.

Co-authored-by: Isaac
Instead of excluding the template from the root ruff format check, add a
.ruff.toml (line-length=120) in the template dir, outside
{{.project_name}}/ so it is never stamped into generated projects. Ruff
can't discover the template's line-length from pyproject.toml.tmpl, so
without this it fell back to the root default of 88. This keeps ruff
linting the template sources (matching the example, which is checked via
its own pyproject.toml) rather than disabling the check.

Reformat the three template test files to line-length=120 so they pass;
the vendored core stays byte-for-byte identical to the example.

Co-authored-by: Isaac
@@ -0,0 +1,37 @@

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spurious newline.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread contrib/dbt_factory/src/databricks_dbt_factory/dbt_factory.py
Comment thread contrib/dbt_factory/src/databricks_dbt_factory/__version__.py
Comment thread contrib/dbt_factory/tests/e2e/README.md Outdated
```
DATABRICKS_CONFIG_PROFILE=<profile> \
DBX_E2E_HTTP_PATH=/sql/1.0/warehouses/<warehouse-id> \
DBX_E2E_CATALOG=<writable-catalog> \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these are specific to DBT factory, I recommend using DBT_FACTORY_zzz env vars.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread contrib/dbt_factory/databricks.yml
include:
- dbt_serverless_env.yaml
- target/partial_parse.msgpack
- dbt_packages/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triggers a warning if empty. You can fix with a .gitkeep in the template that is always generated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

# {{.project_name}}

The '{{.project_name}}' project was generated using the **dbt-factory** template for
Databricks Asset Bundles. It runs a [dbt](https://docs.getdbt.com/) project on Databricks as a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you do a search/replace in these trees for "Declarative Automation Bundles"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

- Rename vendored core modules to snake_case in both the example and the
  template trees (dbt_factory.py, dbt_task.py, task_factory.py, utils.py,
  __version__.py) and update all imports; the two copies stay byte-for-byte
  identical.
- Fix the `dbt_packages/**` sync.include empty-glob warning with a tracked
  .gitkeep, a .gitignore exception, and a Makefile `touch` after `dbt deps`
  (dbt deps empties the directory). Verified: `bundle validate` is warning-free.
- Rename the e2e env vars DBX_E2E_* -> DBT_FACTORY_* and document
  DBT_FACTORY_SCHEMA (offline `dbt parse` only) in the e2e README and profile.
- Remove a spurious leading blank line in dbt_profiles/profiles.yml.
- Use the "Declarative Automation Bundles" terminology consistently.

Co-authored-by: Isaac
…t it

`.ruff.toml` was under template/, which `databricks bundle init` renders into
the output directory — polluting generated projects and causing a "file
already exists" error when re-initializing into the same directory. Move it to
the template root (a sibling of databricks_template_schema.json), which is
never rendered, while the repo-root `ruff format --check` still discovers it
for the sources under template/.

Co-authored-by: Isaac
Update __version__ to 0.3.4 and the NOTICE commit reference to ab27714
(v0.3.4) in both the example and the template. The vendored core is
AST-identical to upstream v0.3.4 (whose 0.3.3->0.3.4 release also renamed
the modules to snake_case, matching the names already used here), so only
the version markers change.

Co-authored-by: Isaac
Pull the v0.3.5 release (commit 29866ed) into both the example and the
template. The only vendored change since v0.3.4 is in the runner notebook
(run_dbt_command.py): it now authenticates dbt via the Databricks SDK
WorkspaceClient instead of the dbutils notebook context. Also bump
__version__ to 0.3.5 and update the NOTICE commit reference.

The vendored core stays AST-identical to upstream v0.3.5 and byte-for-byte
identical between the two trees.

Co-authored-by: Isaac
The v0.3.5 runner notebook imports databricks-sdk. It is not added to the
serverless environment spec explicitly because it is a transitive dependency
of the pinned dbt-databricks, which already pulls it in. Note this in
_serverless_environment_spec so it is not mistaken for a missing dependency,
with a pointer for anyone building a dbt-databricks-free task environment.

Co-authored-by: Isaac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants