ServicesAboutNotesContact Get in touch →
EN FR
Note

dbt Model Versioning

How dbt model versions work — breaking vs non-breaking changes, the state:modified selector, version integers, deprecation dates, and the friction points.

Planted
dbtdata modelingdata quality

Once contracts are in place, schema changes require more thought. You can’t just rename a column and run dbt build anymore — the contract catches the mismatch and the build fails. dbt’s model versioning system provides the mechanism for evolving contracted schemas without breaking downstream consumers.

Breaking vs Non-Breaking Changes

dbt’s state:modified selector detects five categories of breaking changes when comparing against a previous project state:

  • Removing a column
  • Changing a column’s data type
  • Removing or modifying constraints
  • Removing contract enforcement entirely
  • Deleting or renaming a contracted model

Non-breaking changes pass cleanly without triggering errors. Adding a new column, fixing a calculation without changing types, or adding new constraints all work. You update the YAML, update the SQL, and the contract stays valid.

The distinction matters for CI/CD. Breaking changes require versioning. Non-breaking changes just need a contract YAML update alongside the SQL change.

How Versions Work

For breaking changes, dbt expects you to create a new model version. Versions use simple integers, and each version materializes as a separate table (mrt__analytics__customers_v1, mrt__analytics__customers_v2):

models:
- name: mrt__analytics__customers
latest_version: 2
config:
contract: {enforced: true}
columns:
- name: customer__id
data_type: int
- name: customer__name
data_type: string
- name: customer__email_address
data_type: string
versions:
- v: 1
deprecation_date: "2025-12-31"
columns:
- include: all
exclude: [customer__email_address]
- name: customer__email
data_type: string
- v: 2

Version 2 uses the top-level column definitions (with customer__email_address). Version 1 includes all columns except customer__email_address and adds the old customer__email column instead. Both versions materialize as separate tables, both with their respective contracts enforced.

Unpinned ref('mrt__analytics__customers') resolves to latest_version. Consumers that need the old schema can pin with ref('mrt__analytics__customers', v=1). Old versions get a deprecation_date, and dbt warns when anything references a deprecated version. This gives downstream consumers time to migrate.

The Migration Window

The versioning pattern creates a migration window:

  1. Define v2 with the new schema alongside v1
  2. Set deprecation_date on v1 to communicate the timeline
  3. Deploy both versions — they materialize as separate tables
  4. Notify consumers — dbt generates deprecation warnings in build output
  5. Remove v1 after the deprecation date passes and all consumers have migrated

In a dbt Mesh setup, this becomes especially valuable. When another team uses ref('your_project', 'mrt__analytics__customers'), versioning lets you evolve your schema without breaking their builds. The cross-project ref resolves to latest_version, but they can pin to a specific version if they need time to adapt.

The Friction Points

One real friction point: dbt’s breaking change detection is considered too aggressive by many users (GitHub issue #8028). It triggers errors even when you’ve intentionally updated the contract YAML to reflect a desired change, requiring model versioning even for simple precision corrections. You sometimes end up versioning for changes that feel more like fixes than breaking changes.

For example, changing numeric to numeric(38,2) to fix silent rounding is technically a type change. The state:modified selector flags it as breaking, requiring a new version — even though the change is corrective and consumers would benefit from it immediately. The intent behind the strictness is sound (any type change could break a consumer), but the lack of a “this is intentional, skip versioning” escape hatch creates overhead for routine maintenance.

The community has proposed several solutions: an --allow-breaking flag for CI, a breaking: false annotation in YAML, and more granular detection that distinguishes between widening changes (safe) and narrowing changes (unsafe). As of early 2026, none have been merged.

Schema Change Handling for Incremental Models

For incremental models with contracts, the on_schema_change configuration matters:

  • append_new_columns — Adds new columns to the existing table. Compatible with contracts because it only widens the schema.
  • fail — Fails the build if the schema has changed. The safest option when contracts are enforced.
  • sync_all_columns — Avoid this. It removes columns not present in the latest run, which creates exactly the kind of breaking change contracts are meant to prevent. An incremental run that drops a column will violate the contract on the next full refresh.
  • ignore — Silently ignores schema changes. Risky with contracts because it allows the actual table schema to drift from the contracted schema over time.

The recommended combination for contracted incremental models is on_schema_change: 'fail' during normal operation, with deliberate schema migrations handled through versioning when changes are needed.

When to Version

Not every schema change requires versioning. The decision framework:

Version when:

  • Removing a column that consumers depend on
  • Changing a column’s data type in a way that could break consumers (e.g., integer to string)
  • Renaming a column (it’s a removal + addition from the consumer’s perspective)
  • Changing semantics even if the type is the same (e.g., amount changes from cents to dollars)

Don’t version when:

  • Adding a new column (non-breaking by definition)
  • Adding or strengthening constraints (makes the contract stricter, doesn’t break consumers)
  • Fixing a calculation that doesn’t change the output type
  • Updating descriptions or metadata

The goal is to version only when the change could cause a downstream failure. If a consumer’s query would break, version. If it would just get an extra column they don’t use, don’t.