Tutorial: Convert a PowerCenter Mapping to a dbt Model¶
A step-by-step walkthrough for converting a real-world PowerCenter mapping to dbt SQL, including transformation conversion, testing, documentation, and deployment.
Prerequisites¶
- Basic SQL knowledge
- dbt CLI or dbt Cloud account
- Access to your PowerCenter repository (for mapping export)
- Target Azure SQL or Synapse database provisioned
- Git repository for dbt project
Estimated time: 2-3 hours
What you will build¶
By the end of this tutorial, you will have:
- Exported a PowerCenter mapping's metadata
- Analyzed the transformation logic
- Created equivalent dbt models (staging, intermediate, mart)
- Written dbt tests replacing manual QA
- Generated documentation
- Deployed through CI/CD
Step 1: Export the PowerCenter mapping (15 min)¶
Option A: PowerCenter Designer export¶
- Open PowerCenter Designer
- Navigate to the mapping you want to convert (we'll use
m_ORDER_FACTas our example) - Right-click -> Properties -> note all transformation names and types
- For each transformation, record:
- Transformation type (Expression, Lookup, Aggregator, etc.)
- Input/output ports and their expressions
- Lookup SQL overrides
- Filter conditions
Option B: Repository query¶
-- Export mapping metadata from PowerCenter repository
SELECT
m.MAPPING_NAME,
wi.WIDGET_TYPE AS transformation_type,
wi.WIDGET_NAME AS transformation_name,
p.PORT_NAME,
p.PORT_TYPE, -- INPUT, OUTPUT, INPUT/OUTPUT
p.EXPRESSION,
p.DATATYPE,
p.PRECISION,
p.SCALE
FROM REP_MAPPINGS m
JOIN REP_WIDGET_INST wi ON m.MAPPING_ID = wi.MAPPING_ID
JOIN REP_WIDGET_ATTR p ON wi.WIDGET_ID = p.WIDGET_ID
WHERE m.MAPPING_NAME = 'm_ORDER_FACT'
ORDER BY wi.WIDGET_NAME, p.PORT_NAME;
Our example mapping: m_ORDER_FACT¶
This mapping loads an order fact table with the following transformations:
SQ_ORDERS (Source Qualifier)
-> Filter: WHERE order_date >= $$START_DATE
-> Joiner: INNER JOIN to CUSTOMERS on customer_id
|
v
EXP_DERIVE (Expression)
-> order_amount_usd = order_amount * exchange_rate
-> order_year = TO_CHAR(order_date, 'YYYY')
-> order_month = TO_CHAR(order_date, 'MM')
-> is_high_value = IIF(order_amount_usd > 10000, 'Y', 'N')
|
v
LKP_PRODUCT (Lookup)
-> LEFT JOIN to DIM_PRODUCT on product_id
-> Returns: product_name, product_category
|
v
LKP_REGION (Lookup)
-> LEFT JOIN to REF_REGION on region_code
-> Returns: region_name
|
v
AGG_MONTHLY (Aggregator)
-> GROUP BY: customer_id, order_year, order_month, product_category, region_name
-> SUM(order_amount_usd), COUNT(order_id)
|
v
UPD_INSERT (Update Strategy)
-> DD_INSERT for all rows (append-only fact)
|
v
TGT_FACT_ORDERS (Target)
-> INSERT into DW.FACT_ORDER_MONTHLY
Step 2: Set up the dbt project (20 min)¶
Initialize project¶
# Create a new dbt project (skip if you have an existing one)
dbt init order_analytics
# Navigate to the project
cd order_analytics
Configure connection¶
Edit profiles.yml to connect to your Azure SQL or Synapse database:
# ~/.dbt/profiles.yml
order_analytics:
target: dev
outputs:
dev:
type: sqlserver # or synapse, fabric
driver: "ODBC Driver 18 for SQL Server"
server: your-server.database.windows.net
database: your_database
schema: dbt_dev
authentication: ActiveDirectoryInteractive # or ActiveDirectoryServicePrincipal
encrypt: true
trust_cert: false
Define sources¶
Create source definitions for the tables the PowerCenter mapping reads from:
# models/staging/erp/_erp__sources.yml
version: 2
sources:
- name: erp
description: ERP system (source for order data)
database: source_db
schema: dbo
tables:
- name: orders
description: Raw order transactions
columns:
- name: order_id
tests: [unique, not_null]
- name: customer_id
tests: [not_null]
- name: order_date
tests: [not_null]
loaded_at_field: updated_at
freshness:
warn_after: { count: 24, period: hour }
error_after: { count: 48, period: hour }
- name: customers
description: Customer master data
columns:
- name: customer_id
tests: [unique, not_null]
- name: ref
description: Reference data
database: source_db
schema: ref
tables:
- name: regions
description: Region reference lookup
- name: exchange_rates
description: Currency exchange rates
Step 3: Create staging models (30 min)¶
Staging models are 1:1 with source tables. They handle renaming, type casting, and basic cleansing.
Orders staging model¶
-- models/staging/erp/stg_erp__orders.sql
WITH source AS (
SELECT * FROM {{ source('erp', 'orders') }}
),
renamed AS (
SELECT
order_id,
customer_id,
product_id,
region_code,
CAST(order_date AS DATE) AS order_date,
CAST(order_amount AS DECIMAL(18, 2)) AS order_amount,
currency_code,
order_status,
updated_at
FROM source
WHERE order_status != 'cancelled' -- equivalent to SQ filter
)
SELECT * FROM renamed
Customers staging model¶
-- models/staging/erp/stg_erp__customers.sql
WITH source AS (
SELECT * FROM {{ source('erp', 'customers') }}
)
SELECT
customer_id,
customer_name,
customer_segment,
country_code
FROM source
Exchange rates staging model¶
-- models/staging/ref/stg_ref__exchange_rates.sql
SELECT
currency_code,
rate_date,
exchange_rate_to_usd
FROM {{ source('ref', 'exchange_rates') }}
Step 4: Create intermediate model (30 min)¶
The intermediate model replaces the PowerCenter Expression, Lookups, and Joiner transformations.
-- models/intermediate/int_orders__enriched.sql
-- Replaces: SQ_ORDERS join + EXP_DERIVE + LKP_PRODUCT + LKP_REGION
WITH orders AS (
SELECT * FROM {{ ref('stg_erp__orders') }}
),
customers AS (
SELECT * FROM {{ ref('stg_erp__customers') }}
),
products AS (
SELECT * FROM {{ ref('stg_ref__products') }}
),
regions AS (
SELECT * FROM {{ ref('stg_ref__regions') }}
),
exchange_rates AS (
SELECT * FROM {{ ref('stg_ref__exchange_rates') }}
),
-- Step 1: Join orders to customers (replaces SQ_ORDERS Joiner)
orders_with_customers AS (
SELECT
o.order_id,
o.customer_id,
c.customer_name,
c.customer_segment,
o.product_id,
o.region_code,
o.order_date,
o.order_amount,
o.currency_code
FROM orders o
INNER JOIN customers c
ON o.customer_id = c.customer_id
),
-- Step 2: Apply exchange rate (replaces EXP_DERIVE.order_amount_usd)
orders_with_usd AS (
SELECT
oc.*,
COALESCE(er.exchange_rate_to_usd, 1.0) AS exchange_rate,
oc.order_amount * COALESCE(er.exchange_rate_to_usd, 1.0) AS order_amount_usd,
YEAR(oc.order_date) AS order_year,
MONTH(oc.order_date) AS order_month
FROM orders_with_customers oc
LEFT JOIN exchange_rates er
ON oc.currency_code = er.currency_code
AND oc.order_date = er.rate_date
),
-- Step 3: Lookup product (replaces LKP_PRODUCT)
orders_with_product AS (
SELECT
ou.*,
p.product_name,
p.product_category
FROM orders_with_usd ou
LEFT JOIN products p
ON ou.product_id = p.product_id
),
-- Step 4: Lookup region (replaces LKP_REGION)
final AS (
SELECT
op.*,
r.region_name,
-- Derived fields (replaces EXP_DERIVE)
CASE
WHEN op.order_amount_usd > 10000 THEN 'Y'
ELSE 'N'
END AS is_high_value
FROM orders_with_product op
LEFT JOIN regions r
ON op.region_code = r.region_code
)
SELECT * FROM final
Mapping each PowerCenter transformation to dbt¶
| PowerCenter transformation | dbt CTE | What it does |
|---|---|---|
| SQ_ORDERS (Source Qualifier + JOIN) | orders_with_customers | Joins orders to customers (INNER JOIN) |
| EXP_DERIVE (order_amount_usd) | orders_with_usd | Applies exchange rate; derives year/month |
| LKP_PRODUCT | orders_with_product | LEFT JOIN to product dimension |
| LKP_REGION | final | LEFT JOIN to region reference |
| EXP_DERIVE (is_high_value) | final | CASE expression for high-value flag |
Step 5: Create the mart model (20 min)¶
The mart model replaces the PowerCenter Aggregator and produces the final fact table.
-- models/marts/finance/fct_order_monthly.sql
-- Replaces: AGG_MONTHLY + UPD_INSERT + TGT_FACT_ORDERS
{{ config(
materialized='incremental',
unique_key=['customer_id', 'order_year', 'order_month', 'product_category', 'region_name'],
incremental_strategy='merge'
) }}
SELECT
-- Dimension keys
customer_id,
customer_name,
order_year,
order_month,
product_category,
region_name,
-- Measures (replaces AGG_MONTHLY)
SUM(order_amount_usd) AS total_order_amount_usd,
COUNT(order_id) AS order_count,
AVG(order_amount_usd) AS avg_order_amount_usd,
SUM(CASE WHEN is_high_value = 'Y' THEN 1 ELSE 0 END) AS high_value_order_count,
-- Metadata
CURRENT_TIMESTAMP AS loaded_at
FROM {{ ref('int_orders__enriched') }}
{% if is_incremental() %}
WHERE order_date > (SELECT MAX(order_date) FROM {{ this }})
{% endif %}
GROUP BY
customer_id,
customer_name,
order_year,
order_month,
product_category,
region_name
Step 6: Write tests (20 min)¶
Replace manual QA with automated dbt tests.
# models/marts/finance/_finance__models.yml
version: 2
models:
- name: fct_order_monthly
description: |
Monthly aggregated order facts by customer, product category, and region.
Replaces PowerCenter mapping m_ORDER_FACT.
columns:
- name: customer_id
description: Foreign key to dim_customer
tests:
- not_null
- relationships:
to: ref('stg_erp__customers')
field: customer_id
- name: order_year
tests:
- not_null
- accepted_values:
values:
[
"2020",
"2021",
"2022",
"2023",
"2024",
"2025",
"2026",
]
- name: total_order_amount_usd
tests:
- not_null
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100000000 # $100M max per customer-month
- name: order_count
tests:
- not_null
- dbt_expectations.expect_column_values_to_be_between:
min_value: 1
Custom test: reconciliation with PowerCenter¶
During parallel run, verify row counts and totals match:
-- tests/reconciliation/assert_fct_order_monthly_matches_powercenter.sql
-- Compare dbt output to PowerCenter output during parallel run
WITH dbt_totals AS (
SELECT
order_year,
order_month,
SUM(total_order_amount_usd) AS dbt_total,
SUM(order_count) AS dbt_count
FROM {{ ref('fct_order_monthly') }}
GROUP BY order_year, order_month
),
pc_totals AS (
SELECT
order_year,
order_month,
SUM(total_order_amount_usd) AS pc_total,
SUM(order_count) AS pc_count
FROM {{ source('powercenter', 'fact_order_monthly_pc') }}
GROUP BY order_year, order_month
)
SELECT
d.order_year,
d.order_month,
d.dbt_total,
p.pc_total,
ABS(d.dbt_total - p.pc_total) AS amount_diff,
d.dbt_count,
p.pc_count
FROM dbt_totals d
JOIN pc_totals p ON d.order_year = p.order_year AND d.order_month = p.order_month
WHERE ABS(d.dbt_total - p.pc_total) > 0.01 -- tolerance
OR d.dbt_count != p.pc_count
Step 7: Add documentation (10 min)¶
dbt auto-generates documentation from your YAML files.
# models/intermediate/_int__models.yml
version: 2
models:
- name: int_orders__enriched
description: |
Orders enriched with customer, product, region, and exchange rate data.
Converts order amounts to USD.
**PowerCenter origin:** Mapping `m_ORDER_FACT`, transformations SQ_ORDERS through LKP_REGION.
**Key business rules:**
- Orders joined to customers via INNER JOIN (only matched orders)
- Exchange rate applied from rate effective on order date
- Products and regions are LEFT JOINed (nulls allowed)
- High-value flag set at $10,000 USD threshold
columns:
- name: order_id
description: Unique order identifier from ERP
- name: order_amount_usd
description: Order amount converted to USD using daily exchange rate
- name: is_high_value
description: "'Y' if order_amount_usd > 10000, else 'N'"
Generate and serve documentation:
Step 8: Run and validate (20 min)¶
Run the models¶
# Run all models in dependency order
dbt run
# Output:
# Running 1 of 5: stg_erp__orders .............. OK
# Running 2 of 5: stg_erp__customers ........... OK
# Running 3 of 5: stg_ref__exchange_rates ...... OK
# Running 4 of 5: int_orders__enriched ......... OK
# Running 5 of 5: fct_order_monthly ............ OK
Run tests¶
# Run all tests
dbt test
# Output:
# Running 1 of 8: unique_fct_order_monthly_customer_id_order_year_order_month ... PASS
# Running 2 of 8: not_null_fct_order_monthly_customer_id ........................ PASS
# Running 3 of 8: relationships_fct_order_monthly_customer_id ................... PASS
# ...
Check source freshness¶
# Verify source data is fresh
dbt source freshness
# Output:
# Running freshness check: erp.orders ... PASS (last updated 2 hours ago)
Step 9: Deploy through CI/CD (15 min)¶
GitHub Actions workflow¶
# .github/workflows/dbt-deploy.yml
name: dbt Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
dbt-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install dbt-sqlserver
- run: dbt deps
- run: dbt build --target ci # runs models + tests
env:
DBT_PROFILES_DIR: .
dbt-deploy:
needs: dbt-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install dbt-sqlserver
- run: dbt deps
- run: dbt run --target prod
env:
DBT_PROFILES_DIR: .
Step 10: Comparison summary¶
| Aspect | PowerCenter m_ORDER_FACT | dbt equivalent |
|---|---|---|
| Files | 1 mapping (XML in repository) | 5 SQL files + 2 YAML files |
| Version control | Repository export | Git (full diff, branch, PR) |
| Testing | Manual QA after each run | 8+ automated tests, CI-integrated |
| Documentation | Separate wiki page | Auto-generated from YAML |
| Deployment | Repository export + import | git push triggers CI/CD |
| Debugging | PowerCenter session log | dbt logs + SQL profiler |
| Reusability | Mapplet (limited) | Macros (full Jinja templating) |
| Execution time | ~5 min (PowerCenter) | ~3 min (dbt incremental) |
Next steps¶
- Convert your next mapping using this same pattern
- Prioritize simple mappings first (Tier A in the assessment)
- Set up parallel run using the reconciliation test above
- Read: Tutorial: Workflow to ADF for orchestration
- Read: PowerCenter Migration Guide for the full transformation reference
Related resources¶
- PowerCenter Migration Guide -- Complete transformation mapping
- Tutorial: Workflow to ADF -- Orchestration tutorial
- Complete Feature Mapping -- All features mapped
- Best Practices -- Migration execution guidance
- Migration Playbook -- End-to-end migration guide
Last updated: 2026-04-30 Maintainers: CSA-in-a-Box core team