Etl to dbt
A curated collection of agent skills for your AI agents - engineering craft, prompt engineering, design, growth marketing, ...
npx -y skills add mouadja02/skills --skill etl-to-dbtAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
ETL/ELT → dbt migration — Informatica/PowerBI mappings, transformation logic, YAML
SKILL.md
9.8 KB, as published. Nobody here has run it
ETL to dbt Migration
You are converting ETL pipeline metadata (Informatica IICS, PowerCenter XML, or similar) into clean, production-grade dbt models. The goal is not a mechanical 1:1 translation but generating SQL that a data engineer would be proud to maintain.
The Migration Workflow
1. Analyze → Read the mapping/workflow file, identify sources, targets, transformations
2. Plan → Sketch the dbt model graph (staging → intermediate → final)
3. Generate → Write SQL models + YAML metadata
4. Validate → Check column coverage, data type fidelity, test coverage
Step 1: Analyze the Mapping
When given an IICS/Informatica mapping (XML or JSON), extract:
Sources — from Source Qualifier or Read transformations:
- Source connection / schema / table name
- Selected columns and their data types
- Filter conditions (WHERE clause on the source)
- Source join conditions (when the SQ joins multiple tables)
Target — from Target Definition or Write transformations:
- Target schema / table name
- Column mapping (source expression → target column)
- Write mode (insert, update, upsert, delete)
Transformations in order — see the transformation reference below.
Step 2: Map the Model Graph
Typical mapping → model graph:
Raw source table(s)
↓
stg_<source>__<table>.sql (one per source table, light casting)
↓
int_<entity>__<verb>.sql (joins, lookups, aggregations)
↓
fct_<fact>.sql / dim_<dim>.sql (final mart model)
Simple mappings (single source, expression only) collapse to two layers. Complex mappings (multiple sources, lookups, aggregations) use all three.
Step 3: Informatica Transformation → SQL
Source Qualifier (SQ)
The entry point — generates the staging SELECT.
-- stg_crm__customers.sql
select
cast(CUST_ID as varchar(36)) as customer_id,
cast(CUST_NAME as varchar(255)) as customer_name,
cast(CREATED_DT as timestamp_ntz) as created_at
from {{ source('crm', 'customers') }}
-- If SQ has a filter:
where IS_ACTIVE = 'Y'
Expression Transformation (EXP)
Computed columns → computed expressions in SELECT or a CTE.
-- Informatica: IIF(STATUS = 'A', 'Active', 'Inactive')
case when status = 'A' then 'Active' else 'Inactive' end as status_label,
-- Informatica: TO_DATE(DATE_STR, 'YYYY-MM-DD')
try_to_date(date_str, 'YYYY-MM-DD') as parsed_date,
-- Informatica: ISNULL(AMOUNT) → NVL(AMOUNT, 0)
coalesce(amount, 0) as amount,
-- Informatica: SUBSTR(NAME, 1, 50)
left(name, 50) as short_name,
-- Informatica: SYSTIMESTAMP
current_timestamp() as processed_at
Filter Transformation (FIL)
Becomes a WHERE clause in the current CTE.
where status = 'ACTIVE'
and amount > 0
Joiner Transformation (JNR)
Becomes a SQL JOIN. Check the join condition and type (normal = inner, master-outer = left join).
-- Master: orders, Detail: customers → left join orders to customers
select
o.order_id,
o.amount,
c.customer_name
from {{ ref('stg_oms__orders') }} o
left join {{ ref('stg_crm__customers') }} c
on o.customer_id = c.customer_id
Lookup Transformation (LKP)
Becomes a LEFT JOIN or a scalar subquery. Lookup condition = ON clause.
-- Lookup: find product_name from products table by product_id
left join {{ ref('stg_catalog__products') }} p
on f.product_id = p.product_id
-- Then reference p.product_name in the SELECT
When the lookup returns a single value and the source is small, a subquery works:
(select product_name from {{ ref('dim_products') }} where product_id = f.product_id limit 1)
as product_name
Aggregator Transformation (AGG)
Becomes GROUP BY.
select
account_id,
date_trunc('month', order_date) as order_month,
sum(amount) as total_amount,
count(*) as order_count
from {{ ref('int_orders__enriched') }}
group by 1, 2
Router Transformation (RTR)
Splits a stream by condition — becomes either:
- Multiple CTEs with different WHERE clauses (if they land in different targets)
- A CASE expression (if they're combined in one target with a type column)
-- Router with 3 groups landing in one target:
with routed as (
select *,
case
when region = 'EMEA' then 'europe'
when region = 'AMER' then 'americas'
else 'apac'
end as region_group
from source
)
Sorter Transformation (SRT)
Informatica sorts for dedup or ordered processing. In SQL, use window functions:
-- Instead of ORDER BY (which is meaningless without LIMIT), use ROW_NUMBER for dedup:
qualify row_number() over (partition by account_id order by updated_at desc) = 1
Union Transformation (UNI)
→ UNION ALL (Informatica Union always includes all rows)
select account_id, amount, 'source_a' as data_source from {{ ref('stg_a__accounts') }}
union all
select account_id, amount, 'source_b' as data_source from {{ ref('stg_b__accounts') }}
Normalizer Transformation (NRM)
Pivots columns into rows — use UNPIVOT or LATERAL FLATTEN (Snowflake):
select account_id, key as metric_name, value as metric_value
from source
unpivot (value for key in (metric_q1, metric_q2, metric_q3, metric_q4))
Rank Transformation (RNK)
row_number() over (partition by account_id order by score desc) as rnk
-- or:
rank() over (...)
dense_rank() over (...)
Sequence Generator (SEQ)
Use ROW_NUMBER() for surrogate keys, or {{ dbt_utils.generate_surrogate_key([...]) }} for deterministic hashes.
row_number() over (order by created_at) as sequence_num
Update Strategy Transformation (UPD)
Maps to an incremental model with merge strategy:
{{ config(
materialized='incremental',
unique_key='account_id',
incremental_strategy='merge'
) }}
select ...
{% if is_incremental() %}
where updated_at > (select max(updated_at) from {{ this }})
{% endif %}
Data Masking / Anonymization
-- Hash PII
sha2(email, 256) as email_hash,
-- Truncate precision
date_trunc('month', birth_date) as birth_month,
-- Redact
'***REDACTED***' as ssn
Data Type Mapping
| Informatica Type | Snowflake Type |
|---|---|
| String(n) | VARCHAR(n) |
| Nstring(n) | VARCHAR(n) |
| Integer | NUMBER(10,0) |
| Small Integer | NUMBER(5,0) |
| Big Integer | NUMBER(19,0) |
| Decimal(p,s) | NUMBER(p,s) |
| Float | FLOAT |
| Double | DOUBLE |
| Date/Time | TIMESTAMP_NTZ |
| Date | DATE |
| Time | TIME |
| Binary | BINARY |
| Text / Memo | VARCHAR(16777216) |
Always cast at the staging layer — never propagate raw string types into marts.
Generating sources.yml
From Source Qualifier metadata:
version: 2
sources:
- name: crm # logical source name (short, snake_case)
database: RAW_DB # Snowflake database
schema: SALESFORCE_CRM # Snowflake schema
freshness:
warn_after: {count: 6, period: hour}
error_after: {count: 24, period: hour}
loaded_at_field: _loaded_at
tables:
- name: account # dbt source table name
identifier: ACCOUNT_TABLE # actual Snowflake table name (if different)
description: "Raw CRM accounts from Salesforce."
Generating models/_staging.yml
version: 2
models:
- name: stg_crm__accounts
description: "Staged CRM accounts — cast and renamed."
columns:
- name: account_id
data_tests: [not_null, unique]
- name: account_name
data_tests: [not_null]
- name: created_at
data_tests: [not_null]
- name: status
data_tests:
- accepted_values:
values: ["Active", "Inactive", "Pending"]
Naming Conventions
| Layer | Pattern | Example |
|---|---|---|
| Staging | stg_<source>__<table> | stg_crm__accounts |
| Intermediate | int_<entity>__<verb> | int_orders__enriched |
| Fact | fct_<event_plural> | fct_order_lines |
| Dimension | dim_<entity> | dim_customers |
| Snapshot | snp_<entity> | snp_accounts |
Source names: the ETL connection/schema name, lowercased. Table names: the source table name, lowercased, no prefixes.
Complete Model Template
-- models/staging/stg_crm__accounts.sql
with source as (
select * from {{ source('crm', 'account') }}
),
renamed as (
select
-- ids
cast(id as varchar(36)) as account_id,
cast(parent_id as varchar(36)) as parent_account_id,
-- attributes
cast(name as varchar(255)) as account_name,
lower(cast(type as varchar(50))) as account_type,
cast(industry as varchar(100)) as industry,
-- booleans
is_deleted::boolean as is_deleted,
-- dates
cast(created_date as timestamp_ntz) as created_at,
cast(last_modified_date as timestamp_ntz) as updated_at,
-- metadata
_loaded_at as _loaded_at
from source
where is_deleted = false -- apply SQ filter here
)
select * from renamed
Quality Checklist
Before declaring a migration done:
- All source tables have a
source()reference and freshness config - Every staging model casts columns to final types (no implicit casting downstream)
- Primary keys have
not_null+uniquetests - Foreign key relationships have
relationshipstests - Incremental models have a
unique_keyand a filter onis_incremental() - No hardcoded database/schema names — only
ref()andsource() - Model names follow the layer naming convention
- All transformations from the mapping are accounted for