Une équipe modifie le calcul des frais de livraison. La requête s’exécute, les colonnes attendues sont présentes et aucun montant n’est nul. Pourtant, une commande exactement au seuil de gratuité devient payante. Un >= a été remplacé par un >.
Cette erreur fictive ne demande ni des millions de lignes ni une infrastructure complexe pour être détectée. Elle demande un exemple au bon endroit : juste sur la frontière entre les deux règles.
La CI des transformations SQL, ou intégration continue, consiste à exécuter des vérifications à chaque proposition de changement. Son intérêt dépend de ce que ces vérifications savent reconnaître. Un contrôle de syntaxe, un test d’unicité et un résultat métier attendu répondent à trois questions différentes.
Ce guide montre comment construire une première recette automatisée, puis la placer avant la fusion du code. Les références sont vérifiées au 27 octobre 2026. L’atelier utilise des données fictives et SQLite en mémoire ; il ne constitue pas un test d’un entrepôt cloud ni d’un service dbt hébergé.
Télécharger l’atelier de tests SQL : sept commandes fictives, résultats attendus, modèle correct, variante erronée, vérificateur Python et exemple de workflow.
Trois questions à poser avant de valider une transformation
La requête peut-elle s’exécuter ? Un nom de colonne invalide ou une fonction inconnue doit arrêter la livraison. Ce contrôle vérifie une condition nécessaire, mais ne connaît pas la définition de vos indicateurs.
Les données respectent-elles les contraintes attendues ? Une clé doit être unique, un statut appartenir à une liste, une référence client exister. Ces règles détectent des anomalies de structure ou de cohérence. Elles peuvent toutes passer alors qu’un tarif, une période ou une population est mal calculé.
La transformation applique-t-elle la bonne règle ? Pour quelques entrées choisies, on fixe les sorties attendues indépendamment du SQL testé. C’est ce troisième contrôle qui distingue un seuil inclus d’un seuil exclu.
Dans dbt, les unit tests utilisent des entrées statiques pour vérifier la logique SQL. Les data tests vérifient des assertions sur les données et résultats, par exemple l’unicité ou l’absence de valeurs nulles. Les deux mécanismes se complètent. Les unit tests sont disponibles depuis dbt v1.8 ; leur prise en charge dépend aussi du modèle et de l’adaptateur. Unit tests dbt, data tests dbt.
Une quatrième question reste organisationnelle : un échec empêche-t-il réellement la fusion ? Un test utile dont le résultat est ignoré produit un journal d’erreur, pas une règle de livraison.
Choisir les exemples qui protègent la décision métier
Commencez par la modification proposée. Pour un changement de seuil, prenez une valeur en dessous, une valeur exactement dessus et une valeur au-dessus. Pour un filtre, ajoutez une ligne qui doit entrer et une autre qui doit rester dehors. Pour une jointure, prévoyez une clé absente et une correspondance multiple lorsque ce cas est possible.
Le volume de la fixture, ce petit jeu d’entrée contrôlé, importe moins que les comportements qu’elle distingue. Cent lignes toutes identiques peuvent apporter moins d’information que trois cas soigneusement choisis.
Conservez les attentes dans un fichier lisible par le responsable de la règle. Si le programme calcule lui-même les résultats attendus en répétant le SQL, il peut reproduire exactement la même erreur. Une personne qui connaît le métier doit pouvoir expliquer chaque ligne de référence sans ouvrir le modèle.
Lorsque la règle change volontairement, les attentes changent aussi. La revue doit alors montrer ensemble la nouvelle définition, les exemples concernés et la justification. Il ne suffit pas de remplacer les anciennes sorties par les nouvelles pour que les tests redeviennent verts.
Un modèle sensible peut nécessiter plusieurs familles de cas : dates limites, valeurs manquantes, devises, arrondis, annulations ou événements tardifs. Ne les ajoutez pas mécaniquement. Reliez chaque cas à une décision ou à un incident que vous voulez éviter.
Cas concret : la livraison offerte à partir de 50 euros
Notre commerçant fictif adopte cette règle simplifiée : seules les commandes payées sont retenues. Pour la France, la livraison coûte 490 centimes sous 5 000 centimes de panier et devient gratuite à partir de ce seuil inclus. Pour les autres pays de l’exercice, elle coûte 990 centimes.
Les montants sont des entiers en centimes. Ce choix évite de mélanger l’exercice sur les seuils avec les approximations des nombres flottants. Il ne traite ni taxes, ni remboursements, ni devises multiples.
| Commande fictive |
Panier |
Situation |
Frais attendus |
| C01 |
4 999 centimes |
Payée, France |
490 centimes |
| C02 |
5 000 centimes |
Payée, France |
0 centime |
| C03 |
5 001 centimes |
Payée, France |
0 centime |
| C04 |
5 000 centimes |
Annulée, France |
Ligne exclue |
| C05 |
100 centimes |
Payée, France |
490 centimes |
| C06 |
7 000 centimes |
Payée, Belgique |
990 centimes |
| C07 |
6 000 centimes |
En attente, France |
Ligne exclue |
Le résultat attendu contient cinq lignes et 1 970 centimes de frais. Ces chiffres décrivent seulement notre jeu fictif. Ils ne sont ni une mesure de performance ni un résultat client.
La comparaison doit porter sur les commandes et leurs montants. Le total seul ne suffit pas : si le filtre sur le statut disparaît, C04 et C07 entrent dans le résultat avec des frais nuls. Le nombre de lignes devient faux, mais le total reste identique.
Atelier : faire échouer une vraie erreur de logique
1. Ouvrir le projet local
Décompressez l’archive dans un dossier atelier-sql, puis ouvrez un terminal dans ce dossier. Il faut Python 3.10 ou ultérieur avec le module sqlite3, fourni par de nombreuses distributions Python. Aucun paquet à installer avec pip, compte cloud ou abonnement dbt n’est nécessaire. Vérifiez la présence du module :
python3 -c "import sqlite3; print(sqlite3.sqlite_version)"
Le programme crée une base en mémoire, charge commandes.csv, puis exécute le modèle. La documentation Python décrit ce module et ses prérequis. Si votre distribution ne l’inclut pas, utilisez une installation Python qui le fournit.
2. Lire la règle et son attendu
Ouvrez modele.sql et attendu.csv. Le premier contient la transformation ; le second contient les cinq sorties définies plus haut. Le cœur du modèle est volontairement court :
SELECT order_id,
CASE
WHEN country = 'FR' AND total_cents >= 5000 THEN 0
WHEN country = 'FR' THEN 490
ELSE 990
END AS shipping_cents
FROM commandes
WHERE status = 'paid';
La table d’entrée impose un identifiant unique, un montant renseigné et non négatif. La liste des statuts et pays reste limitée au scénario fourni ; l’exercice n’est pas un contrat de données complet pour une boutique réelle.
3. Exécuter la version correcte
python3 run_checks.py
Le résultat doit indiquer passed: true, cinq lignes et un total de 1 970 centimes. Le code de sortie est zéro. Le vérificateur compare chaque couple identifiant-montant et compte les occurrences : une ligne supplémentaire identique reste une différence.
Ce dernier point est utile lorsqu’on construit son propre test en SQL. Dans SQLite, EXCEPT retire les doublons. Une comparaison par différence d’ensembles, même dans les deux sens, ne contrôle donc pas à elle seule les multiplicités. Notre vérificateur utilise des compteurs de lignes. Comportement de SELECT et EXCEPT dans SQLite.
4. Exécuter la variante erronée
python3 run_checks.py --model modele-bug.sql
Cette commande doit échouer avec le code de sortie 1. Le diagnostic signale que la ligne C02 à zéro centime manque et qu’une ligne C02 à 490 centimes apparaît à sa place. La requête a pourtant été exécutée sans erreur SQL.
C’est un contrôle volontairement négatif. Si cette version passe, le dispositif ne démontre plus qu’il protège le seuil. Ne masquez pas son code de sortie avec une commande qui transforme tout échec en succès.
5. Vérifier le dispositif de recette
python3 test_harness.py
Les huit contrôles doivent réussir. Ils vérifient notamment la détection du mauvais seuil, du filtre supprimé malgré un total inchangé, d’un doublon et d’une erreur SQL. Ils confirment aussi que l’ordre des lignes n’a pas d’importance pour ce contrat et que les codes de sortie sont utilisables en CI.
Les huit contrôles ont été exécutés localement pour cet article. Ils valident ce petit scénario et son vérificateur. Ils ne prouvent pas que vos modèles existants sont couverts.
Passer du terminal à un contrôle avant fusion
Dans votre propre dépôt, placez les fichiers de l’atelier sous atelier-sql/. L’archive fournit ci-sql.yml.example, à copier dans .github/workflows/ci-sql.yml si vous utilisez GitHub Actions. Ce workflow lance la recette correcte et les contrôles du vérificateur sur les pull requests. Il ne contient aucune commande de déploiement.
L’événement pull_request permet notamment de relancer les contrôles lorsque du code est ajouté à la proposition. Le fichier d’exemple n’emploie pas de secret ni de connexion à une base distante. Pour votre projet réel, définissez précisément l’environnement de test et les droits dont il a besoin. Événements GitHub Actions.
Configurez ensuite la règle de branche pour rendre le contrôle sql-metier obligatoire avant fusion, selon les possibilités de votre offre GitHub. L’exécution automatique et le caractère obligatoire sont deux réglages distincts. Branches protégées.
Faites une PR de démonstration avec le mauvais seuil, observez le refus, puis corrigez-la. Vérifiez le résultat sur la dernière révision proposée. Un succès obtenu avant une modification ultérieure ne valide pas cette modification. Le workflow fourni est un exemple à intégrer ; nous n’avons pas exécuté ce workflow dans votre dépôt.
Adapter la méthode à une CI dbt existante
Gardez les cas métier et transposez-les dans les fixtures de votre projet. Exécutez aussi une recette d’intégration dans le dialecte et l’environnement cible : SQLite ne reproduit pas les types, permissions, fonctions ou comportements de chaque entrepôt.
Pour limiter le périmètre, une CI dbt peut sélectionner les modèles modifiés et leurs descendants avec state:modified+. Cette sélection utilise un état antérieur du projet ; elle ne détecte pas les nouvelles lignes des sources. Conservez le manifeste de référence dans un emplacement séparé des nouveaux artefacts. Limites de la comparaison d’état.
Avec --defer, des références amont peuvent être résolues vers les objets de l’environnement de référence. Vérifiez donc les lectures réellement effectuées : une recette peut mélanger des données de test et de production. Ce mécanisme réduit les reconstructions ; il ne crée pas des résultats attendus et ne rend pas les données figées. Documentation defer.
Les modèles incrémentaux demandent également une recette de leur comportement lors d’une seconde exécution. Un chargement complet réussi ne couvre pas les corrections tardives ou les rejouements. Notre guide des modèles incrémentaux dbt propose un atelier distinct pour ces cas.
Décider ce qui bloque et mesurer l’effort
Commencez par un modèle dont une erreur change une décision : marge, facturation, stock disponible ou population éligible. Définissez la règle, quelques frontières et le propriétaire de l’attendu. Puis mesurez le temps de préparation, la durée des contrôles et l’effort nécessaire pour comprendre un échec.
Pour ce prototype local, aucun service payant n’est appelé. Dans votre CI réelle, le coût dépend des minutes d’exécution, du stockage éventuel et du calcul dans l’entrepôt. Ajoutez le temps de maintenance des fixtures. Nous ne proposons pas de tarif universel ni de gain chiffré : ils dépendent du projet et des offres utilisées.
Un écart sur une règle critique doit avoir un traitement explicite. Si une dérogation est nécessaire, documentez qui l’accorde, pourquoi et jusqu’à quand. Conservez la preuve de l’échec et la nouvelle référence validée. Une suite qui finit verte parce que les contrôles gênants ont été désactivés ne protège plus la décision initiale.
Avant de généraliser, vérifiez enfin les consommateurs concernés par le changement. Le guide d’analyse d’impact par le data lineage aide à les identifier. Les tests décrits ici vérifient ensuite les résultats là où une différence compte vraiment.
Pour votre premier modèle, la preuve attendue tient en trois éléments : une règle compréhensible, un cas qui échoue quand cette règle est cassée, et une fusion effectivement bloquée par cet échec.