Un contrat sur un modèle est une fonctionnalité qui aide dans la gouvernance des données. Il permet de garantir la cohérence et la fiabilité de vos modèles dbt. Les personnes qui interrogent votre modèle en aval, au sein comme à l’extérieur de dbt, disposent d’un ensemble prévisible et cohérent de colonnes à utiliser dans leurs analyses.
Un contrat de modèle est une spécification formelle qui définit les garanties structurelles d’un modèle de données. Il s’agit d’une promesse explicite concernant :
- les colonnes exactes qui doivent exister
- leurs types de données
- les contraintes sur les colonnes
Il n’est pas possible de définir des contrats sur les modèles dont la matérialisation est ephemeral ou view. Ce n’est également pas supporté pour les modèles en python et il n’est pas possible d’en définir sur d’autres ressources comme les seeds, les sources, les snapshots, etc.
Les tests sont une autre fonctionnalité dans dbt, ils permettent de vérifier la conformité des données. Il est important de comprendre la différence entre les contrats sur les modèles de données et les tests.
Les contrats sont là, plus pour garantir la forme, la structure de la table et les schéma tandis que les tests permettent de valider le contenu, la qualité des données. Les contrats interviennent au moment du build du modèle alors que pour les tests c’est après :
- Lorsqu’un contrat est définie sur un modèle et qu’on souhaite le matérialiser dans notre plateforme de données, le modèle se matérialisera uniquement si le contrat est respecté. Dans le cas contraire, une erreur sera générée.
- Lorsqu’un test est définie sur un modèle, pour l’exécuter il faut au préalable que le modèle soit matérialisé dans la plateforme de données.
Concrètement, comment ça marche les contrats ?
Les contrats sont définis dans un fichier yml. On y liste toutes les colonnes que doit contenir le modèle ainsi que leur type. De plus, on peut y définir des contraintes, au niveau du modèle ou au niveau d’une colonne. Voici les différentes contraintes possibles :
- not null
- clé primaire
- clé étrangère
- unique
- check
- custom
Exemple donné dans la documentation :
models:
- name: <model_name>
# required
config:
contract: {enforced: true}
# model-level constraints
constraints:
- type: primary_key
columns: [first_column, second_column, ...]
warn_unsupported: True # show a warning if unsupported
- type: foreign_key # multi_column
columns: [first_column, second_column, ...]
to: ref('my_model_to') | source('source', 'source_table')
to_columns: [other_model_first_column, other_model_second_columns, ...]
- type: check
columns: [first_column, second_column, ...]
expression: "first_column != second_column"
name: human_friendly_name
- type: ...
columns:
- name: first_column
data_type: string
# column-level constraints
constraints:
- type: not_null
- type: unique
- type: foreign_key
to: ref('my_model_to') | source('source', 'source_table')
to_columns: [other_model_column]
warn_unenforced: False # skips warning if supported but not enforced
- type: ...
Appliquons un contrat pour un modèle matérialisé dans une base de données PostgreSQL.
Pour l’exemple, faisons un simple select sur une table de notre base de données. Créons un modèle ‘model1’ avec la définition suivante.
SELECT
id
, category
, nb
, nb2
, last_update
FROM {{ source('bdlearn_public', 'source1') }}
Ajoutons dans un fichier yml la configuration suivante pour notre modèle :
version: 2
models:
- name: model1
description: "Ce modèle permet d'illustrer l'utilisation des contrats"
config:
contract:
enforced: true
constraints:
# on vérifie que la colonne nb2 a des valeurs supérieures ou égales à celles de la colonne nb.
- type: check
expression: "nb>=nb2"
columns:
- name: id
description: "Il s'agit de la clé primaire de la table"
data_type: integer
constraints:
- type: not_null
- type: primary_key
- name: category
data_type: varchar(50)
- name: nb
data_type: integer
constraints:
# on vérifie que la colonne nb contient des valeurs posifives
- type: check
expression: "nb>=0"
- name: nb2
data_type: integer
- name: last_update
data_type: timestamp
Matérialisons notre modèle dans PostgreSQL avec la commande dbt run -s model1 et regardons le résultat dans postgres :

Voici un exemple d’erreur lorsque le contrat n’est pas respecté :
Non respect de la contrainte nb >= nb2 :

Nom de colonne qui diffère (dans le contrat on a remplacé la colonne “category” par “categorie” pour générer l’erreur) :

⚠️ Attention, actuellement, si vous définissez des contraintes au niveau du modèle, elles n’apparaîtront pas dans la documentation générée par dbt !
Dans notre exemple on avait définit une contrainte au niveau du modèle :

Celle-ci n’apparaît pas dans la documentation, mais elle s’inscrit bien dans PostgreSQL.
En revanche si on la place au niveau de la colonne du modèle elle sera visible dans la documentation. Par exemple, si on l’ajoute au niveau de la colonne nb (ou nb2) :
- name: nb
data_type: integer
constraints:
# on vérifie que la colonne nb contient des valeurs positives
- type: check
expression: "nb>=0"
- type: check
expression: "nb>=nb2"

La contrainte est maintenant visible dans la documentation.
Sinon vous pourriez aussi l’indiquer vous même dans la description du modèle, néanmoins cela crée de la redondance et ça pourrait conduire à des incohérences si la documentation n’est pas bien maintenue.
Conclusion
En conclusion, nous avons vu dans cet article comment définir un contrat sur un modèle dbt. Ils permettent de transformer vos hypothèses implicites en garanties explicites et ils protègent des surprises en production. Ils marquent une nouvelle étape dans la maturité de vos projets de transformation de données.
La documentation :
- https://docs.getdbt.com/reference/resource-configs/contract
- https://docs.getdbt.com/docs/mesh/govern/model-contracts
- https://docs.getdbt.com/reference/resource-properties/constraints
D’autres articles sur dbt :

