Migrer de Dataform vers dbt : guide étape par étape

Vous utilisez Dataform sur BigQuery, et ça fonctionne. Le SQL compile, les planifications s’exécutent, et vos parties prenantes reçoivent leurs données à temps. Mais quelque chose a changé. Peut-être que votre entreprise a acquis une équipe sous Snowflake. Peut-être que vous en avez assez d’implémenter manuellement des logiques de test que les packages dbt gèrent nativement. Peut-être avez-vous remarqué que chaque offre d’emploi en analytics engineering liste dbt comme prérequis.

Quelle que soit la raison, vous envisagez une migration. Ce guide couvre le processus de l’évaluation à la validation, y compris les conversions mécaniques et les parties qui demandent un vrai effort.

Quand la migration a vraiment du sens

Tous les projets Dataform ne doivent pas migrer. La décision se résume à trois questions.

Passez-vous au multi-warehouse ? Dataform ne fonctionne qu’avec BigQuery. Si votre organisation adopte Snowflake, Databricks ou Redshift en complément de BigQuery, l’architecture d’adaptateurs de dbt devient essentielle. C’est le signal de migration le plus clair.

Avez-vous besoin de l’écosystème ? Le hub de packages dbt propose plus de 200 packages couvrant tout, de la qualité des données (dbt_expectations, Elementary) à l’attribution marketing (dbt-ga4 de Velir) en passant par les fonctions utilitaires (dbt_utils). Si vous implémentez manuellement des fonctionnalités qui existent en tant que package dbt, la migration se rentabilise par la réduction de la maintenance.

La portabilité de carrière compte-t-elle ? La maîtrise de dbt apparaît dans la plupart des offres d’emploi en analytics engineering. L’expertise Dataform reste précieuse mais concentrée dans les organisations fortement orientées GCP. Si les membres de votre équipe se soucient de leur CV, l’expérience dbt a une applicabilité plus large.

Quand rester en place

La migration a un coût réel. Restez sur Dataform si :

  • Vos includes JavaScript sont complexes. La capacité de Dataform à générer des modèles programmatiquement avec du JavaScript standard n’a pas d’équivalent direct dans dbt. Convertir des fichiers .js sophistiqués qui créent dynamiquement des dizaines de modèles nécessite une réécriture substantielle.
  • Des pipelines ML dépendent du comportement du templating. La migration d’une équipe a pris deux mois, plus trois semaines supplémentaires pour corriger des problèmes où le templating JavaScript différait de Jinja de manières qui cassaient le réentraînement des modèles. La fraude qui s’est glissée pendant cette période a coûté plus que des années de frais de licence.
  • Votre cas d’usage est simple. Si vous exécutez des modèles de transformation basiques sans logique incrémentale complexe, le tier gratuit de Dataform sur BigQuery fait sens économiquement.

Le calcul de rentabilité : si la migration prend trois mois de temps d’ingénierie et que vous comparez avec dbt Cloud à 100 $/utilisateur/mois, il faut une équipe de 10 personnes pendant plus de deux ans avant que les économies de licence seules justifient le changement. Les besoins multi-warehouse ou les exigences d’écosystème changent cette équation immédiatement.

Comprendre la correspondance conceptuelle

Avant de toucher au code, comprenez comment les concepts Dataform se transposent dans dbt. Certaines correspondances sont directes ; d’autres nécessitent de repenser votre approche.

Équivalents directs

DataformdbtNotes
${ref("model")}{{ ref('model') }}Même concept, syntaxe différente
config { type: "table" }{{ config(materialized='table') }}Déclaration de matérialisation
config { type: "view" }{{ config(materialized='view') }}Par défaut dans les deux outils
.sqlx files.sql filesChangement d’extension
definitions/ foldermodels/ folderNommage des répertoires

Changements conceptuels

Sources vs. déclarations. Dataform utilise des fichiers de déclaration pour définir les tables sources. dbt utilise des définitions de sources en YAML qui remplissent le même rôle mais ajoutent les vérifications de fraîcheur et la documentation au même endroit.

Déclaration Dataform :

definitions/sources/ga4.js
declare({
database: "my-project",
schema: "analytics_123456789",
name: "events_*"
});

Source dbt :

models/base/_sources.yml
sources:
- name: ga4
database: my-project
schema: analytics_123456789
tables:
- name: events
identifier: "events_*"
freshness:
warn_after: {count: 24, period: hour}

Jinja vs. templating JavaScript. Dataform permet d’écrire du vrai JavaScript (boucles, conditions, fonctions) n’importe où dans votre projet. dbt utilise le templating Jinja2, qui ressemble visuellement mais se comporte différemment. Cet écart philosophique, exploré dans mon comparatif JavaScript vs Jinja, affecte chaque aspect de la migration.

Condition Dataform :

${when(incremental(), `AND updated_at > (SELECT MAX(updated_at) FROM ${self()})`)}

Condition dbt :

{% if is_incremental() %}
AND updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

La syntaxe est différente mais gérable. Le vrai défi vient de la génération dynamique de modèles, que nous couvrirons plus loin.

Les lacunes à anticiper

Certaines fonctionnalités Dataform n’ont pas d’équivalent direct dans dbt :

  • Seeds. Les seeds dbt sont des fichiers CSV qui se chargent comme des tables. Dataform n’a pas d’équivalent, donc vous avez probablement des tables BigQuery remplissant ce rôle que vous déclarerez comme sources.
  • Snapshots. La fonctionnalité snapshot de dbt pour les tables SCD Type 2 nécessite une implémentation manuelle dans Dataform. Si vous avez construit une logique SCD personnalisée, vous la convertirez vers la syntaxe snapshot plus simple de dbt.
  • Packages. Dataform n’a pas d’écosystème de packages. Tous les includes personnalisés que vous avez écrits en JavaScript doivent être convertis en macros dbt ou remplacés par des packages existants.

Configurer votre environnement dbt pour BigQuery

La correspondance conceptuelle étant claire, configurez votre projet dbt.

dbt Core vs. dbt Cloud

Pour les équipes BigQuery migrant depuis Dataform, commencez par dbt Core sauf si vous avez besoin immédiatement de fonctionnalités spécifiques à Cloud (voir mon comparatif dbt Core vs Cloud pour les détails). Raisons :

  • Zéro coût supplémentaire pendant la migration (vous êtes déjà habitué à la gratuité avec Dataform)
  • Le développement local correspond à votre workflow actuel
  • Passage à Cloud ultérieur si vous avez besoin du semantic layer, de Mesh ou de la gouvernance entreprise

Installez dbt avec l’adaptateur BigQuery :

Terminal window
pip install dbt-bigquery

Initialisation du projet

Créez un nouveau projet dbt :

Terminal window
dbt init my_project

Configurez votre connexion BigQuery dans ~/.dbt/profiles.yml :

my_project:
outputs:
dev:
type: bigquery
method: oauth
project: my-gcp-project
dataset: dbt_dev
threads: 4
location: US
target: dev

Comparaison des structures de répertoires

Votre structure de projet Dataform se transpose dans dbt ainsi :

# Dataform # dbt
definitions/ models/
sources/ base/
staging/ intermediate/
reporting/ marts/
includes/ macros/
dataform.json dbt_project.yml

Le répertoire models/ remplace definitions/. Organisez par couche (base, intermediate, marts) plutôt que par système source pour respecter les conventions dbt. Pour un aperçu approfondi, consultez mon guide des couches de modèles dbt.

Convertir vos modèles étape par étape

Procédez à la conversion par phases, en validant chacune avant de passer à la suivante.

Phase 1 : Conversions basiques de tables et vues

Commencez par les modèles simples sans logique incrémentale ni templating complexe. Ce sont des conversions mécaniques.

Dataform :

config {
type: "table",
schema: "reporting"
}
SELECT
customer_id,
SUM(order_total) AS lifetime_value
FROM ${ref("base_orders")}
GROUP BY 1

dbt :

{{ config(
materialized='table',
schema='reporting'
) }}
SELECT
customer_id,
SUM(order_total) AS customer__lifetime_value
FROM {{ ref('base__shopify__orders') }}
GROUP BY 1

La conversion est directe : changez la syntaxe du bloc config, remplacez ${ref()} par {{ ref() }}, et enregistrez avec une extension .sql.

Phase 2 : Déclarations de sources

Remplacez les déclarations Dataform par des fichiers YAML de sources dbt. Créez un fichier source par système source dans votre dossier base.

models/base/ga4/_ga4__sources.yml
version: 2
sources:
- name: ga4_raw
database: "{{ var('ga4_project') }}"
schema: "{{ var('ga4_dataset') }}"
tables:
- name: events
identifier: "events_*"
description: "Raw GA4 event export"

Dans vos modèles, remplacez les références aux sources :

-- Dataform: ${ref("analytics_123456789", "events_*")}
-- dbt:
SELECT
event_date,
event_timestamp,
event_name,
user_pseudo_id
FROM {{ source('ga4_raw', 'events') }}

Définissez les variables dans dbt_project.yml :

vars:
ga4_project: my-gcp-project
ga4_dataset: analytics_123456789

Phase 3 : Modèles incrémentaux

La syntaxe incrémentale de Dataform se traduit vers la macro is_incremental() de dbt (couvert en détail dans mon guide des modèles incrémentaux dbt) :

Dataform :

config {
type: "incremental",
uniqueKey: ["event_id"],
updatePartitionFilter: "event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)"
}
SELECT
event_id,
event_date,
event_name,
user_pseudo_id
FROM ${ref("base_events")}
${when(incremental(), `WHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)`)}

dbt :

{{ config(
materialized='incremental',
unique_key='event_id',
partition_by={
'field': 'event_date',
'data_type': 'date'
},
incremental_strategy='merge'
) }}
SELECT
event_id,
event_date,
event_name,
user_pseudo_id
FROM {{ ref('base__ga4__events') }}
{% if is_incremental() %}
WHERE event_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
{% endif %}

Différences clés :

  • dbt requiert une configuration partition_by explicite pour BigQuery
  • La incremental_strategy est merge par défaut pour BigQuery, mais la rendre explicite améliore la lisibilité
  • updatePartitionFilter est intégré à votre logique de clause WHERE

Phase 4 : Migration des tests

Les assertions inline de Dataform deviennent des tests YAML dans dbt.

Dataform :

config {
type: "table",
assertions: {
uniqueKey: ["customer_id"],
nonNull: ["customer_id", "email"],
rowConditions: ['email LIKE "%@%.%"']
}
}

dbt :

models/marts/_marts__models.yml
version: 2
models:
- name: mrt__sales__customers
columns:
- name: customer_id
tests:
- unique
- not_null
- name: customer__email
tests:
- not_null

Pour les conditions de lignes personnalisées, utilisez le package dbt_expectations :

- name: customer__email
tests:
- dbt_expectations.expect_column_values_to_match_regex:
regex: "^.+@.+\\..+$"

Installez les packages en les ajoutant dans packages.yml :

packages:
- package: calogica/dbt_expectations
version: 0.10.4

Puis lancez dbt deps pour installer.

Gérer les parties difficiles

Les sections précédentes couvrent les conversions mécaniques. Passons maintenant aux parties qui demandent une vraie réflexion.

Conversion des macros

Les includes JavaScript de Dataform deviennent des macros dbt. La logique reste similaire ; la syntaxe change significativement.

Include Dataform :

includes/helpers.js
function unnest_event_param(param_name, value_type = 'string_value') {
return `(SELECT value.${value_type} FROM UNNEST(event_params) WHERE key = '${param_name}')`;
}
module.exports = { unnest_event_param };

Macro dbt :

-- macros/unnest_event_param.sql
{% macro unnest_event_param(param_name, value_type='string_value') %}
(SELECT value.{{ value_type }} FROM UNNEST(event_params) WHERE key = '{{ param_name }}')
{% endmacro %}

L’utilisation passe de ${helpers.unnest_event_param('page_location')} à {{ unnest_event_param('page_location') }}.

Génération dynamique de modèles

Le JavaScript de Dataform excelle dans la génération dynamique de modèles, tandis que dbt n’a pas d’équivalent propre. Si vous avez du code comme celui-ci :

definitions/country_tables.js
const countries = ["US", "GB", "FR", "DE"];
countries.forEach(country => {
publish(`reporting_${country}`)
.dependencies(["source_table"])
.query(ctx => `SELECT * FROM ${ctx.ref("source_table")} WHERE country = '${country}'`);
});

dbt n’a pas d’équivalent direct. Vos options :

  1. Écrire des modèles individuels. Si vous avez peu de variations, créez simplement des fichiers .sql séparés.
  2. Utiliser dbt_codegen. Générez les fichiers YAML et SQL une fois, puis maintenez-les manuellement.
  3. Prétraitement externe. Utilisez un script Python pour générer les fichiers .sql avant l’exécution de dbt.

Dans la plupart des cas, l’option 1 est la bonne réponse. Résistez à la tentation de sur-ingénierer.

Pré-opérations et post-opérations

Les pre_operations et post_operations de Dataform deviennent des hooks dbt :

Dataform :

config {
type: "table",
pre_operations: ["DELETE FROM ${self()} WHERE date < DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)"]
}

dbt :

{{ config(
materialized='table',
pre_hook="DELETE FROM {{ this }} WHERE date < DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)"
) }}

Pour les pré/post opérations complexes couvrant plusieurs instructions, envisagez des matérialisations personnalisées.

Valider la migration

Ne basculez pas tout d’un coup. Exécutez les systèmes en parallèle et validez minutieusement.

Exécution en parallèle

Gardez Dataform en fonctionnement pendant la mise en place de dbt. Configurez dbt pour écrire dans un dataset séparé :

dbt_project.yml
models:
my_project:
+schema: dbt_migration_validation

Exécutez les deux systèmes sur les mêmes données sources.

Requêtes de comparaison

Pour chaque modèle migré, lancez une validation :

-- Comparaison du nombre de lignes
SELECT 'dataform' AS source, COUNT(*) AS row_count FROM dataform_dataset.model_name
UNION ALL
SELECT 'dbt' AS source, COUNT(*) AS row_count FROM dbt_migration_validation.model_name;
-- Comparaison au niveau des colonnes pour les métriques clés
SELECT
ABS(d.total_revenue - t.total_revenue) AS revenue_diff,
ABS(d.order_count - t.order_count) AS order_diff
FROM (SELECT SUM(revenue) AS total_revenue, COUNT(*) AS order_count FROM dataform_dataset.orders) d
CROSS JOIN (SELECT SUM(revenue) AS total_revenue, COUNT(*) AS order_count FROM dbt_migration_validation.orders) t;

Tests de régression des pipelines ML

Si des modèles de machine learning consomment vos sorties de transformation, testez minutieusement. L’histoire d’avertissement de la recherche : une équipe a découvert que le comportement du templating JavaScript et Jinja différait de manières qui cassaient le réentraînement des modèles. La précision numérique, le traitement des nulls et le formatage des timestamps peuvent tous diverger de manière subtile.

Exécutez votre pipeline d’entraînement ML sur les sorties dbt avant de déclarer victoire. Comparez les métriques de performance des modèles, pas seulement le nombre de lignes.

Le calendrier réaliste

En fonction de la complexité du projet, voici à quoi vous attendre :

Taille du projetDélaiEffort principal
Petit (~20 modèles)1-2 semainesConversion essentiellement automatisée
Moyen (50-100 modèles)2-4 semainesConversion des macros, mise en place des tests
Grand (100+ modèles, macros complexes)2-3 moisRéécriture JavaScript, validation
Entreprise avec dépendances ML3-6 moisExécution en parallèle, validation par les parties prenantes

Ce qui allonge les délais :

  • Des includes JavaScript complexes nécessitant une réécriture manuelle
  • Des stratégies incrémentales personnalisées au-delà du simple merge
  • Des pipelines ML nécessitant une validation de régression
  • Les processus d’approbation des parties prenantes
  • Les exigences d’exécution en parallèle pour la conformité

Outils disponibles

Deux outils open source peuvent accélérer la migration :

ra_dbt_to_dataform - Malgré le nom suggérant la direction inverse, la même équipe fournit des patterns pour la conversion. Utilise GPT-4 pour la conversion de macros complexes.

dataform-to-dbt - Outil Node.js qui gère les refs, assertions et matérialisations en vue. Lancez avec npx dataform-to-dbt. Limites : ne gère pas les includes JavaScript, les modèles incrémentaux ni les pré-opérations complexes.

Ces outils couvrent environ 60-70 % d’un projet typique. Prévoyez que le reste sera du travail manuel.

Prendre la décision

La migration de Dataform vers dbt n’est ni universellement bonne ni universellement mauvaise. Tout dépend de votre situation spécifique.

Migrez quand vous passez au multi-warehouse, avez besoin de l’écosystème de packages, ou voulez des fonctionnalités entreprise comme le semantic layer et Mesh. L’investissement de 2-3 mois porte ses fruits en maintenance réduite et capacités élargies.

Restez en place quand votre projet Dataform est stable, que vous êtes uniquement sur BigQuery sans intention de changer, et qu’il ne vous manque pas de fonctionnalités. Le principe du “si ça marche, on ne touche pas” s’applique.

Réévaluez le calendrier quand vous avez de la génération JavaScript complexe, des dépendances avec des pipelines ML, ou des parties prenantes exigeant une exécution en parallèle prolongée. L’estimation de deux mois devient six mois dans ces scénarios.

Les outils transforment le SQL de manière identique au niveau du warehouse. Ce qui diffère, c’est l’écosystème, le modèle commercial et les implications de carrière. Faites votre choix en fonction de la trajectoire de votre organisation, pas de la syntaxe qui vous paraît plus élégante.