CertSafari
    DBT Architect· Lessons

    Domain 7 · Lesson 37/39

    dbt model contracts and versions

    Utilizing model governance

    10 min read
    2.56% of exam
    4 sources
    Published 13 Oct 2026
    Docs as of 12 Oct 2026

    What you will be able to do

    • Enforce a model contract and know which materializations support it
    • Decide between a platform constraint and a data test
    • Identify the changes dbt treats as breaking and decide when a new model version is needed
    • Roll out a new model version without silently switching consumers to it

    1.Contracts guarantee a model's shape

    A model's columns usually come from whatever its select returns. That is convenient while you iterate, and risky once other people and processes query the model. A contract fixes the shape in advance. You declare the columns, their data types and, where the platform allows, their constraints. dbt then refuses to build the model if its SQL produces anything else. Contracts are recommended for public models that others rely on, whether inside dbt (other groups and projects) or outside it (dashboards and reports).

    An enforced contract with a not_null constraintyaml
    models:
      - name: dim_customers
        config:
          contract:
            enforced: true
        columns:
          - name: customer_id
            data_type: int
            constraints:
              - type: not_null
          - name: customer_name
            data_type: string

    A contract is all or nothing. With enforced: true, every column needs a name and a data_type the platform understands. A partial contract is not allowed. For wide models that means a lot of YAML, and the explicitness is deliberate. At build time an enforced contract changes two things. First, a preflight check confirms the query returns matching column names and types, and column order doesn't matter to it. Second, dbt puts the names, types and constraints into the DDL it submits and orders the columns as the contract lists them, not as the SQL selects them.

    Not every model can be contracted. SQL models materialized as table, view or incremental can, but each comes with a condition:

    - Views support column names and data types, but not constraints. - Incremental models need on_schema_change: append_new_columns or fail. - Unsupported: Python models, materialized views, ephemeral models, custom materializations (unless the author added support), and BigQuery models with recursive CTEs.

    Checkpoint 1 of 5· Check yourself

    Which of these models can enforce a contract that includes column names and data types?

    Sources1

    2.Constraints versus data tests

    Declaring a constraint in a contract doesn't mean the platform will enforce it. dbt sorts every constraint, per platform, into one of three categories, and the category decides whether a violation actually stops the build. In cloud warehouses, many constraints can be declared but are not enforced. They are stored as metadata only.

    Constraint categories and what a violation does
    CategoryEffect of a violating row
    Definable and enforcedThe model won't build
    Definable and not enforcedThe model still builds; the constraint is metadata only
    Not definable and not enforcedThe constraint can't be specified on that platform

    Data tests are the more flexible tool. They run after the build against content you can query. They support custom severity thresholds, and their failures are easier to debug because the built model is there to inspect. Replacing a test with a constraint moves the validation to build time and probably costs less compute. It only works when three things are true: the platform enforces that constraint (most enforce only not_null), the model is a table or incremental, and the model has a full contract.

    Tests are deliberately not part of the contract. Compare a software API: the structure of the response is the contract, while quality and uptime matter but sit outside it. Changing the structure in a backwards-incompatible way is a breaking change, and that is the point where versions come in.

    Checkpoint 2 of 5· Match them up

    Match each constraint category to what happens when a row violates it

    Tap a term, then the definition that fits it.

    Sources1

    3.Breaking changes and when to version

    With a contract enforced, dbt compares your project with its previous state and raises a contract error when it finds a change that could hurt downstream consumers. The changes it treats as breaking are:

    - Removing an existing column. - Changing the data_type of an existing column. - Removing or modifying a constraint on an existing column (dbt v1.6 or higher). - Deleting, renaming or disabling a contracted model (dbt v1.9 or higher). This is an error for versioned models and a warning for unversioned ones.

    A new version is for an intentional breaking change. Removing, renaming or retyping a column all qualify. Some changes pass the contract and still change what consumers see: different metric logic, a new grain, modified filters, a rewritten CASE statement. Whether those need a version is a judgment call for the model's owner. Small teams, and models that are still immature, can often just ship the break and have consumers adapt. That stops scaling once people and systems outside your control depend on the model.

    Model versions work differently from git. Version control deploys one state of the whole project into an environment, and rolling back means reverting a commit. Model versions live side by side in the same repository and are deployed into the same environment at the same time, the way two or three versions of a web API run together. Each one you keep adds warehouse cost and clutter. Prefer non-breaking changes, and bump the latest version on a predictable cadence, once or twice a year and announced well in advance, rather than for every small change.

    Checkpoint 3 of 5· Check yourself

    Under an enforced contract, which change does dbt flag as breaking when it compares against the previous state?

    Checkpoint 4 of 5· Exam question

    Which two of the following are core building blocks of dbt's model governance framework? (Select two.)(Select 2)

    Sources123

    4.Rolling out a new version safely

    The riskiest part of releasing a new version is the default. A plain ref('model_name') with no version resolves to whatever latest_version points to. If latest_version isn't set, it is the numerically greatest version. So the moment you add v: 2, every unpinned consumer moves to it, breaking change included. To keep the default stable, give the new version its own SQL file (for example fishtown_analytics_orders_v2.sql), list both versions, and set latest_version to the old one until you're ready to switch.

    latest_version set explicitly, so unpinned refs resolve to v2 and v3 is a prereleaseyaml
    models:
      - name: model_name
        latest_version: 2
        versions:
          - v: 3
          - v: 2
          - v: 1

    Tools outside dbt never use ref. They query the table by name. An alias, or a view over the latest version, keeps an unsuffixed fishtown_analytics_orders object in the warehouse that always points at the current version, so BI dashboards keep working. A deprecation_date, supported since dbt 1.6, puts a known end on the migration window. Consumers can test the new version with ref('upstream_project', 'fishtown_analytics_orders', v=2). If it breaks them, they can pin to v=1 for now, but they still have to migrate before the deprecation date.

    Checkpoint 5 of 5· Put it in order

    Put the producer's steps for introducing a new model version in order

    1. 1.Clean up the deprecated version
    2. 2.Communicate the new version to consumers
    3. 3.Create the new version without changing the default
    4. 4.Decide whether the change needs a new version
    5. 5.Set the new version as latest
    6. 6.Add a deprecation date to the old version

    Sources342

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.A contracted view enforces its not_null constraints the same way a table does.Why is that wrong?

      Views support contracts for column names and data types only. Constraints need a table or incremental materialization.

      Covered in Contracts guarantee a model's shape

    2. 2.Adding v2 to a model's versions list leaves unpinned refs on v1 until you say otherwise.Why is that wrong?

      Without an explicit latest_version, unpinned refs resolve to the highest numerical version, so consumers move to v2 immediately.

      Covered in Rolling out a new version safely

    3. 3.Data tests are part of a model's contract.Why is that wrong?

      The contract covers structure only: column names, types and constraints. Tests check content after the build.

      Covered in Constraints versus data tests

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “dbt will verify that your model's transformation will produce a dataset matching up with its contract, or it will fail to build.”
      ↩︎ Contracts guarantee a model's shape
      “When enforced, your contract must include every column's name and data_type”
      ↩︎ Contracts guarantee a model's shape
      “This check is agnostic to the order of columns specified in your model (SQL) or YAML spec.”
      ↩︎ Contracts guarantee a model's shape
      “We recommend defining contracts for "public" models that are being relied on downstream.”
      ↩︎ Contracts guarantee a model's shape
      “Most platforms only enforce not_null.”
      ↩︎ Constraints versus data tests
      “Materializing your model as table or incremental (not view or ephemeral).”
      ↩︎ Constraints versus data tests
      “versioned models will raise an error. unversioned models will raise a warning.”
      ↩︎ Breaking changes and when to version
      “views offer support for column names and data types, but not constraints”
      ↩︎ Exam trap 1
      “When the contract changes in a backwards-incompatible way, it is a breaking change that requires a bump in major version.”
      ↩︎ Exam trap 3
      “views offer support for column names and data types, but not constraints”
      ↩︎ Checkpoint
      “a model can still build even if building the model violates the constraint”
      ↩︎ Checkpoint
      “Changing the data_type of an existing column”
      ↩︎ Checkpoint
    2. 2.
      “Multiple versions of a model will live in the same code repository at the same time”
      ↩︎ Breaking changes and when to version
      “opt for a predictable cadence (once or twice a year, communicated well in advance)”
      ↩︎ Breaking changes and when to version
      “dbt 1.6 introduced first-class support for deprecating models by specifying a deprecation_date.”
      ↩︎ Rolling out a new version safely
      “you don't need a new version—such as adding a new column, or fixing a bug in an existing column's calculation”
      ↩︎ Prediction
    3. 3.
      “Changing logic behind a metric”
      ↩︎ Breaking changes and when to version
      “Each version of a model must have its own SQL file.”
      ↩︎ Rolling out a new version safely
      “Bear in mind that you will need to migrate at some point before the deprecation date.”
      ↩︎ Rolling out a new version safely
      “Without setting this, the default will be the highest numerical version, which could be a breaking change for consumers.”
      ↩︎ Exam trap 2
      “Step 5: Set the new latest version”
      ↩︎ Checkpoint
    4. 4.
      “Any unpinned references will resolve to model_name.v2.”
      ↩︎ Rolling out a new version safely

    Ready to test yourself?

    Practise the 9 questions on this subdomain.

    Spotted a mistake, or was something unclear? Tell us.