Skip to content

Migrating from dbt

The migration is the conversion path. Most teams adopting Rocky have a dbt project today; the day-one question is “how much rewriting?” The answer: little to none. Run rocky import-dbt against your existing repo, get a Rocky project on disk in seconds, and adopt the trust primitives — typed compile, contracts, column-level lineage, branches, cost — incrementally.

The wedge in five steps:

  1. Run rocky import-dbt. Jinja {{ ref() }} and {{ source() }} resolve to bare references; configs become TOML sidecars; the importer writes MIGRATION-NOTES.md listing anything that didn’t translate.
  2. Run rocky compile. First time through, expect real diagnostics — E013 on type mismatches, P002 on SELECT * blast radius, P001 on dialect-portability issues. Each one is something dbt couldn’t catch.
  3. Add contracts on the boundary models. [contract] required_columns = […], protected_columns = […]. From here, the column rename that quietly breaks 47 downstream models becomes an E010 in CI before it ships.
  4. Adopt rocky lineage-diff in PR review. Per-changed-column downstream blast radius. Drops into a PR comment. This is the moment your team stops reviewing changes blind.
  5. Turn on rocky preview cost. Per-PR cost projection — catch expensive plans before they ship instead of explaining them after.

Everything below is the mechanics. The strategic point is above: you don’t rewrite, you import and adopt.

Before starting, make sure you have:

  1. Rocky installed — see Installation
  2. An existing dbt project with models in a models/ directory
  3. Access to your warehouse credentials (Databricks host, HTTP path, token)

Rocky does not require dbt to be installed. The importer reads .sql files directly and parses Jinja expressions with its own regex-based extractor.

Walkthrough: end-to-end against a tiny dbt project

Section titled “Walkthrough: end-to-end against a tiny dbt project”

Before working through the per-step guide below, here is the full path against a real, runnable example. This mirrors the POC at examples/playground/pocs/06-developer-experience/03-import-dbt-validate/. Every command and snippet here was captured from that POC running against the current rocky build.

The POC ships a minimal dbt project with two models and one source:

dbt_project/
├── dbt_project.yml # name: ecommerce, profile: ecommerce, +materialized: table
└── models/
├── sources.yml # source 'raw' / table 'orders'
├── stg_orders.sql # {{ config(materialized='view') }} + {{ source('raw', 'orders') }}
└── fct_revenue.sql # {{ config(materialized='table') }} + {{ ref('stg_orders') }}

stg_orders.sql:

{{ config(materialized='view') }}
SELECT
order_id,
customer_id,
amount,
LOWER(status) AS status
FROM {{ source('raw', 'orders') }}
WHERE status != 'cancelled'

fct_revenue.sql:

{{ config(materialized='table') }}
SELECT
customer_id,
SUM(amount) AS total_revenue,
COUNT(*) AS order_count
FROM {{ ref('stg_orders') }}
GROUP BY customer_id

There is no profiles.yml in the POC and no compiled target/manifest.json, so this exercises the regex-based importer with no warehouse credentials.

Terminal window
rocky import-dbt \
--dbt-project dbt_project \
--output-dir imported \
--no-manifest \
--overwrite

Output (table mode):

dbt Migration Report
====================
Project: ecommerce
Method: regex
Models: 2 total
1 imported successfully (full_refresh: 2)
1 with warnings
Sources: 1 tables from 1 sources
1 mapped to Rocky
Output: 2 models translated, 0 seeds copied → imported
rocky.toml → imported/rocky.toml
MIGRATION-NOTES.md → imported/MIGRATION-NOTES.md
Warnings:
stg_orders: materialized='view' not supported in Rocky — using full_refresh

The importer produces a self-contained Rocky repo on disk. The full layout:

imported/
├── MIGRATION-NOTES.md
├── rocky.toml
└── models/
├── _defaults.toml
├── stg_orders.sql
├── stg_orders.toml
├── fct_revenue.sql
└── fct_revenue.toml

imported/rocky.toml (the importer wrote a DuckDB stub because no profiles.yml was found):

# rocky.toml — generated by `rocky import-dbt`
# Connection fields use ${VAR} env-var substitution. Set the env vars
# listed in MIGRATION-NOTES.md before running `rocky plan` + `rocky apply`.
# Default per-model target: catalog=warehouse, schema=main (see models/_defaults.toml).
[adapter]
type = "duckdb"
path = "warehouse.duckdb"
[pipeline.default]
type = "transformation"
models = "models/**"
[pipeline.default.target]
adapter = "default"

imported/models/_defaults.toml (directory-level target defaults):

[target]
catalog = "warehouse"
schema = "main"

imported/models/stg_orders.sql (Jinja resolved to bare references):

SELECT
order_id,
customer_id,
amount,
LOWER(status) AS status
FROM raw.orders
WHERE status != 'cancelled'

imported/models/stg_orders.toml (note the view → ephemeral mapping triggered by the warning above):

name = "stg_orders"
[strategy]
type = "ephemeral"
[target]
catalog = "warehouse"
schema = "main"
table = "stg_orders"
[[sources]]
catalog = "warehouse"
schema = "raw"
table = "orders"

imported/models/fct_revenue.toml:

name = "fct_revenue"
[strategy]
type = "full_refresh"
[target]
catalog = "warehouse"
schema = "main"
table = "fct_revenue"

imported/MIGRATION-NOTES.md is the canonical record of what didn’t translate — counts of skipped tests / macros, required env vars per adapter, and the explicit “Known limitations” list. Read it first.

The cheapest end-to-end check is to compile against the new repo:

Terminal window
cd imported
rocky compile --models models
✓ stg_orders (4 columns)
✓ fct_revenue (2 columns)
Compiled: 2 models, 0 errors, 0 warnings

Validate the generated rocky.toml:

Terminal window
rocky -c rocky.toml validate
ok Config syntax valid (v2 format)
ok adapter.default: duckdb (local)
ok pipeline.default: transformation / models='models/**'
ok 2 transformation models loaded
ok DAG valid (2 nodes, no cycles)
Validation complete.

A clean rocky compile + rocky validate is the success criterion. The POC’s run.sh stops here and then runs rocky validate-migration as an orthogonal cross-check that every dbt model has a matching Rocky model.

Running the emitted repo against real data

Section titled “Running the emitted repo against real data”

rocky -c rocky.toml plan followed by rocky apply <plan-id> will work once two preconditions are met, neither of which the importer can supply for you:

  1. Source data exists in the warehouse. The dbt project references {{ source('raw', 'orders') }}; the importer translates that to FROM raw.orders but does not create or populate the source. Load the source rows into the configured warehouse (warehouse.duckdb for the DuckDB stub, or your real Databricks/Snowflake target) before invoking rocky apply.
  2. Ephemeral models have a downstream-visible target. When the importer flattens viewephemeral, stg_orders is emitted as type = "ephemeral" but fct_revenue.sql still reads FROM stg_orders verbatim — the importer does not rewrite the downstream body to inline the CTE. For any model the importer flattened from view to ephemeral, you have two manual workarounds:
    • flip the strategy to full_refresh in the sidecar so stg_orders materialises as a real table; or
    • paste the upstream SELECT into the downstream model as a CTE.

Without (2), executing the pipeline ends with Catalog Error: Table with name stg_orders does not exist — the runtime expected the compiler to have already inlined the ephemeral CTE.

What translates cleanly today, and what doesn’t

Section titled “What translates cleanly today, and what doesn’t”

What the importer translates cleanly:

  • {{ ref('model') }} → bare table reference + sidecar depends_on
  • {{ source('s', 't') }} → fully qualified reference + sidecar [[sources]]
  • {{ config(materialized='table' \| 'incremental' \| 'view') }} → sidecar [strategy] block (view flattens to ephemeral after import — see caveat above)
  • {{ config(unique_key=...) }}merge strategy with unique_key array
  • {{ this }} → resolved against the sidecar [target]
  • is_incremental() branches → stripped (Rocky derives the watermark filter from [strategy])
  • dbt generic tests (unique, not_null, accepted_values, relationships) — translated column-by-column to [[tests]] blocks in the model sidecar (see Generic test mapping below)
  • Top-level dbt_project.yml — used to detect project name and seeds path
  • <dbt_project>/seeds/ → copied verbatim into <out>/seeds/
  • profiles.yml adapter type → mapped to a Rocky [adapter] block (DuckDB / Databricks / Snowflake / BigQuery), or a DuckDB stub when absent or unrecognised

By design, the importer does not translate the following — Rocky has no Jinja runtime, and these need a manual pass. Each item is detected and listed under “Known limitations” in MIGRATION-NOTES.md, with # TODO: dbt-jinja-not-translated comments above any leftover Jinja in emitted SQL:

  • dbt tests outside the canonical fourdbt_utils.*, dbt_expectations.*, project-defined generic tests, and model-level (non-column) tests are surfaced as structured warnings (UnsupportedTest) per occurrence and not stubbed in the emitted TOML. Rewrite as a Rocky expression test or a quality-pipeline check.
  • Singular tests in tests/ (custom SQL) — copy and rewrite manually.
  • dbt macros and dbt_packages/ — Rocky has no Jinja runtime; macro bodies do not expand.
  • {% if %}, {% for %}, {{ var() }} outside of is_incremental() — emitted verbatim with a TODO marker.
  • Unmapped materialized values (materialized_view, dynamic_table, seed) — flattened to full_refresh and listed in MIGRATION-NOTES.md.
  • Ephemeral CTE inlining into downstream model bodies — see the run-prerequisite note above.
  • Adapters Rocky does not natively support (e.g. Postgres, Redshift) — the generated repo stubs DuckDB so the project still loads; replace the [adapter] block once a Rocky adapter for the warehouse exists, or pass --target-adapter <kind> to skip detection.
  • Custom Jinja macros emitting SQL (e.g. {{ generate_schema_name() }}, dynamic UNION ALL macros) — surfaced as failed models with the macro name in the reason.
  • Python dbt models (.py files) — not SQL; rewrite manually.

The remainder of this guide walks each phase in detail (mapping profiles.yml to rocky.toml, handling unsupported Jinja, converting tests to contracts, cutover strategy). It is structured to read top-to-bottom as a migration playbook; the walkthrough above grounds it.

Run rocky import-dbt pointing at your dbt project directory:

Terminal window
rocky import-dbt --dbt-project ./my-dbt-project --output-dir ./rocky-models

This scans my-dbt-project/models/ for .sql files and produces Rocky sidecar files in ./rocky-models/:

rocky-models/
├── stg_orders.sql
├── stg_orders.toml
├── stg_customers.sql
├── stg_customers.toml
├── fct_orders.sql
├── fct_orders.toml
├── dim_customers.sql
└── dim_customers.toml

The importer handles these dbt patterns:

dbt PatternRocky Conversion
{{ ref('model_name') }}Bare table reference (model_name) + depends_on in TOML
{{ source('source_name', 'table') }}Fully qualified table reference (source_name.table)
{{ config(materialized='incremental', unique_key='id') }}[strategy] section in TOML
{{ this }}Target table reference from [target] in TOML
schema.yml column tests (unique, not_null, accepted_values, relationships)[[tests]] blocks in the model sidecar TOML — see Section 9 below

For programmatic use, request JSON via the global -o json flag:

Terminal window
rocky -o json import-dbt --dbt-project ./my-dbt-project --output-dir ./rocky-models
{
"version": "<rocky-version>",
"command": "import-dbt",
"imported": 42,
"warnings": 3,
"failed": 2,
"imported_models": ["stg_orders", "stg_customers", "fct_orders", "..."],
"warning_details": [
["stg_payments", "contains {{ var() }} — replaced with placeholder"]
],
"failed_details": [
["complex_macro_model", "unsupported Jinja: custom macro {{ generate_schema_name() }}"]
]
}

If your dbt project has a compiled manifest (target/manifest.json), Rocky uses it automatically for a more accurate import — all Jinja is pre-resolved in the compiled SQL.

To force or skip the manifest:

  • --manifest path/to/manifest.json — explicit manifest path
  • --no-manifest — skip manifest, use regex-based import

After import, review each generated model pair. Here is what a typical conversion looks like.

-- models/stg_orders.sql
{{ config(materialized='incremental', unique_key='order_id') }}
SELECT
order_id,
customer_id,
order_date,
total_amount,
_fivetran_synced
FROM {{ source('shopify', 'orders') }}
{% if is_incremental() %}
WHERE _fivetran_synced > (SELECT MAX(_fivetran_synced) FROM {{ this }})
{% endif %}

stg_orders.sql:

SELECT
order_id,
customer_id,
order_date,
total_amount,
_fivetran_synced
FROM shopify.orders

stg_orders.toml:

name = "stg_orders"
depends_on = []
[strategy]
type = "incremental"
unique_key = ["order_id"]
timestamp_column = "_fivetran_synced"
[target]
catalog = "warehouse"
schema = "staging"
table = "stg_orders"
[[sources]]
catalog = "shopify"
schema = "default"
table = "orders"

Notice several changes:

  • The {{ config() }} block became the [strategy] section
  • The {{ source() }} call became a fully qualified table reference
  • The {% if is_incremental() %} block was removed — Rocky handles incremental logic based on the strategy config and watermark column
  • The {{ this }} reference was removed — Rocky generates the target table reference from [target]

The importer cannot convert all Jinja patterns. It produces warnings and failures for cases it cannot handle automatically.

PatternImporter BehaviorManual Fix
{{ var('some_var') }}Replaced with a TODO placeholderReplace with a hardcoded value or ${VAR} / ${VAR:-default} substitution. Env vars resolve in rocky.toml, in models/_defaults.toml, and in per-model sidecar .toml files — letting an orchestrator inject [target] values per asset without templating.
{% if target.name == 'prod' %}Stripped, keeping the default branchRemove environment branching or use separate rocky.toml files per environment
{% set ... %} variable assignmentsStripped with a warningInline the value or refactor the query
PatternReasonManual Fix
Custom Jinja macros ({{ generate_schema_name() }})Rocky cannot interpret custom macrosRewrite the SQL without the macro
{% for ... %} loops generating SQLDynamic SQL generation not supportedWrite out the SQL explicitly or use a CTE
{% macro ... %} definitionsRocky uses pure SQL, not macrosConvert shared logic to CTEs or separate models
Python dbt models (.py files)Not SQLRewrite in SQL

For each failed model, check the error message and rewrite the SQL manually. Most Jinja macros exist to work around SQL limitations that Rocky handles differently (incremental logic, schema naming, environment branching).

Create a rocky.toml in your project root. Rocky uses named adapters plus named pipelines — define one adapter for the source and one for the warehouse, then a pipeline that wires them together. If you were using dbt with Databricks, your settings map directly:

[adapter.prod]
type = "databricks"
host = "${DATABRICKS_HOST}"
http_path = "${DATABRICKS_HTTP_PATH}"
token = "${DATABRICKS_TOKEN}"
[pipeline.bronze]
type = "replication"
strategy = "incremental"
timestamp_column = "_fivetran_synced"
[pipeline.bronze.source]
adapter = "prod"
catalog = "raw_catalog"
[pipeline.bronze.source.schema_pattern]
prefix = ""
separator = "__"
components = ["source"]
[pipeline.bronze.target]
adapter = "prod"
catalog_template = "warehouse"
schema_template = "staging"
[pipeline.bronze.execution]
concurrency = 8
[state]
backend = "local"

Set the environment variables:

Terminal window
export DATABRICKS_HOST="your-workspace.cloud.databricks.com"
export DATABRICKS_HTTP_PATH="/sql/1.0/warehouses/abc123"
export DATABRICKS_TOKEN="dapi..."
dbt (profiles.yml / dbt_project.yml)Rocky (rocky.toml)
host[adapter.prod] host
http_path[adapter.prod] http_path
token[adapter.prod] token
catalog[pipeline.<name>.target] catalog_template
schema[pipeline.<name>.target] schema_template
threads[pipeline.<name>.execution] concurrency

Run the compiler to type-check all imported models:

Terminal window
rocky compile --models ./rocky-models

The compiler will:

  • Resolve depends_on references into a DAG
  • Type-check column references across model boundaries
  • Report any unresolved references, type mismatches, or missing dependencies
✓ stg_orders (5 columns)
✓ stg_customers (4 columns)
✓ fct_orders (7 columns)
✗ fct_revenue
error[E0002]: unresolved reference 'stg_payments'
--> rocky-models/fct_revenue.sql:8:6
|
8 | FROM stg_payments p
| ^^^^^^^^^^^^ model not found in project
|
= hint: add 'stg_payments' to depends_on in fct_revenue.toml
Compiled: 4 models, 1 error, 0 warnings

Fix each error until compilation succeeds. Common issues after import:

  • Missing depends_on: The importer may miss dependencies that were implicit in dbt (e.g., via {{ ref() }} in a macro). Add them to the TOML config.
  • Unqualified table references: Rocky resolves bare table names against the project’s models. If a query references a warehouse table directly, use the fully qualified name (catalog.schema.table).
  • Type mismatches: Rocky infers types from upstream models. If a column is used in an incompatible context, the compiler reports it.

Once compilation passes, run local tests using DuckDB:

Terminal window
rocky test --models ./rocky-models
Testing 4 models...
All 4 models passed
Result: 4 passed, 0 failed

Tests execute each model’s SQL against DuckDB in dependency order. This catches SQL syntax errors and runtime issues without needing a warehouse connection.

Compare the dbt and Rocky outputs side by side:

Terminal window
rocky validate-migration --dbt-project ~/my-dbt-project

This compiles both projects and compares schemas, column types, and optionally sample data.

Before switching production traffic, run both tools side by side and compare outputs.

Terminal window
rocky plan --filter tenant=acme

This shows the SQL Rocky will generate for each model. Compare it against dbt compile output for the same models.

Add a test pipeline to your rocky.toml that points at a sandbox catalog and reuses the same adapter:

[pipeline.bronze_test]
type = "replication"
strategy = "full_refresh"
[pipeline.bronze_test.source]
adapter = "prod"
[pipeline.bronze_test.source.schema_pattern]
prefix = ""
separator = "__"
components = ["source"]
[pipeline.bronze_test.target]
adapter = "prod"
catalog_template = "test_warehouse"
schema_template = "staging"

Run the test pipeline:

Terminal window
plan_id=$(rocky plan --pipeline bronze_test --filter tenant=acme --output json | jq -r .plan_id)
rocky apply "$plan_id"

Then compare row counts, column types, and data values between the dbt-generated tables and Rocky-generated tables.

9. Convert dbt Tests to Rocky Tests and Contracts

Section titled “9. Convert dbt Tests to Rocky Tests and Contracts”

rocky import-dbt translates two kinds of dbt tests onto Rocky sidecars:

  • The four canonical column-level generic tests (unique, not_null, accepted_values, relationships) — emitted as [[tests]] blocks on each model sidecar.
  • Unit tests from manifest.unit_tests (dbt 1.8+) — emitted as [[test]] blocks on the matching model sidecar. Manifest-only; the regex path does not see unit tests.

Anything else — column-level type/nullability contracts, project-defined generics, singular tests — still needs a manual step.

For these dbt tests in schema.yml (dbt 1.7+ also accepts data_tests:, which the importer reads as a synonym for tests:):

models:
- name: fct_orders
columns:
- name: order_id
tests:
- unique
- not_null
- name: status
tests:
- accepted_values:
values: ['completed', 'pending', 'cancelled']
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: customer_id

The importer emits [[tests]] blocks directly into models/fct_orders.toml:

[[tests]]
type = "unique"
column = "order_id"
[[tests]]
type = "not_null"
column = "order_id"
[[tests]]
type = "accepted_values"
values = ["completed", "pending", "cancelled"]
column = "status"
[[tests]]
type = "relationships"
to_table = "warehouse.main.dim_customers"
to_column = "customer_id"
column = "customer_id"

relationships.to: ref('m') resolves to the fully-qualified Rocky table via the importer’s name → (catalog, schema) lookup over the imported model set; cross-project refs fall back to the importer defaults. These tests run as part of rocky test against the materialised tables.

dbt TestRocky [[tests]]
not_nulltype = "not_null" + column
uniquetype = "unique" + column
accepted_valuestype = "accepted_values" + values = [...] + column
relationshipstype = "relationships" + to_table + to_column + column

Anything else (dbt_utils.*, dbt_expectations.*, project-defined generics, model-level tests) is surfaced as an UnsupportedTest warning with the model, column, and test name. Rewrite those as a Rocky expression test or a quality-pipeline check — the importer does not stub them in the emitted TOML.

If your dbt project has compiled to a manifest.json and declares unit_tests: blocks (dbt 1.8+), rocky import-dbt --manifest target/manifest.json walks manifest.unit_tests and emits each entry as a [[test]] block in the matching model’s sidecar TOML. ref('upstream_model') / source('s', 't') wrappers on given.input are stripped to bare references.

models/fct_orders.yml
unit_tests:
- name: stamps_status_when_completed
model: fct_orders
given:
- input: ref('stg_orders')
rows:
- { order_id: 1, status: 'completed' }
expect:
format: dict
rows:
- { order_id: 1, status: 'completed' }
# Rocky: models/fct_orders.toml — emitted by `rocky import-dbt`
[[test]]
name = "stamps_status_when_completed"
[[test.given]]
ref = "stg_orders"
[[test.given.rows]]
order_id = 1
status = "completed"
[test.expect]
ordered = false
[[test.expect.rows]]
order_id = 1
status = "completed"

The importer also surfaces three new counters on the --output json payload and in MIGRATION-NOTES.mdunit_tests_found, unit_tests_converted, unit_tests_skipped — plus two warning variants:

  • OrphanUnitTest — the unit test targets a model the importer didn’t pick up. Skipped and counted as skipped.
  • UnsupportedUnitTestFormatexpect.format = "csv" / "sql", fixture references, or any other shape Rocky’s UnitTestDef doesn’t yet model. Skipped.

CSV / SQL fixtures and overrides: blocks are deferred until Rocky’s runtime test runner grows the matching surface; emitted [[test]] blocks deserialize through Rocky today but aren’t yet wired into rocky test execution.

If you want compile-time guarantees on column types and nullability — beyond the row-level test runtime — add a .contract.toml alongside the model. Contracts are not autogenerated from dbt; write them for the models that need the extra rigour:

contracts/stg_orders.contract.toml
[[columns]]
name = "order_id"
type = "Int64"
nullable = false
[[columns]]
name = "customer_id"
type = "Int64"
nullable = false
[[columns]]
name = "total_amount"
type = "Decimal"
nullable = false
[rules]
required = ["order_id", "customer_id", "total_amount"]
protected = ["order_id"]
Terminal window
rocky compile --models ./rocky-models --contracts ./contracts

The compiler validates that every model satisfies its contract at compile time. If a model’s output does not match the contract (missing column, wrong type, removed protected column), compilation fails.

Rocky’s AI layer uses intent descriptions to understand what each model does. Adding intent to your migrated models enables ai-sync (automatic schema change propagation) and ai-test (test generation).

Generate intent for all models at once:

Terminal window
export ANTHROPIC_API_KEY="sk-ant-..."
rocky ai-explain --all --save --models ./rocky-models

This reads each model’s SQL, generates a plain-English description, and saves it to the TOML config:

# stg_orders.toml (after ai-explain --save)
name = "stg_orders"
intent = "Stage raw Shopify orders with order_id, customer, date, and amount columns"
depends_on = []
[strategy]
type = "incremental"
timestamp_column = "_fivetran_synced"
[target]
catalog = "warehouse"
schema = "staging"
table = "stg_orders"

You do not need to migrate everything at once. Here is a recommended phased approach:

  1. Run rocky import-dbt to convert all models
  2. Fix compilation errors
  3. Add contracts for critical models
  4. Run rocky ci in your CI pipeline alongside dbt
  1. Run rocky test locally to validate SQL execution
  2. Compare Rocky output against dbt output on a test catalog
  3. Add rocky compile as a required check on PRs

Phase 3: Production cutover (per model group)

Section titled “Phase 3: Production cutover (per model group)”
  1. Start with leaf models (no downstream dependents)
  2. Switch their execution from dbt to Rocky
  3. Monitor output parity for 1-2 weeks
  4. Move upstream to the next layer
  1. Migrate all models to Rocky
  2. Remove dbt from CI/CD
  3. Set up Dagster integration for orchestration

During migration, you can run both tools on the same project by keeping the dbt models/ directory and the Rocky rocky-models/ directory separate. Your CI pipeline can run both:

# GitHub Actions example
steps:
- name: dbt compile
run: dbt compile
- name: Rocky compile
run: rocky compile --models ./rocky-models --contracts ./contracts
- name: Rocky test
run: rocky test --models ./rocky-models

Once Rocky covers all models, remove the dbt steps.

Keeping dbt packages without converting them

Section titled “Keeping dbt packages without converting them”

You don’t need to convert everything. dbt packages like fivetran/facebook_ads or fivetran/stripe produce tables in your warehouse that Rocky can reference directly as external sources. Rocky’s resolver automatically classifies schema-qualified table references (dbt_fivetran.stg_facebook_ads__ad_history) as external — they appear in lineage but do not create DAG dependencies.

This lets you keep vendor-maintained staging packages in dbt and write your custom analytics in Rocky. See Using Rocky with dbt Packages for the full walkthrough.

The importer names models after the SQL file’s stem (e.g., stg_orders.sql becomes stg_orders). If your dbt project uses custom model names via {{ config(alias='...') }}, the depends_on references may not match. Check each TOML file’s name field and update depends_on references accordingly.

Incremental models do not pick up the right watermark

Section titled “Incremental models do not pick up the right watermark”

Rocky uses the timestamp_column from the [strategy] section, not Jinja logic. Make sure the column name matches what your data actually contains (e.g., _fivetran_synced, updated_at).

dbt uses {{ target.name }} for environment branching. Rocky does not have environment-specific SQL — use separate rocky.toml files per environment instead:

Terminal window
rocky compile --config pipeline.prod.toml --models ./rocky-models
rocky compile --config pipeline.dev.toml --models ./rocky-models

If your dbt project relies on macros that generate SQL (e.g., a union_all macro that combines tables), rewrite the SQL explicitly. In most cases, a CTE with UNION ALL is clearer and more maintainable:

WITH all_orders AS (
SELECT * FROM raw_catalog.us_west_shopify.orders
UNION ALL
SELECT * FROM raw_catalog.eu_central_shopify.orders
)
SELECT
order_id,
customer_id,
total_amount
FROM all_orders