dbt-hodor
Documentation | Interactive guide with a run simulator | Design spec
dbt-hodor is a free, self-hosted alternative to dbt Labs' dbt State. It runs inside dbt. For every selected node it decides whether to reuse the object that is already in the warehouse or to build it again. A node is reused when its logic has not changed and its upstream data has not changed since the last build.
It needs no dbt platform account and sends nothing to external services. State lives in a Delta table on Databricks, in a Postgres database such as Databricks Lakebase, or in a local DuckDB file.
Status: early development. hodor has end-to-end tests on dbt-duckdb and live tests on a Databricks SQL warehouse and on Databricks Lakebase; the test docs list what they cover. It has not run in a large production project yet. Before enabling reuse in production, run in shadow mode for at least a week of scheduled runs and review its decisions. See the rollout guidance. Changes between releases, including the breaking ones, are in Upgrading.
Why "hodor"
The name is a backronym: Hold Old Data, Or Rebuild. That is the only decision hodor makes, once per node.
hodor holds the door in front of your warehouse. A node gets through to a rebuild only when its code changed or new data arrived upstream. Everything else keeps the data it already has.
How it works
For each node in dbt run, dbt build, dbt test, dbt seed or dbt snapshot, hodor:
- Hashes the logic. For a seed this is the CSV file. Otherwise the compiled SQL is normalized with sqlglot (Databricks dialect), so case, quoting, comments and table aliases do not count as changes. Values that differ on every run, such as
invocation_id, are replaced first. The node config is hashed as well. - Checks the upstream data. Each upstream table has a data version. On Databricks it comes from the Delta commit history; maintenance commits such as
OPTIMIZEorVACUUMdo not count as data changes. On DuckDB it is a content fingerprint. - Decides. It compares both against the record of the node's last build and returns
reuseorbuild, with a reason code. - Records. It appends one record per node to the state table.
A run with no code changes skips sqlglot entirely: when the compiled SQL is byte-identical to the last build, hodor reuses the stored analysis. For 10,000 compiled models, hashing then takes about 0.1 seconds instead of about 4 minutes.
Safety: fail open
Every failure leads to a build, never to a reuse. An error while deciding, a store that cannot be read, a table whose data version is unknown, an input hodor cannot trace (for example read_files(...)): in each case the node builds as plain dbt would. hodor never fails a dbt run.
Reason codes
| Reason | Decision |
|---|---|
full_refresh |
build |
unsupported |
build |
no_history |
build |
relation_missing |
build |
code_changed |
build |
config_changed |
build |
target_modified |
build |
select_star_view |
build |
view_unchanged |
reuse |
unknown_upstream |
build |
upstream_code_changed |
build |
no_new_data |
reuse |
within_lag_tolerance |
reuse |
new_upstream_data |
build |
volatile_sql |
build |
hodor_error |
build |
The rules are checked in this order; the first match decides. dbt-hodor explain -s <model> shows the reason for one node, with the hashes and upstream data versions it compared. The full rules are in the design spec.
Modes
| Mode | Behavior |
|---|---|
off |
hodor does nothing. |
shadow (default) |
hodor decides and records, but every node builds. Use it to check the decisions before you trust them. |
on |
hodor reuses nodes. |
Installing the package never skips anything until someone sets on.
Configuration
Node configs
hodor reads the same state: block as dbt State, so a project can switch between the two. Set it in YAML:
# dbt_project.yml
models:
my_project:
+state:
lag_tolerance: 4h
require_fresh_data_from: any
| Key | Default | Values |
|---|---|---|
lag_tolerance |
45m |
0s, 45m, 4h, 1d, 1w, integer seconds, or {count: 4, period: hour} |
require_fresh_data_from |
any |
any: build when any upstream has new data. all: build only when all of them have. |
compare_unrendered_code |
false |
true: build only when both the rendered and the unrendered code changed |
execute_hooks_on_any_reuse |
false |
true: run pre- and post-hooks on a reused node. execute_hooks_on_reuse is also accepted. |
evaluate_volatile_sql |
auto | Auto: volatile functions such as current_date(), current_user() and current_timestamp() count as an upstream that changes, unless they only fill a stamp column. true: stamp columns count too. false: they never count (the dbt State default). |
pre_clone, allow_clones and defer_to_target are accepted and ignored, with a warning.
Settings
Set them on the profiles.yml target, or for one run with --vars '{hodor_mode: off}'. Precedence: environment variable, then --vars, then the profile key.
| Profile key | Env var | Default | Values |
|---|---|---|---|
hodor_mode |
HODOR_MODE |
shadow |
off, shadow, on |
hodor_store |
HODOR_STORE |
delta |
delta, duckdb, postgres (alias lakebase) |
hodor_database |
HODOR_DATABASE |
the target's catalog (or database) |
Databricks catalog; Postgres database (later). Not used by duckdb |
hodor_schema |
HODOR_SCHEMA |
none for delta (required); main for duckdb |
schema name |
hodor_table |
HODOR_TABLE |
hodor_state |
table name |
hodor_path |
HODOR_PATH |
.hodor/state.duckdb |
DuckDB database file, for duckdb |
hodor_metadata_threads |
HODOR_METADATA_THREADS |
8 |
1-64 |
hodor_lookback_days |
HODOR_LOOKBACK_DAYS |
90 |
1-3650 |
Name parts may contain letters, digits, _ and -. hodor always quotes them.
Where the state lives
| Store | Location | Created on first use |
|---|---|---|
delta |
<hodor_database>.<hodor_schema>.<hodor_table> |
the table only. The schema must exist, because creating one needs catalog-level grants |
duckdb |
table <hodor_schema>.<hodor_table> in the file hodor_path |
the file, the schema and the table |
postgres (alias lakebase) |
<hodor_schema>.<hodor_table> in the Postgres database hodor_database |
the schema and the table |
delta has no default schema on purpose: hodor writes nothing to a catalog until someone names the schema. Without it, hodor turns itself off with a warning.
# profiles.yml
my_project:
target: prod
outputs:
prod:
type: databricks
catalog: my_catalog
# ... connection settings ...
hodor_mode: shadow
hodor_schema: ops
# state table: my_catalog.ops.hodor_state
local:
type: duckdb
path: dev.duckdb
hodor_store: duckdb
# state table: main.hodor_state in .hodor/state.duckdb
The state table decides what gets skipped in prod. Grant MODIFY on it only to the job service principals.
Scope of v1
Supported: models materialized as table, incremental (append, merge, delete+insert, insert_overwrite, replace_where; an unset strategy counts as merge) and view, seeds, snapshots, and data tests (generic and singular), on dbt-databricks 1.12.7 and dbt-duckdb, with dbt-core 1.12.x.
Model versions: each version is its own node with its own table and records. Moving latest_version rebuilds the models that ref() it.
Slim CI: --defer and --favor-state are supported, and each push of a pull request reuses what the last push built. See Slim CI. The node configs pre_clone, allow_clones and defer_to_target are not.
Always built as plain dbt: Python models, microbatch, materialized views, streaming tables, metric views, custom materializations, custom incremental strategies and unit tests.
Known limit: a change to dbt's or the adapter's own macros, for example a dbt-databricks upgrade, does not show in any hash. Run once with HODOR_MODE=shadow after such an upgrade: shadow builds every node and records the builds. off records nothing, so the old records would still count. Project and package macros that a materialization or a hook runs are in the config hash.
Development
Requires uv. Local development and tests run on DuckDB, with no Databricks access.
uv sync
uv run pytest
uv run ruff check
uv run ruff format
The live Databricks test is opt-in: see docs/development.md.
dbt-hodor is available on PyPI:
pip install dbt-hodor
To build a wheel from the source checkout, run uv build.
Contributing
Issues and pull requests are welcome. CONTRIBUTING.md has the setup, the checks to run, and what a decision change needs. A wrong reuse, where hodor skipped a node it should have rebuilt, is a security-grade bug: report it privately through the Security tab.
License
Apache-2.0. NOTICE carries the attribution for the derived files and the trademark notice; LICENSE is the full licence text.
Metadata
Release files for dbt-hodor 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| dbt_hodor-0.2.0.tar.gz | 51.7 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dbt_hodor-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 108.8 kB
Release files / dbt_hodor-0.2.0.tar.gz
| Download URL | dbt_hodor-0.2.0.tar.gz |
|---|---|
| Size | 51.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
de7adcb167b3ab73c4e9bebc3717d145bc98aee14f78984c3113198f764ff6ca
|
|
BLAKE2b-256 checksum How to use checksums |
7bfa4015410b43c8a5a3f1a6d3ffc641ce7280707ae33a98374354be050b01b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / dbt_hodor-0.2.0-py3-none-any.whl
| Download URL | dbt_hodor-0.2.0-py3-none-any.whl |
|---|---|
| Size | 57.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2668ce8b56fbf91b90a52ae248f8d1a13c3ce0b70028d32652f26fba90d5800a
|
|
BLAKE2b-256 checksum How to use checksums |
320d1f2cbebb079feec6cbfd0c6c0b3f785822ed4be72fdaeab00c3636a95a8c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for dbt-hodor 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for dbt_hodor-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.