Add dbt-factory bundle template and example to contrib - #163
Open
mwojtyczka wants to merge 17 commits into
Open
Conversation
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
|
Quick Screen capture of usage https://drive.google.com/file/d/1VcEkn6Hc3JOUYyOGFdUGQA2mLSJx4sg8 |
pietern
reviewed
Jul 16, 2026
|
Additional Context on why not native dbt task type but rather a notebook task type for optimal speed:
2 not speed related reasons:
|
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.
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
pietern
reviewed
Aug 13, 2026
| @@ -0,0 +1,37 @@ | |||
|
|
|||
| ``` | ||
| DATABRICKS_CONFIG_PROFILE=<profile> \ | ||
| DBX_E2E_HTTP_PATH=/sql/1.0/warehouses/<warehouse-id> \ | ||
| DBX_E2E_CATALOG=<writable-catalog> \ |
Contributor
There was a problem hiding this comment.
If these are specific to DBT factory, I recommend using DBT_FACTORY_zzz env vars.
| include: | ||
| - dbt_serverless_env.yaml | ||
| - target/partial_parse.msgpack | ||
| - dbt_packages/** |
Contributor
There was a problem hiding this comment.
Triggers a warning if empty. You can fix with a .gitkeep in the template that is always generated.
| # {{.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 |
Contributor
There was a problem hiding this comment.
Can you do a search/replace in these trees for "Declarative Automation Bundles"
- 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
This was referenced Aug 13, 2026
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
pietern
approved these changes
Aug 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
dbt's dependencies pre-cached in the serverless environment, avoiding a cold start on every task.
It ships two things:
contrib/dbt_factory/— a complete, deployable example you can clone and run.contrib/templates/dbt-factory/— adatabricks bundle inittemplate 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;The job is not written as YAML. It's generated at
databricks bundle deploytime from the dbt manifest via the PyDABspython.resourceshook:resources/__init__.py'sload_resourcesreadstarget/manifest.jsonand 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 commit29866ed, v0.3.5, MIT — seeNOTICEfor attribution). The only integration code is the smallresources/__init__.py.Design choices
dbtRunner) for the fastest task start times.bundle_tests,environment_key,extra_dbt_command_options. Target / warehouse / catalog / schema live indbt_profiles/profiles.yml.target/manifest.jsonso it deploys out of the box;make manifestregenerates it. The README covers migrating an existing dbt project (init, then move your dbt files into the generated project).Testing
databricks bundle deploy+databricks bundle runcompletes SUCCESS with all model and test tasks passing.make test) passes — including an integration test that exercisesload_resourcesagainst the committed manifest with no workspace.databricks bundle initon the template renders cleanly; the generated project'sresources/__init__.pycompiles and its bundle validates.ruff format --check(the repo'sfmtCI check).Benchmarks
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 anoversight:
databricks bundle initcan only stamp out files that live under the template'stemplate/directory — a template file can't reference code outside it. So both need their own copy.contrib/templates/data-engineering+contrib/data_engineeringpairing, 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
NOTICEfiles credit the originaldatabricks-dbt-factorysource for attribution and it passesruff 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 withdiff. Relative to the upstreamdatabricks-dbt-factoryv0.3.5 (commit29866ed) 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 inpyproject.toml.tmpl(which ruff can't discover), a small.ruff.tomlat the template root (a sibling ofdatabricks_template_schema.json, outsidetemplate/sobundle initnever renders it) setsline-length = 120so the rootruff format --checkvalidates 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:

More complex example:
