dbt: Environment-Specific Contract Validation in dbt Using Custom Macros

Here’s a polished and professional English explanation with a concrete example, perfect for your website or technical blog:


โœ… Environment-Specific Contract Validation in dbt Using Custom Macros

In dbt, contract enforcement ensures that the output of a model strictly matches a predefined schema. But in real-world projects, validation requirements can vary across environments (e.g. dev, staging, prod):

  • You might allow looser validation in dev (e.g., nullable fields).
  • And require strict contracts in prod (e.g., strict types, not nulls).

๐Ÿง  Problem

Without a flexible strategy, you may end up duplicating models or cluttering logic with environment-specific conditionals. This makes the project harder to maintain and increases the risk of inconsistencies.


๐Ÿ’ก Solution: Use Environment-Specific Contract Macros

By using a custom macro that adapts contract enforcement based on the environment (dev, prod, etc.), you can:

  • Write a single clean model,
  • Apply different validation rules depending on the environment,
  • Centralize and manage your contract logic in a reusable way.

๐Ÿ“ฆ Example

1. Model using a contract macro

In models/orders.sql:

{{ config(
    materialized = 'table',
    contract = true,
    columns = contract_columns()
) }}

select
  id,
  customer_id,
  total_amount,
  created_at
from {{ source('raw', 'orders') }}

2. Macro: contract_columns.sql

In macros/contract_columns.sql:

{% macro contract_columns() %}
  {% set env = target.name %}

  {% if env == 'prod' %}
    [
      {"name": "id", "data_type": "int", "description": "Order ID", "constraints": {"not_null": true}},
      {"name": "customer_id", "data_type": "int", "description": "Customer ID", "constraints": {"not_null": true}},
      {"name": "total_amount", "data_type": "numeric", "constraints": {"not_null": true}},
      {"name": "created_at", "data_type": "timestamp", "constraints": {"not_null": true}}
    ]
  {% else %}
    [
      {"name": "id", "data_type": "int"},
      {"name": "customer_id", "data_type": "int"},
      {"name": "total_amount", "data_type": "numeric"},
      {"name": "created_at", "data_type": "timestamp"}
    ]
  {% endif %}
{% endmacro %}

๐Ÿงผ Result

  • In prod, contracts are enforced strictly with not_null constraints.
  • In dev or staging, contracts are relaxed to facilitate exploration and testing.
  • The model definition stays clean and environment-agnostic.
  • Contract logic is centralized in a macro, easy to audit and update.

๐Ÿš€ Benefits

โœ… Benefit๐Ÿ’ฌ Description
Single source of truthOne macro governs contract logic.
Flexible across environmentsAdjust validation based on target.name.
Clean model definitionsAvoid inline logic or duplicated models.
Easier to updateModify macro in one place instead of many models.
Safe promotion to productionStricter checks automatically apply in prod.

This pattern is especially useful in teams with CI/CD pipelines, where staging environments may validate schema compliance differently than production, while ensuring all models remain reusable and easy to maintain.

Leave a Reply

Your email address will not be published. Required fields are marked *