Créer et publier votre propre package dbt

Vous avez écrit la même macro de date spine dans trois projets différents. Ce jeu de modèles de reconnaissance de revenus ? Votre équipe n’arrête pas de vous solliciter pour la dernière version. À un moment donné, copier du SQL entre les repos crée plus de problèmes que ça n’en résout.

Les packages dbt sont la solution, et en créer un est plus simple que la plupart des gens ne le pensent. Un package est simplement un projet dbt structuré pour la réutilisation. Les patterns qui le font fonctionner (var(), dispatch, namespacing) sont simples une fois qu’on les voit en action.

Ce qui différencie un package d’un projet

Un package dbt est un projet dbt. Mêmes fichiers, même structure, même modèle d’exécution. Le seul fichier obligatoire est dbt_project.yml. Ce qui rend un package différent, c’est l’intention : il est conçu pour que quelqu’un d’autre l’installe dans son projet via dbt deps.

Trois principes distinguent un package bien construit d’un projet classique :

  1. Configurable. Pas de noms de bases de données, de références de schéma ou d’identifiants de tables en dur. Tout ce que les utilisateurs pourraient avoir besoin de personnaliser passe par var().
  2. Namespacé. Les noms de modèles incluent le préfixe du package pour éviter les collisions. my_package__customers, pas customers.
  3. Adapté aux différents adaptateurs. Le SQL qui diffère selon les warehouses utilise adapter.dispatch() pour que le package fonctionne sur Snowflake, BigQuery, Redshift et les autres.

Structure du répertoire d’un package

Voici la structure utilisée par dbt Labs et Fivetran pour leurs propres packages :

dbt-my_package/
├── dbt_project.yml # Requis : configuration du package
├── packages.yml # Dépendances en amont
├── macros/
│ ├── my_macro.sql
│ └── _macros.yml # Documentation des macros
├── models/
│ ├── base/
│ └── marts/
├── tests/generic/ # Tests génériques personnalisés
├── integration_tests/ # Sous-projet pour les tests
│ ├── dbt_project.yml
│ ├── packages.yml # Référence le parent via local: ../
│ ├── seeds/ # Données simulées
│ ├── models/
│ └── tests/
├── .github/workflows/ # Configuration CI
├── README.md
├── CHANGELOG.md
└── LICENSE

Le dbt_project.yml contient quelques paramètres spécifiques aux packages :

name: 'my_package'
version: '0.1.0'
require-dbt-version: [">=1.3.0", "<3.0.0"]
config-version: 2
models:
my_package:
+materialized: view
vars:
my_package_schema: 'my_data'
my_package_database: null
my_package__some_model_enabled: true

La plage require-dbt-version doit inclure à la fois dbt Core 1.x et Fusion 2.x. Fixer la borne supérieure à <3.0.0 couvre les deux. La matérialisation par défaut doit être view, pas table, pour que les utilisateurs ne créent pas de tables physiques inutiles en installant votre package. Chaque option configurable doit avoir une valeur par défaut raisonnable déclarée sous vars.

Écrire des macros réutilisables avec dispatch

Si votre macro génère du SQL qui diffère selon le warehouse, adapter.dispatch() la rend portable. Vous écrivez une macro dispatcher qui appelle des implémentations spécifiques à chaque adaptateur.

-- macros/my_safe_divide.sql
{% macro my_safe_divide(numerator, denominator) %}
{{ return(adapter.dispatch('my_safe_divide', 'my_package')(numerator, denominator)) }}
{% endmacro %}
{% macro default__my_safe_divide(numerator, denominator) %}
CASE
WHEN {{ denominator }} = 0 THEN NULL
ELSE {{ numerator }} / {{ denominator }}
END
{% endmacro %}
{% macro bigquery__my_safe_divide(numerator, denominator) %}
SAFE_DIVIDE({{ numerator }}, {{ denominator }})
{% endmacro %}

L’appel dispatch recherche une implémentation bigquery__my_safe_divide lors de l’exécution sur BigQuery, puis se rabat sur default__my_safe_divide pour tout autre adaptateur.

Avant d’écrire des macros spécifiques à un adaptateur, vérifiez si dbt Core en fournit déjà une intégrée. Depuis dbt-utils v1.0, les macros cross-database comme datediff, dateadd, safe_cast et hash vivent dans le namespace dbt. Appelez {{ dbt.datediff(...) }} au lieu de réimplémenter l’arithmétique des dates vous-même.

Les utilisateurs de votre package peuvent également surcharger le comportement de dispatch dans leur propre dbt_project.yml en spécifiant une configuration dispatch avec un search_order personnalisé. C’est ainsi que des packages comme spark_utils assurent la compatibilité avec des adaptateurs non-core. Pour en savoir plus sur la construction de patterns de macros réutilisables, les mêmes principes qui s’appliquent aux macros de projet s’appliquent aux packages.

Rendre les modèles packageables

L’équipe Fivetran maintient plus de 100 packages dbt, et leur pattern pour les modèles configurables est devenu le standard. Trois techniques rendent les modèles installables par n’importe qui.

Schéma et base de données via var(). Ne codez jamais en dur l’emplacement des données source :

-- models/base/base__my_package__events.sql
WITH source AS (
SELECT
event_id,
event_name,
event_timestamp,
user_id
FROM {{ source('my_package', 'events') }}
)
SELECT
event_id,
event_name,
event_timestamp,
user_id
FROM source
models/base/_sources.yml
sources:
- name: my_package
schema: "{{ var('my_package_schema', 'my_data') }}"
database: "{{ var('my_package_database', target.database) }}"
tables:
- name: events
identifier: "{{ var('my_package_events_identifier', 'events') }}"

Les utilisateurs orientent le package vers leur propre schéma en définissant my_package_schema dans leur dbt_project.yml. La variable identifier gère les cas où une table a un nom différent dans le warehouse de quelqu’un.

Activer/désactiver les modèles. Permettez aux utilisateurs de désactiver les parties du package dont ils n’ont pas besoin :

-- models/marts/my_package__daily_summary.sql
{{ config(enabled=var('my_package__daily_summary_enabled', true)) }}

Préfixer les noms de modèles. Chaque modèle de votre package doit commencer par le nom du package. Si votre package s’appelle revenue_tools, nommez vos modèles revenue_tools__monthly_mrr et revenue_tools__churn_events, pas monthly_mrr et churn_events. Cela évite les collisions de noms quand les utilisateurs ont plusieurs packages installés.

Tester avec le pattern de tests d’intégration

Vous ne pouvez pas tester un package dbt de manière isolée car il est conçu pour être installé dans un autre projet. La solution est un sous-projet integration_tests/ à l’intérieur du repo de votre package qui installe le package parent comme dépendance locale.

integration_tests/packages.yml
packages:
- local: ../

Le workflow de test utilise des seeds comme données simulées. Créez des fichiers CSV représentant les données source attendues par votre package, configurez-les pour qu’ils arrivent dans le bon schéma, puis comparez les sorties des modèles aux résultats attendus.

integration_tests/dbt_project.yml
name: 'my_package_integration_tests'
seeds:
my_package_integration_tests:
+schema: my_data # Correspond au schéma source par défaut du package

Écrivez un test de schéma qui compare la sortie de votre modèle à un seed attendu :

integration_tests/models/_schema.yml
models:
- name: my_package__daily_summary
data_tests:
- dbt_utils.equality:
compare_model: ref('expected_daily_summary')

Exécutez la suite complète depuis le sous-projet :

Terminal window
cd integration_tests/
dbt deps && dbt seed && dbt run && dbt test

Ce pattern, utilisé par dbt-utils et chaque package Fivetran, vous donne la certitude que votre package produit des résultats corrects avant que quiconque ne l’installe. Si vous voulez aller plus loin, une stratégie de test plus large avec des tests génériques sur les modèles de votre package ajoute une couche de protection supplémentaire.

Publier sur le dbt Hub

Le dbt Hub est le canal de distribution standard pour les packages dbt open-source. La publication nécessite trois choses : le package est hébergé sur GitHub, il possède un dbt_project.yml avec un champ name, et les releases utilisent des tags de versionnage sémantique.

Le processus :

  1. Créez une release GitHub avec un tag semver (par ex., v0.1.0). Les tags comme first-release ou beta sont ignorés par l’indexeur du Hub.
  2. Ouvrez une pull request sur le dépôt hub.getdbt.com pour ajouter votre package à l’index.
  3. Les PR sont généralement approuvées en un jour ouvré.
  4. Après l’enregistrement initial, un script appelé hubcap s’exécute toutes les heures pour détecter automatiquement les nouvelles releases GitHub. Plus besoin de PR pour les mises à jour de versions.

Une fois publié, les utilisateurs installent votre package avec :

packages.yml
packages:
- package: your-namespace/my_package
version: [">=0.1.0", "<1.0.0"]

Les packages du Hub ont un avantage significatif par rapport aux packages Git : la résolution automatique des dépendances transitives. Si votre package dépend de dbt-utils et que le projet de l’utilisateur dépend aussi de dbt-utils, le Hub réconcilie automatiquement les conflits de versions. Les packages Git ne peuvent pas faire cela.

Bonnes pratiques du Hub d’après la documentation hubcap :

  • Définissez require-dbt-version pour que les utilisateurs connaissent la compatibilité d’emblée
  • Déclarez les dépendances depuis le Hub (pas Git) autant que possible
  • Utilisez les plages de versions les plus larges possibles (ne vous bloquez pas sur des versions de patch)
  • Ne surchargez pas le comportement de dbt Core qui affecte des ressources en dehors de votre package
  • Utilisez les macros cross-database intégrées à dbt Core au lieu d’écrire les vôtres

Packages privés via Git

Tous les packages n’ont pas vocation à être sur le Hub. Les packages internes partagés au sein d’une entreprise peuvent être distribués comme dépendances Git :

packages.yml
packages:
- git: "https://github.com/my-org/dbt-internal-utils.git"
revision: v0.3.0

Fixez la référence à un tag ou un SHA de commit, pas un nom de branche. Pointer vers main signifie que la résolution de votre package change à chaque commit, ce qui casse la reproductibilité.

Pour les dépôts privés, dbt supporte l’authentification native via les fournisseurs Git :

packages:
- private: my-org/dbt-internal-utils
provider: github
revision: v0.3.0

Cela utilise l’intégration Git déjà configurée dans votre environnement (GitHub, GitLab, Azure DevOps) sans intégrer de tokens dans votre packages.yml. Pour les cas où vous avez besoin de tokens explicites, l’approche traditionnelle fonctionne toujours via env_var() :

packages:
- git: "https://{{ env_var('GIT_TOKEN') }}@github.com/my-org/dbt-internal-utils.git"
revision: v0.3.0

CI/CD pour les packages

Un package qui fonctionne sur votre ordinateur pourrait échouer sur un warehouse différent ou une version de dbt différente. GitHub Actions avec une stratégie matricielle couvre les deux dimensions :

.github/workflows/ci.yml
name: CI
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
warehouse: [snowflake, bigquery, postgres]
dbt-version: ['1.9.0', '1.11.0']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install dbt-${{ matrix.warehouse }}==${{ matrix.dbt-version }}
- run: |
cd integration_tests/
dbt deps
dbt seed --target ${{ matrix.warehouse }}
dbt run --target ${{ matrix.warehouse }}
dbt test --target ${{ matrix.warehouse }}
env:
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
BIGQUERY_KEYFILE: ${{ secrets.BIGQUERY_KEYFILE }}

Stockez les credentials du warehouse comme GitHub Secrets. Chaque combinaison de la matrice exécute la suite complète de tests d’intégration, détectant les problèmes spécifiques aux adaptateurs avant qu’ils n’atteignent les utilisateurs.

Erreurs courantes dans les packages dbt

Après avoir examiné des dizaines de packages communautaires, les mêmes problèmes reviennent régulièrement.

Références de schéma en dur. FROM my_database.raw_stripe.payments fonctionne dans votre projet, mais casse dans tous les autres. Utilisez toujours source() avec var() pour la configuration du schéma et de la base de données.

Implémentations dispatch manquantes. Si votre package utilise du SQL qui varie selon le warehouse et que vous n’écrivez qu’une implémentation default__, les utilisateurs sur d’autres adaptateurs obtiennent un comportement inattendu ou des erreurs. Testez sur chaque adaptateur que vous prétendez supporter.

Contraintes de version trop strictes. Fixer version: "0.20.1" oblige chaque utilisateur à utiliser exactement cette version et crée des conflits de dépendances avec d’autres packages. Utilisez des plages : [">=0.20.0", "<1.0.0"].

Noms de modèles génériques. Un modèle appelé customers entrera en collision avec le propre modèle customers de l’utilisateur. Préfixez tout avec le nom de votre package.

Matérialisation en table par défaut. Quand quelqu’un exécute dbt deps && dbt run, votre package ne devrait pas créer 30 tables physiques dans son warehouse. Utilisez les vues par défaut et laissez les utilisateurs surcharger pour la performance.

Pas de require-dbt-version. Sans cela, les utilisateurs sur des versions de dbt incompatibles obtiennent des erreurs de compilation cryptiques au lieu d’un message clair. Avec le moteur Fusion (dbt 2.0) maintenant disponible, définir [">=1.3.0", "<3.0.0"] évite la confusion entre les deux runtimes.

Packages vs Mesh : quand utiliser lequel

Les packages dbt et dbt Mesh résolvent des problèmes différents, même s’ils se chevauchent sur les bords.

Les packages servent au partage de code. Quand quelqu’un installe votre package, il obtient le code source complet (macros, modèles, tests). Le code s’exécute dans son projet et compile contre son warehouse. Les packages open-source sur le Hub, les bibliothèques utilitaires internes et les tests génériques partagés sont tous de bons candidats pour ce modèle.

Mesh (via dependencies.yml et les refs cross-project) sert au partage de produits de données. Les équipes référencent les modèles publiés les unes des autres sans installer de code source. Une équipe marketing peut faire ref('finance', 'mrt__finance__monthly_revenue') sans savoir comment ce modèle est construit. Mesh nécessite dbt Cloud Enterprise et des contrôles d’accès aux modèles (public, protected, private).

Si vous partagez de la logique réutilisable (macros, tests génériques, templates de modèles), créez un package. Si vous partagez des produits de données (modèles curatés avec des contrats définis), utilisez Mesh. Pour les équipes explorant Mesh, l’outil CLI dbt-meshify aide à découper un projet monolithique en projets interconnectés. Pour en savoir plus sur la structuration des projets dbt, cette décision précède souvent le choix entre packages et Mesh.

La plupart des packages commencent comme du code que vous avez déjà écrit et éprouvé au combat dans vos propres projets. L’étape de packaging consiste principalement à rendre ce code configurable, à l’épreuve des collisions et vérifiable. Vous n’avez pas besoin de publier sur le Hub dès le premier jour. Un package Git partagé entre les projets de votre équipe est un très bon point de départ, et quand il sera suffisamment stable pour la communauté au sens large, le Hub n’est qu’à une seule PR.

L’écosystème de packages dbt compte plus de 400 packages, mais il y a toujours des manques. Si vous avez résolu un problème que d’autres analytics engineers rencontrent, packager cette solution est l’une des contributions les plus précieuses que vous puissiez apporter à la communauté.