dbt: Macro vs Valeurs en Dur : Pourquoi la Macro Gagne

🤔 La Question Légitime

Pourquoi faire une macro quand on peut écrire directement :

-- Approche "simple" - valeur en dur
select customer_id::varchar(50) as customer_id
from {{ ref('stg_orders') }}

Au lieu de :

-- Approche macro
select customer_id::{{ resolve_conflict('customer_id') }} as customer_id  
from {{ ref('stg_orders') }}

📊 Comparaison Pratique

❌ Approche Valeurs en Dur

-- models/customer_summary.sql
select customer_id::varchar(50) as customer_id from {{ ref('stg_orders') }}

-- models/customer_analysis.sql  
select customer_id::varchar(50) as customer_id from {{ ref('dim_customers') }}

-- models/customer_reports.sql
select customer_id::varchar(50) as customer_id from {{ ref('raw_data') }}

Problèmes :

  • 🔴 Duplication : varchar(50) répété 20+ fois
  • 🔴 Maintenance cauchemar : Changer le type = modifier 20+ fichiers
  • 🔴 Erreurs humaines : Oubli d’un fichier, types incohérents
  • 🔴 Pas de documentation : Pourquoi varchar(50) ?
  • 🔴 Pas de traçabilité : Qui a décidé ? Quand ? Pourquoi ?

✅ Approche Macro

-- models/customer_summary.sql
select customer_id::{{ resolve_conflict('customer_id') }} from {{ ref('stg_orders') }}

-- models/customer_analysis.sql
select customer_id::{{ resolve_conflict('customer_id') }} from {{ ref('dim_customers') }}

-- models/customer_reports.sql  
select customer_id::{{ resolve_conflict('customer_id') }} from {{ ref('raw_data') }}

Avantages :

  • DRY Principle : Règle définie une seule fois
  • Maintenance facile : Changer la macro = tout se met à jour
  • Cohérence garantie : Même règle partout
  • Documentation intégrée : Logs explicatifs
  • Évolutivité : Règles complexes possibles

🎯 Cas Concrets : Quand Ça Devient Critique

Scénario 1 : Changement de Règle Business

Nouvelle exigence : “Les customer_id doivent passer en bigint pour supporter l’international”

Avec valeurs en dur :

# Rechercher dans 50+ fichiers
grep -r "varchar(50)" models/
# Modifier manuellement chaque occurrence
# Risque d'en oublier une
# Tests manuels partout

Avec macro :

-- Une seule ligne à changer
{% macro resolve_conflict(column_name) %}
  {% if column_name == 'customer_id' %}
    {{ return('bigint') }}  -- ← Seul changement nécessaire
  {% endif %}
{% endmacro %}

Scénario 2 : Règles Contextuelles

Exigence : “customer_id en varchar pour les modèles externes, bigint pour les modèles internes”

Avec valeurs en dur :

-- Impossible sans duplication massive et erreurs
-- Comment savoir quel modèle utilise quel type ?

Avec macro :

{% macro resolve_conflict(column_name, context='internal') %}
  {% if column_name == 'customer_id' %}
    {% if context == 'external' %}
      {{ return('varchar(50)') }}
    {% else %}
      {{ return('bigint') }}
    {% endif %}
  {% endif %}
{% endmacro %}

-- Usage
select customer_id::{{ resolve_conflict('customer_id', 'external') }}

Scénario 3 : Audit et Traçabilité

Besoin : “Qui a décidé que customer_id soit en varchar ? Quand ? Pourquoi ?”

Avec valeurs en dur :

-- Aucune trace, aucune justification
select customer_id::varchar(50) as customer_id

Avec macro :

{% macro resolve_conflict(column_name) %}
  {% if column_name == 'customer_id' %}
    {{ log("📋 customer_id resolved to varchar(50) - Reason: Support alphanumeric IDs from legacy system (Decision: 2024-03-15, Team: Data Architecture)", info=True) }}
    {{ return('varchar(50)') }}
  {% endif %}
{% endmacro %}

🏗️ Exemple Réel : Évolution d’un Projet

Phase 1 : Début Simple (Valeurs en Dur)

-- 3 modèles, types en dur, tout va bien
select customer_id::varchar(50) as customer_id

Phase 2 : Croissance (10+ Modèles)

-- 15 modèles avec varchar(50) dupliqué
-- Maintenance devient pénible mais encore gérable

Phase 3 : Complexité (50+ Modèles)

-- 50 modèles, 5 types différents par erreur :
-- - varchar(50) dans 30 modèles  
-- - varchar(100) dans 15 modèles (quelqu'un a "amélioré")
-- - integer dans 3 modèles (bug)
-- - bigint dans 2 modèles (nouveau développeur)
-- = CHAOS TOTAL

Phase 3 bis : Salvation avec Macro

{% macro resolve_conflict(column_name) %}
  {% if column_name == 'customer_id' %}
    {{ return('varchar(50)') }}  -- UNE SEULE SOURCE DE VÉRITÉ
  {% endif %}
{% endmacro %}

-- Dans les 50 modèles :
select customer_id::{{ resolve_conflict('customer_id') }} as customer_id
-- = COHÉRENCE RETROUVÉE

💡 Arguments Techniques Détaillés

1. Principe DRY (Don’t Repeat Yourself)

-- ❌ Violation DRY
varchar(50)  -- Répété 50 fois = 50 points de maintenance

-- ✅ Respect DRY  
{{ resolve_conflict('customer_id') }}  -- 1 définition, 50 utilisations

2. Single Source of Truth

-- ❌ Multiple sources
-- Fichier A: varchar(50)
-- Fichier B: varchar(100)  -- Qui a raison ?

-- ✅ Source unique
-- Macro: varchar(50)  -- SEULE vérité

3. Évolutivité

-- ❌ Règle fixe
varchar(50)  -- Comment ajouter de la logique ?

-- ✅ Règle évolutive
{% macro resolve_conflict(column_name, source_system=none) %}
  {% if column_name == 'customer_id' %}
    {% if source_system == 'legacy' %}
      {{ return('varchar(50)') }}
    {% elif source_system == 'new_system' %}
      {{ return('bigint') }}  
    {% else %}
      {{ return('varchar(50)') }}  -- default
    {% endif %}
  {% endif %}
{% endmacro %}

4. Testabilité

-- ❌ Valeurs en dur = pas de tests centralisés
-- Comment tester que tous les modèles utilisent le bon type ?

-- ✅ Macro = tests centralisés
-- tests/test_contract_resolution.sql
{{ audit_resolved_types(['customer_id', 'order_id']) }}

🎯 Quand Utiliser Quoi ?

Valeurs en Dur = OK pour :

  • Projet tiny (< 5 modèles)
  • Types jamais changeront (id technique)
  • Équipe 1 personne (pas de coordination)
  • Prototype rapide (jetable)

Macro = OBLIGATOIRE pour :

  • Projet moyen/large (10+ modèles)
  • Types métier évolutifs (customer_id, product_id)
  • Équipe multiple (coordination nécessaire)
  • Production long terme (maintenabilité)
  • Conformité/Audit (traçabilité requise)

📋 Template de Décision

Question simple : “Si demain je dois changer le type de cette colonne, combien de fichiers je dois modifier ?”

  • 1 fichier → Valeur en dur OK
  • 2+ fichiers → Macro OBLIGATOIRE

Règle d’or : Dès que vous tapez le même type 2 fois, créez une macro ! 🎯

🚀 Exemple Ultra-Concret

Avant (Nightmare Mode)

-- customer_summary.sql
select customer_id::varchar(50)

-- customer_orders.sql  
select customer_id::varchar(50)

-- customer_analysis.sql
select customer_id::varchar(50)

-- [45 autres fichiers avec varchar(50)]

-- 6 mois plus tard : "Il faut passer en bigint"
-- 😱 48 fichiers à modifier manuellement

Après (Dream Mode)

-- macros/types.sql
{% macro customer_id_type() %}{{ return('varchar(50)') }}{% endmacro %}

-- customer_summary.sql
select customer_id::{{ customer_id_type() }}

-- [47 autres fichiers identiques]

-- 6 mois plus tard : "Il faut passer en bigint"  
-- 😍 1 ligne à changer dans la macro
{% macro customer_id_type() %}{{ return('bigint') }}{% endmacro %}
-- ✅ TOUS les modèles mis à jour automatiquement

Conclusion : La macro n’est pas de la “complexité inutile”, c’est de l’architecture future-proof ! 🎯

Leave a Reply

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