Model Versions

Concepts covered: Model contracts and versions

A contract stops country from turning into country_code by accident. Sometimes the change is the plan: the regions team wants country_code and a new region column, while 4 downstream models and a dashboard still read country. A versioned model lets both shapes build on the same run, each under its own contract, so the readers move when their owners choose.

2 files, 1 name

models:
  - name: dim_customers
    latest_version: 1
    config:
      contract:
        enforced: true
    columns:
      - name: customer_id
        data_type: integer
      - name: country
        data_type: varchar
      - name: signup_date
        data_type: date
    versions:
      - v: 1
        config:
          alias: dim_customers
      - v: 2
        columns:
          - include: all
            exclude:
              - country
          - name: country_code
            data_type: varchar
          - name: region
            data_type: varchar

The model is still dim_customers. Its SQL now lives in dim_customers_v1.sql and dim_customers_v2.sql, 1 file per entry under versions. dbt builds 1 relation per file. The default relation names are dim_customers_v1 and dim_customers_v2; the alias under version 1 overrides the first to dim_customers, for a reason the dashboard explains below. The top-level columns list is the shape every version starts from. Version 1 adds nothing, so it inherits all 3 columns. Version 2 says include: all then excludes country, and declares 2 new columns, so its contract has 4 columns: customer_id, signup_date, country_code, region.

select
    customer_id,
    signup_date,
    country as country_code,
    case
        when country in ('DE', 'FR', 'ES', 'IT') then 'EU'
        else 'ROW'
    end as region
from {{ source('raw', 'customers') }}

What ref() resolves to

Calllatest_version: 1latest_version: 2
`ref('dim_customers')``dim_customers` (version 1, via its alias)`dim_customers_v2`
`ref('dim_customers', v=1)``dim_customers``dim_customers`
`ref('dim_customers', v=2)``dim_customers_v2``dim_customers_v2`

latest_version decides where an unpinned ref('dim_customers') points. Leaving it at 1 while version 2 first appears means nothing downstream changes shape: version 2 builds, sits beside version 1, and a reader opts in with ref('dim_customers', v=2) when its owner is ready. When most readers have moved, flip latest_version to 2 and pin the stragglers with v=1, which compiles to the same relation they always read. A reader's shape changes only when its owner edits it to add a pin or remove one.

Readers outside dbt

A dashboard reads a relation by name and knows nothing about ref() or latest_version. Before versioning, that name was dim_customers. After it, with no alias, dbt builds dim_customers_v1 and dim_customers_v2. The dim_customers relation the dashboard reads stops updating, and neither side reports an error. The alias: dim_customers on version 1 keeps that name attached to the old shape. Moving the alias to version 2 later is the dashboard's cutover, a YAML edit made on the day its owner has changed the panels that read country.

Retiring version 1

versions:
  - v: 1
    deprecation_date: 2027-03-31
    config:
      alias: dim_customers
  - v: 2

deprecation_date on the v: 1 entry puts an end date on the old shape. Every parse then prints a warning for each model that still refs version 1, as an upcoming deprecation before the date and a passed one after it, so every run lists the stragglers. When the list is empty, delete dim_customers_v1.sql along with the v: 1 entry. The same change moves the alias to version 2. The next run builds version 2 into dim_customers, replacing the old shape under the name the dashboard reads, so that relation stays. The one left behind is dim_customers_v2, which version 2 built under its default name until now: drop it after that first run. Retiring version 1 without moving the alias leaves the reverse, a dim_customers frozen in the old shape, to drop once nothing reads it. Versions are for changes of shape. A wrong region mapping gets fixed in version 2 in place, because the shape its readers depend on does not change.

Do
  • Keep latest_version on the old shape until most readers have opted into the new one
  • Give the version a dashboard reads an alias so its relation name holds through the cutover
  • Set a deprecation_date so dbt lists the stragglers on every parse
  • Put a contract on every version so each shape is a checked promise
Avoid
  • Rename or retype a column in place when anything outside the model reads it
  • Flip latest_version and migrate the readers in the same change
  • Retire a version without dropping the relation name that no version builds any more

About this lesson

This section is part of the Models: Advanced lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.