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:
- Run
rocky import-dbt. Jinja{{ ref() }}and{{ source() }}resolve to bare references; configs become TOML sidecars; the importer writesMIGRATION-NOTES.mdlisting anything that didn’t translate. - Run
rocky compile. First time through, expect real diagnostics —E013on type mismatches,P002onSELECT *blast radius,P001on dialect-portability issues. Each one is something dbt couldn’t catch. - Add contracts on the boundary models.
[contract] required_columns = […],protected_columns = […]. From here, the column rename that quietly breaks 47 downstream models becomes anE010in CI before it ships. - Adopt
rocky lineage-diffin PR review. Per-changed-column downstream blast radius. Drops into a PR comment. This is the moment your team stops reviewing changes blind. - 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.
Prerequisites
Section titled “Prerequisites”Before starting, make sure you have:
- Rocky installed — see Installation
- An existing dbt project with models in a
models/directory - 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.
Setup: the input dbt project
Section titled “Setup: the input dbt project”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 statusFROM {{ source('raw', 'orders') }}WHERE status != 'cancelled'fct_revenue.sql:
{{ config(materialized='table') }}
SELECT customer_id, SUM(amount) AS total_revenue, COUNT(*) AS order_countFROM {{ ref('stg_orders') }}GROUP BY customer_idThere is no profiles.yml in the POC and no compiled target/manifest.json, so this exercises the regex-based importer with no warehouse credentials.
Run the importer
Section titled “Run the importer”rocky import-dbt \ --dbt-project dbt_project \ --output-dir imported \ --no-manifest \ --overwriteOutput (table mode):
dbt Migration Report====================
Project: ecommerceMethod: 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_refreshWhat gets emitted
Section titled “What gets emitted”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.tomlimported/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 statusFROM raw.ordersWHERE 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.
Verify the emitted repo loads
Section titled “Verify the emitted repo loads”The cheapest end-to-end check is to compile against the new repo:
cd importedrocky compile --models models ✓ stg_orders (4 columns) ✓ fct_revenue (2 columns) Compiled: 2 models, 0 errors, 0 warningsValidate the generated rocky.toml:
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:
- Source data exists in the warehouse. The dbt project references
{{ source('raw', 'orders') }}; the importer translates that toFROM raw.ordersbut does not create or populate the source. Load the source rows into the configured warehouse (warehouse.duckdbfor the DuckDB stub, or your real Databricks/Snowflake target) before invokingrocky apply. - Ephemeral models have a downstream-visible target. When the importer flattens
view→ephemeral,stg_ordersis emitted astype = "ephemeral"butfct_revenue.sqlstill readsFROM stg_ordersverbatim — the importer does not rewrite the downstream body to inline the CTE. For any model the importer flattened fromviewtoephemeral, you have two manual workarounds:- flip the strategy to
full_refreshin the sidecar sostg_ordersmaterialises as a real table; or - paste the upstream SELECT into the downstream model as a CTE.
- flip the strategy to
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 + sidecardepends_on{{ source('s', 't') }}→ fully qualified reference + sidecar[[sources]]{{ config(materialized='table' \| 'incremental' \| 'view') }}→ sidecar[strategy]block (viewflattens toephemeralafter import — see caveat above){{ config(unique_key=...) }}→mergestrategy withunique_keyarray{{ 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.ymladapter 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 four —
dbt_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 Rockyexpressiontest 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 ofis_incremental()— emitted verbatim with a TODO marker.- Unmapped
materializedvalues (materialized_view,dynamic_table,seed) — flattened tofull_refreshand listed inMIGRATION-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() }}, dynamicUNION ALLmacros) — surfaced as failed models with the macro name in the reason. - Python dbt models (
.pyfiles) — 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.
1. Import the dbt Project
Section titled “1. Import the dbt Project”Run rocky import-dbt pointing at your dbt project directory:
rocky import-dbt --dbt-project ./my-dbt-project --output-dir ./rocky-modelsThis 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.tomlWhat the importer converts
Section titled “What the importer converts”The importer handles these dbt patterns:
| dbt Pattern | Rocky 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 |
JSON output
Section titled “JSON output”For programmatic use, request JSON via the global -o json flag:
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() }}"] ]}Manifest Fast Path
Section titled “Manifest Fast Path”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
2. Review the Imported Models
Section titled “2. Review the Imported Models”After import, review each generated model pair. Here is what a typical conversion looks like.
Before (dbt)
Section titled “Before (dbt)”-- models/stg_orders.sql{{ config(materialized='incremental', unique_key='order_id') }}
SELECT order_id, customer_id, order_date, total_amount, _fivetran_syncedFROM {{ source('shopify', 'orders') }}
{% if is_incremental() %}WHERE _fivetran_synced > (SELECT MAX(_fivetran_synced) FROM {{ this }}){% endif %}After (Rocky)
Section titled “After (Rocky)”stg_orders.sql:
SELECT order_id, customer_id, order_date, total_amount, _fivetran_syncedFROM shopify.ordersstg_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]
3. Handle Unsupported Jinja
Section titled “3. Handle Unsupported Jinja”The importer cannot convert all Jinja patterns. It produces warnings and failures for cases it cannot handle automatically.
Common warnings
Section titled “Common warnings”| Pattern | Importer Behavior | Manual Fix |
|---|---|---|
{{ var('some_var') }} | Replaced with a TODO placeholder | Replace 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 branch | Remove environment branching or use separate rocky.toml files per environment |
{% set ... %} variable assignments | Stripped with a warning | Inline the value or refactor the query |
Common failures
Section titled “Common failures”| Pattern | Reason | Manual Fix |
|---|---|---|
Custom Jinja macros ({{ generate_schema_name() }}) | Rocky cannot interpret custom macros | Rewrite the SQL without the macro |
{% for ... %} loops generating SQL | Dynamic SQL generation not supported | Write out the SQL explicitly or use a CTE |
{% macro ... %} definitions | Rocky uses pure SQL, not macros | Convert shared logic to CTEs or separate models |
Python dbt models (.py files) | Not SQL | Rewrite 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).
4. Configure rocky.toml
Section titled “4. Configure rocky.toml”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:
export DATABRICKS_HOST="your-workspace.cloud.databricks.com"export DATABRICKS_HTTP_PATH="/sql/1.0/warehouses/abc123"export DATABRICKS_TOKEN="dapi..."Mapping dbt config to Rocky
Section titled “Mapping dbt config to Rocky”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 |
5. Compile the Imported Models
Section titled “5. Compile the Imported Models”Run the compiler to type-check all imported models:
rocky compile --models ./rocky-modelsThe compiler will:
- Resolve
depends_onreferences 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 warningsFix 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.
6. Run Tests Locally
Section titled “6. Run Tests Locally”Once compilation passes, run local tests using DuckDB:
rocky test --models ./rocky-modelsTesting 4 models...
All 4 models passed
Result: 4 passed, 0 failedTests execute each model’s SQL against DuckDB in dependency order. This catches SQL syntax errors and runtime issues without needing a warehouse connection.
7. Validate the Migration
Section titled “7. Validate the Migration”Compare the dbt and Rocky outputs side by side:
rocky validate-migration --dbt-project ~/my-dbt-projectThis compiles both projects and compares schemas, column types, and optionally sample data.
8. Compare Output with dbt
Section titled “8. Compare Output with dbt”Before switching production traffic, run both tools side by side and compare outputs.
Preview Rocky’s SQL
Section titled “Preview Rocky’s SQL”rocky plan --filter tenant=acmeThis shows the SQL Rocky will generate for each model. Compare it against dbt compile output for the same models.
Run on a test catalog
Section titled “Run on a test catalog”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:
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.
Generic test mapping
Section titled “Generic test mapping”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_idThe 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 Test | Rocky [[tests]] |
|---|---|
not_null | type = "not_null" + column |
unique | type = "unique" + column |
accepted_values | type = "accepted_values" + values = [...] + column |
relationships | type = "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.
dbt unit tests (manifest path)
Section titled “dbt unit tests (manifest path)”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.
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 = 1status = "completed"
[test.expect]ordered = false
[[test.expect.rows]]order_id = 1status = "completed"The importer also surfaces three new counters on the --output json payload and in MIGRATION-NOTES.md — unit_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.UnsupportedUnitTestFormat—expect.format = "csv"/"sql", fixture references, or any other shape Rocky’sUnitTestDefdoesn’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.
Column-level contracts (manual)
Section titled “Column-level contracts (manual)”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:
[[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"]Compile with contracts
Section titled “Compile with contracts”rocky compile --models ./rocky-models --contracts ./contractsThe 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.
10. Add Intent Descriptions
Section titled “10. Add Intent Descriptions”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:
export ANTHROPIC_API_KEY="sk-ant-..."rocky ai-explain --all --save --models ./rocky-modelsThis 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"11. Incremental Adoption Strategy
Section titled “11. Incremental Adoption Strategy”You do not need to migrate everything at once. Here is a recommended phased approach:
Phase 1: Import and compile
Section titled “Phase 1: Import and compile”- Run
rocky import-dbtto convert all models - Fix compilation errors
- Add contracts for critical models
- Run
rocky ciin your CI pipeline alongside dbt
Phase 2: Test parity
Section titled “Phase 2: Test parity”- Run
rocky testlocally to validate SQL execution - Compare Rocky output against dbt output on a test catalog
- Add
rocky compileas a required check on PRs
Phase 3: Production cutover (per model group)
Section titled “Phase 3: Production cutover (per model group)”- Start with leaf models (no downstream dependents)
- Switch their execution from dbt to Rocky
- Monitor output parity for 1-2 weeks
- Move upstream to the next layer
Phase 4: Full migration
Section titled “Phase 4: Full migration”- Migrate all models to Rocky
- Remove dbt from CI/CD
- Set up Dagster integration for orchestration
Running dbt and Rocky side by side
Section titled “Running dbt and Rocky side by side”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 examplesteps: - 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-modelsOnce 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.
Troubleshooting
Section titled “Troubleshooting””model not found” after import
Section titled “”model not found” after import”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).
Environment-specific logic
Section titled “Environment-specific logic”dbt uses {{ target.name }} for environment branching. Rocky does not have environment-specific SQL — use separate rocky.toml files per environment instead:
rocky compile --config pipeline.prod.toml --models ./rocky-modelsrocky compile --config pipeline.dev.toml --models ./rocky-modelsMacros that generate SQL dynamically
Section titled “Macros that generate SQL dynamically”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_amountFROM all_orders