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 truth | One macro governs contract logic. |
Flexible across environments | Adjust validation based on target.name . |
Clean model definitions | Avoid inline logic or duplicated models. |
Easier to update | Modify macro in one place instead of many models. |
Safe promotion to production | Stricter 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.