dbt: Versioned Schema Contracts


🧠 Concept : Versioned schema contracts pour modèles incremental

Quand tu fais du materialized='incremental', le modèle dbt :

  • ajoute uniquement les nouvelles lignes à une table existante
  • doit garder le même schéma entre les runs successifs
  • est vulnérable aux changements inattendus de colonnes (ex : ajout, suppression, renommage)

👉 Pour sécuriser ce processus, on applique une stratégie appelée “contrats de schéma versionnés” :
on verrouille le schéma et on contrôle explicitement ses évolutions, version après version.


✅ Étapes valides pour gérer des contrats de schéma versionnés

1. Définir un modèle incrémental avec contrat de schéma

-- models/staging/products/stg_products_v1.sql

{{ config(
    materialized='incremental',
    schema_contract=true,
    contract={"enforced": true}
) }}

SELECT
  id AS product_id,
  name,
  price
FROM {{ source('api', 'products') }}

✅ Ici :

  • schema_contract=true dit à dbt : “un contrat existe”
  • contract={"enforced": true} oblige dbt à appliquer exactement les colonnes du SELECT
  • Pas de colonnes définies ici : elles seront déclarées dans le fichier schema.yml (cf. étape suivante)

2. Déclarer les colonnes dans le fichier schema.yml

# models/staging/products/schema.yml
version: 2

models:
  - name: stg_products_v1
    description: "Modèle produit version 1"
    meta:
      schema_version: "v1"
    columns:
      - name: product_id
        data_type: string
        tests: [not_null]
      - name: name
        data_type: string
      - name: price
        data_type: float

✅ Ce fichier :

  • déclare les colonnes officiellement attendues
  • leur type (data_type) est utilisé pour validation si enforced: true
  • le champ meta.schema_version permet de versionner proprement le modèle

3. Migrer vers une nouvelle version du modèle

Imaginons que tu ajoutes une colonne category.

Tu ne modifies pas directement le modèle v1. Tu crées une nouvelle version :

-- models/staging/products/stg_products_v2.sql

{{ config(
    materialized='incremental',
    schema_contract=true,
    contract={"enforced": true}
) }}

SELECT
  id AS product_id,
  name,
  price,
  category
FROM {{ source('api', 'products') }}

Et dans schema.yml :

  - name: stg_products_v2
    description: "Modèle produit version 2 avec colonne category"
    meta:
      schema_version: "v2"
    columns:
      - name: product_id
        data_type: string
      - name: name
        data_type: string
      - name: price
        data_type: float
      - name: category
        data_type: string

🔁 Résultat : ce que tu obtiens

AvantageDétail
✅ Contrôle strictdbt refusera toute colonne non déclarée
✅ Séparation des versionsv1 et v2 coexistent, rien ne casse
✅ Audit clairTu sais qui a modifié quoi, à quelle version
✅ Migration maîtriséeTu peux tester v2 avant de remplacer v1
✅ Compatible CI/CDChaque version a ses propres tests

🧪 Bonus : test de cohérence entre versions

Tu peux créer un modèle temporaire pour comparer les deux versions :

-- models/audit/compare_v1_v2.sql

SELECT 'product_count' AS metric, COUNT(*) AS v1_count
FROM {{ ref('stg_products_v1') }}
UNION ALL
SELECT 'product_count', COUNT(*)
FROM {{ ref('stg_products_v2') }}

Ou ajouter des tests de non-régression personnalisés avec dbt assertions.


✅ En résumé

Élément dbtUtilisé ?Où ?
schema_contract=truedans config()
contract={"enforced": true}dans config()
Colonnes et typesdans schema.yml
Numéro de versionvia meta.schema_version ou dans le nom du modèle
Attributs fictifs (columns dans config)jamais utilisés ✅



🧪Bonus 2

⚙️ 4. Orchestration avec selectors.yml

Tu crées un fichier selectors.yml à la racine du projet :

selectors:
  - name: version_v1
    description: "Exécute tous les modèles de la version 1"
    definition:
      method: path
      value: models/staging/products/stg_products_v1.sql

  - name: version_v2
    description: "Exécute tous les modèles de la version 2"
    definition:
      method: path
      value: models/staging/products/stg_products_v2.sql

Ou mieux : via des tags

Ajoute un tag dans chaque modèle :

{{ config(
    materialized='incremental',
    schema_contract=true,
    contract={"enforced": true},
    tags=['products', 'v1']
) }}

Et le fichier selectors.yml devient :

selectors:
  - name: version_v1
    definition:
      method: tag
      value: v1

  - name: version_v2
    definition:
      method: tag
      value: v2

🚀 5. Exécution conditionnelle dans ton pipeline

# En prod stable :
dbt run --selector version_v1

# En test QA :
dbt run --selector version_v2

Tu peux donc :

  • exécuter uniquement la version stable (v1)
  • tester la version future (v2) sans impacter la prod
  • faire une bascule maîtrisée plus tard

✅ Résumé de l’approche complète

ÉtapeÉlément dbtDescription
✅ Schéma verrouilléschema_contract=true + enforced: true dans le modèle
✅ Colonnes définiesDans schema.yml, avec data_type
✅ Versions distinctesNommage (_v1, _v2) ou meta.schema_version
✅ Exécution cibléeAvec selectors.yml basé sur tag ou path

Leave a Reply

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