Les matérialisations personnalisées échouent de manière prévisible. Les erreurs semblent cryptiques au premier abord, mais une fois qu’on les a rencontrées plusieurs fois, on peut diagnostiquer la plupart des problèmes à partir du seul message d’erreur. Cette note couvre les modes d’échec courants, leurs causes et une approche systématique pour tester de nouvelles matérialisations.
Erreurs courantes et leurs causes
”Relation does not exist”
Vous référencez une relation qui n’est pas dans le cache de métadonnées de dbt. Cela signifie généralement que vous avez essayé d’interroger ou de manipuler une relation directement au lieu d’utiliser load_cached_relation().
{# Incorrect — interroge directement la base de données #}{% set exists = adapter.get_relation(database, schema, identifier) %}
{# Correct — vérifie le cache #}{% set existing = load_cached_relation(this) %}L’autre cause fréquente : vous avez modifié une relation en dehors de la connaissance du cache (supprimée ou renommée lors d’une étape précédente), et une étape ultérieure s’attend à ce qu’elle existe toujours sous son nom d’origine. Suivez soigneusement l’état tout au long de la matérialisation. Si vous supprimez une relation, définissez la variable à none pour que la logique en aval sache qu’elle a disparu.
{% do adapter.drop_relation(existing_relation) %}{% set existing_relation = none %} {# Ne pas oublier ceci #}”Transaction rolled back” ou modifications non persistantes
Vous avez oublié adapter.commit(). Celui-ci est subtil car la matérialisation peut sembler réussir — dbt ne signale pas toujours le commit manquant comme une erreur. Mais la table n’apparaît jamais dans l’entrepôt, ou disparaît après la fin de la session.
Appelez toujours adapter.commit() avant l’instruction de retour. C’est l’étape 6 dans la structure en six étapes pour une raison.
{% do adapter.commit() %}{{ return({'relations': [target_relation]}) }}”Relation type mismatch”
La relation existante est une vue mais vous essayez de travailler avec elle comme une table (ou vice versa). Cela se produit quand quelqu’un change le type de matérialisation d’un modèle — par exemple, de view vers votre zero_downtime_table personnalisée — dans un projet qui tourne depuis des mois.
Gérez-le explicitement au début de chaque matérialisation :
{% if existing_relation is not none and existing_relation.type != 'table' %} {% do adapter.drop_relation(existing_relation) %} {% set existing_relation = none %}{% endif %}Ce pattern défensif traite l’incompatibilité de type comme une première exécution. L’ancienne vue est supprimée, et la matérialisation procède comme si la table n’existait pas encore.
Résultats de statement non disponibles
Vous avez appelé statement() sans fetch_result=True et avez ensuite essayé d’accéder au résultat avec load_result() :
{# Incorrect — pas de flag fetch_result #}{% call statement('validate') %} SELECT COUNT(*) FROM {{ temp_relation }}{% endcall %}{% set count = load_result('validate')['data'][0][0] %} {# Échoue #}
{# Correct — fetch_result=True explicite #}{% call statement('validate', fetch_result=True) %} SELECT COUNT(*) FROM {{ temp_relation }}{% endcall %}{% set count = load_result('validate')['data'][0][0] %} {# Fonctionne #}Le message d’erreur pour celui-ci est généralement une erreur NoneType ou une erreur de clé, qui ne pointe pas évidemment vers le flag manquant.
Droits ou permissions silencieusement absents
Si vous appliquez des grants avant run_hooks(post_hooks) ou avant que la relation soit dans son état final, les grants peuvent cibler la mauvaise relation ou être écrasés. Suivez l’ordre standard : SQL principal, post-hooks, puis grants et docs.
Vérifiez également que vous passez les bons arguments à apply_grants(). La signature attend la relation et un dictionnaire de configuration de grants provenant de config.get('grants').
Tester une nouvelle matérialisation
Étape 1 : Créer un modèle de test minimal
Commencez par le modèle le plus simple possible qui utilise votre matérialisation :
-- models/test_zero_downtime.sql{{ config( materialized='zero_downtime_table', min_row_count=1) }}
SELECT 1 AS id, 'test' AS valueÉtape 2 : L’exécuter deux fois
dbt run --select test_zero_downtimedbt run --select test_zero_downtimeLa première exécution teste le chemin “aucune relation existante”. La deuxième teste le chemin “relation existante trouvée” — la logique de swap, la validation par rapport à l’ancienne table, la sauvegarde et le nettoyage. Les deux chemins doivent fonctionner. De nombreuses matérialisations ne sont testées que sur le chemin de première exécution pendant le développement, puis échouent en production à la deuxième exécution.
Étape 3 : Vérifier le SQL compilé
Consultez target/compiled/ pour voir le SQL réel généré par votre matérialisation. C’est inestimable pour le débogage car vous voyez exactement quelle instruction dbt a essayé d’exécuter, avec tout le Jinja résolu.
# Trouver la sortie compilée pour votre modèle de testcat target/compiled/your_project/models/test_zero_downtime.sqlPour les matérialisations qui génèrent plusieurs instructions (le pattern zero-downtime inclut CREATE TABLE, COUNT et plusieurs RENAMEs), consultez également target/run/ — il contient le journal d’exécution complet avec le résultat de chaque instruction.
Étape 4 : Tester les chemins d’échec
C’est là que la plupart des personnes s’arrêtent trop tôt. Testez que votre matérialisation échoue correctement :
-- Test : la validation doit bloquer les tables vides{{ config( materialized='zero_downtime_table', min_row_count=100) }}
SELECT 1 AS id WHERE FALSE {# Retourne zéro ligne #}Exécutez ceci et vérifiez : le message d’erreur est clair, l’ancienne table (si elle existe) est préservée, et aucune table temporaire ou de sauvegarde n’est laissée derrière. Une matérialisation qui échoue gracieusement est plus précieuse qu’une qui réussit — le chemin de succès s’exécute une fois, mais le chemin d’échec est ce qui vous sauve à 2 h du matin.
Étape 5 : Vérifier les opérations post-build
Pour les matérialisations comme la table sécurisée, vérifiez que les politiques d’accès aux lignes et autres DDL ont bien été appliqués :
-- Vérifier les politiques d'accès aux lignes sur BigQuerySELECT *FROM `region-us`.INFORMATION_SCHEMA.OBJECT_PRIVILEGESWHERE object_name = 'your_model_name'Ne faites pas confiance au succès de la matérialisation sur la seule base du rapport de succès de dbt. Interrogez l’entrepôt directement pour confirmer l’état attendu.
Techniques de débogage
Utiliser les instructions log
La fonction {{ log() }} est votre débogueur printf à l’intérieur des matérialisations :
{{ log("existing_relation: " ~ existing_relation, info=true) }}{{ log("row_count: " ~ row_count, info=true) }}{{ log("config min_row_count: " ~ min_row_count, info=true) }}Le flag info=true affiche sur stdout pour que vous le voyiez dans la sortie dbt. Sans lui, les messages de log n’apparaissent que dans le fichier de log de débogage.
Utiliser exceptions.raise_compiler_error() stratégiquement
Pendant le développement, utilisez les erreurs de compilation comme points d’arrêt pour inspecter l’état à des points spécifiques de la matérialisation :
{{ exceptions.raise_compiler_error( "DEBUG: existing=" ~ existing_relation ~ " temp=" ~ temp_relation) }}Cela arrête l’exécution et affiche les variables que vous devez inspecter. Supprimez-les avant de déployer — ce sont l’équivalent des instructions debugger en JavaScript.
Vérifier le log de débogage dbt
Exécutez avec dbt --debug run --select test_model pour une sortie détaillée. Le log de débogage affiche chaque instruction SQL envoyée à l’entrepôt, la réponse et le timing. Pour les matérialisations qui émettent plusieurs instructions, c’est le seul moyen de voir quelle instruction spécifique a échoué.
Flux de développement des matérialisations
Un flux de travail pratique pour construire et itérer sur les matérialisations :
- Commencez par le code source de la matérialisation
tableintégrée de dbt comme référence. Lisez-le pour comprendre les patterns que dbt lui-même utilise. Vous le trouverez dans le code source de dbt-core ou du package d’adaptateur. - Écrivez la matérialisation avec le comportement le plus simple possible d’abord — juste le swap, pas de validation.
- Testez avec un modèle trivial (l’approche
SELECT 1ci-dessus). - Ajoutez des fonctionnalités une par une : validation, vérifications relatives, opérations post-build.
- Testez chaque ajout par rapport aux chemins de première exécution et d’exécutions suivantes.
- Testez explicitement les chemins d’échec.
- Appliquez-le ensuite à de vrais modèles, en commençant par les moins critiques.
La tentation est d’écrire la matérialisation complète en une seule fois et de la tester sur un modèle réel. Résistez-y. Le débogage des macros est déjà plus difficile que le débogage de modèles parce qu’on ne peut pas voir l’état intermédiaire. Les matérialisations ajoutent une couche d’indirection supplémentaire. Le développement incrémental et les tests approfondis sont la seule approche fiable.