Modeling Commands
Commands for working with Rocky’s SQL models: compile, lineage, local tests, and CI.
rocky compile
Section titled “rocky compile”Compile models: resolve dependencies, type-check SQL, validate data contracts, and build the semantic graph.
rocky compile [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing .sql and .toml model files. |
--contracts <PATH> |
PathBuf |
Directory containing data contract definitions. | |
--model <NAME> |
string |
Restrict the reported result and exit status to one exact model name — whether that model’s own source is valid, not whether its upstreams can be rebuilt. The full project is still loaded and compile-checked internally for dependency and type context. | |
--expand-macros |
bool |
false |
Expand macros from macros/ and include the expanded SQL in the output. |
--target-dialect <DIALECT> |
dbx | sf | bq | duckdb |
Run the P001 dialect-portability lint against the chosen target. Non-portable constructs emit error-severity diagnostics. Precedence: flag > [portability] target_dialect in rocky.toml > unset. See Portability linting. |
|
--with-seed |
bool |
false |
Execute data/seed.sql against an in-memory DuckDB and use its information_schema as the source-of-truth for raw source schemas. Turns leaf .sql models from Unknown columns into concrete types. Requires the duckdb feature (enabled by default in the shipped binary). |
Examples
Section titled “Examples”Compile all models:
rocky compile{ "version": "1.11.0", "command": "compile", "models": 14, "execution_layers": 4, "has_errors": false, "diagnostics": [], "compile_timings": { "load_ms": 8, "resolve_ms": 2, "typecheck_ms": 42 }, "models_detail": [ { "name": "fct_revenue", "strategy": { "type": "full_refresh" }, "target": { "catalog": "acme_warehouse", "schema": "gold", "table": "fct_revenue" }, "freshness": { "max_lag_seconds": 86400, "time_column": "order_date", "severity": "warning" }, "contract_source": "auto", "incrementality_hint": { "is_candidate": true, "recommended_column": "order_date", "confidence": "medium", "signals": ["column name 'order_date' ends with '_date' (timestamp pattern)"] }, "cost_hint": { "estimated_rows": 10000, "estimated_bytes": 2560000, "estimated_cost_usd": 0.0000228, "confidence": "high" }, "depends_on": ["stg_orders", "stg_refunds"], "tags": { "domain": "finance", "tier": "gold", "owner": "analytics", "region": "emea" } } /* one entry per model */ ]}models_detail carries each compiled model’s declarative shape. Four fields are always there: name, the materialization strategy (wire form {"type": "..."}), the target coordinates, and the direct depends_on list.
Four more appear only when they apply:
freshness— the model’s freshness expectation.contract_source—"auto"for a sibling.contract.toml,"explicit"for one passed via--contracts.incrementality_hint— set on afull_refreshmodel that has a monotonic-looking column.cost_hint— set when the upstream statistics support an estimate.
The tags object holds the model’s [tags] merged over any config-group baseline, with the sidecar winning. Rocky omits an empty tags, an empty depends_on, and any absent optional field.
Compile a single model with contracts, showing a warning diagnostic:
rocky compile --model fct_revenue --contracts contracts/{ "version": "1.6.0", "command": "compile", "models": 1, "execution_layers": 1, "has_errors": false, "diagnostics": [ { "severity": "warning", "code": "W010", "model": "fct_revenue", "message": "column 'discount_pct' not declared in contract", "span": null, "suggestion": null } ], "compile_timings": { "load_ms": 5, "resolve_ms": 1, "typecheck_ms": 12 }}Model selection is exact: an unknown name is an error. Rocky still loads and compile-checks the full project internally so the selected model has dependency and type context, but the visible counts, layers, model details, diagnostics, and failure state describe only the selected model.
What the exit status does and does not cover. The selector filters diagnostics by exact attribution — a diagnostic is reported only when its owning model name equals the selector — and the exit status follows that filtered set. The dividing line is how a problem is reported, not what kind of problem it is:
- A hard compilation failure — one that aborts compilation rather than emitting a diagnostic, such as a model whose SQL cannot be parsed — fails the command regardless of the selector, because compilation never gets far enough to filter anything. Failures during semantic-graph construction and contract loading behave the same way.
- An error diagnostic attributed to another model is filtered out, so the
selected model is reported clean. This holds even though that other model
genuinely fails to compile:
rocky runclassifies such a model as a compile error and excludes it from execution. Selecting a model therefore tells you nothing about whether its upstreams compile.
Two consequences worth knowing:
- Not every diagnostic names a model you can select. Import-level
diagnostics (
E033,E034,W012) carry the import name, andW011carries a contract name that need not correspond to a model at all. Because--modelrequires a real model name, those cannot be surfaced by selecting anything — run without a selector to see them. - A diagnostic can be attributed to more than one model. A target collision
(
E036) attaches an error to every participating model, so selecting any one of them reports it.
To check whether a model can actually be built, compile without a selector.
rocky run --model builds only the selected model and needs --defer to
resolve references to upstreams you did not build; without it the SQL is left
unchanged and the run succeeds only if the reference already resolves in the
active namespace.
Note that under --model, execution_layers counts only the layers containing
the selected model — so it reports 1, not the model’s depth in the DAG and not
how many layers rebuilding it would take.
Every diagnostic carries a severity ("error", "warning", "info"), a code (E### errors, W### warnings, P### portability lints, or V### validation), the owning model, and (when the compiler can locate it) a span and suggestion.
Compile models from a non-default directory:
rocky compile --models src/transformations/Reject SQL that won’t run on BigQuery (P001 dialect-portability lint):
rocky compile --target-dialect bq{ "version": "1.11.0", "command": "compile", "has_errors": true, "diagnostics": [ { "severity": "error", "code": "P001", "model": "fct_revenue", "message": "NVL is not portable to BigQuery (supported by: Snowflake, Databricks)", "span": { "file": "models/fct_revenue.sql", "line": 1, "col": 1 }, "suggestion": "use COALESCE" } ]}The --target-dialect flag and the [portability] config block (see Configuration) drive the same check. Project-wide allow-lists and per-model -- rocky-allow: … pragmas exempt specific constructs. See Portability linting.
Compile with seeded source schemas so leaf .sql models pick up real types:
rocky compile --with-seed--with-seed looks for data/seed.sql relative to the project root (one level up from --models). It opens an in-memory DuckDB, runs the seed, and feeds the resulting information_schema.columns back into the compiler so downstream incrementality and type-inference get concrete types instead of RockyType::Unknown. Bails if data/seed.sql is missing or fails to execute.
Related Commands
Section titled “Related Commands”rocky lineage– trace column-level dependenciesrocky test– run local model testsrocky ci– compile + test in one steprocky serve– expose the semantic graph via HTTP
rocky lineage
Section titled “rocky lineage”Show column-level lineage for a model, tracing how each output column is derived from upstream sources.
rocky lineage <target> [flags]Arguments
Section titled “Arguments”| Argument | Type | Default | Description |
|---|---|---|---|
target |
string |
(required) | Model name, or model.column to trace a specific column. |
| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing model files. |
--column <NAME> |
string |
Specific column to trace (alternative to model.column syntax). |
|
--format <FORMAT> |
string |
Output format. Use dot for Graphviz DOT output. |
|
--downstream |
bool |
false |
Walk the column-level graph forward (consumers) instead of backward (sources). Mutually exclusive with --upstream. |
--upstream |
bool |
true |
Walk the column-level graph backward (sources). Default; the flag exists for explicitness in scripted callers. |
Examples
Section titled “Examples”Show lineage for a model. Returns the model’s columns, its upstream and downstream models, and every column-level edge with the transform kind:
rocky lineage fct_revenue{ "version": "1.6.0", "command": "lineage", "model": "fct_revenue", "columns": [ { "name": "customer_id" }, { "name": "revenue_amount" } ], "upstream": ["stg_orders", "stg_refunds"], "downstream": [], "edges": [ { "source": { "model": "stg_orders", "column": "customer_id" }, "target": { "model": "fct_revenue", "column": "customer_id" }, "transform": "direct" }, { "source": { "model": "stg_orders", "column": "total_amount" }, "target": { "model": "fct_revenue", "column": "revenue_amount" }, "transform": "expression" }, { "source": { "model": "stg_refunds", "column": "refund_amount" }, "target": { "model": "fct_revenue", "column": "revenue_amount" }, "transform": "expression" } ]}Tracing a single column returns a flat trace shape instead. Use either --column or model.column syntax:
rocky lineage fct_revenue --column revenue_amount{ "version": "1.6.0", "command": "lineage", "model": "fct_revenue", "column": "revenue_amount", "direction": "upstream", "trace": [ /* LineageEdgeRecord entries, same shape as edges above */ ]}Trace a specific column and export as Graphviz DOT:
rocky lineage fct_revenue --column revenue_amount --format dotdigraph lineage { rankdir=LR; "stg_orders.total_amount" -> "fct_revenue.revenue_amount"; "stg_refunds.refund_amount" -> "fct_revenue.revenue_amount";}Use the dot syntax shorthand:
rocky lineage fct_revenue.revenue_amount --format dot | dot -Tpng -o lineage.pngWalk downstream to see every consumer of a column (the answer to “what breaks if I change this?”):
rocky lineage stg_orders.customer_id --downstream{ "version": "1.11.0", "command": "lineage", "model": "stg_orders", "column": "customer_id", "direction": "downstream", "trace": [ { "source": { "model": "stg_orders", "column": "customer_id" }, "target": { "model": "fct_revenue", "column": "customer_id" }, "transform": "direct" }, { "source": { "model": "fct_revenue", "column": "customer_id" }, "target": { "model": "mart_ltv", "column": "customer_id" }, "transform": "direct" } ]}Upstream output has "direction": "upstream" (the default shape, unchanged). The transitive walker is backed by an edges_by_source_model index so cost scales with fan-out rather than total edges.
Related Commands
Section titled “Related Commands”rocky compile– build the semantic graph that lineage readsrocky ai-explain– generate natural language descriptions of model logic
rocky catalog
Section titled “rocky catalog”Emit a project-wide column-level lineage snapshot to disk. Walks every model in the SemanticGraph and serializes the result as persisted catalog artifacts (a catalog.json front door plus edges.parquet / assets.parquet) so downstream consumers (BI tools, governance dashboards, AI review bots) can query lineage without re-invoking the engine.
rocky catalog [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing .sql and .toml model files. |
--out <PATH> |
PathBuf |
.rocky/catalog/ |
Output directory. catalog.json is written to <out>/catalog.json; the Parquet artifacts to <out>/edges.parquet and <out>/assets.parquet. |
--format <FORMAT> |
json | parquet | both |
both |
Which artefact family to emit. json writes only catalog.json; parquet writes only edges.parquet + assets.parquet; both writes all three. |
--catalog <NAME> |
string |
Scope the snapshot to a single warehouse catalog. Only assets whose FQN sits in the named catalog are emitted, and edges referencing dropped assets are pruned. |
Behaviour
Section titled “Behaviour”By default (--format both) rocky catalog writes <out>/catalog.json, <out>/edges.parquet, and <out>/assets.parquet; pass --format json to write only the JSON front door. The CLI’s stdout is a short summary in the default --output table mode, or the same JSON payload mirrored to stdout in --output json mode.
The artifact contains:
assets— one entry per model or upstream source, with columns (name plus inferred type and nullability when known, and a per-columndescriptionfrom the sidecar[columns]table when set), upstream / downstream lists, and the model’s intent description when supplied.edges— one entry per column-level lineage edge: source column, target column, transform kind (direct,cast,expression,aggregation: <fn>), and a confidence grade (Highfor explicit projections,Mediumfor star-expanded edges,Lowreserved for future use).stats— aggregate counts (asset_count,edge_count,column_count,assets_with_star,orphan_columns,duration_ms).- A
config_hashfingerprint ofrocky.tomlso consumers can tell whether the catalog was built against the current configuration.
Examples
Section titled “Examples”Build the default snapshot:
rocky catalogrocky catalog project: playground assets: 3 columns: 13 edges: 13 wrote: .rocky/catalog/catalog.json wrote: .rocky/catalog/edges.parquet wrote: .rocky/catalog/assets.parquet duration: 12msPipe the JSON shape directly:
rocky catalog --output json | jq '.stats'Write to a custom directory (for example, when building a per-PR artifact):
rocky catalog --out build/catalogLimitations
Section titled “Limitations”- Per-asset
last_run_idandlast_materialized_atare populated from the state store when a matching successful run exists; they staynullfor assets that have never been materialized (or built before the run history was captured). - Lineage extraction inherits the existing extractor’s coverage: window functions, CTEs, set operations,
CASE WHENprojections, and join keys are not yet surfaced as edges. Asset-level partial lineage is flagged viastats.assets_with_star.
Related Commands
Section titled “Related Commands”rocky lineage– per-model lineage exploration with--columntracesrocky compile– build the semantic graph that the catalog reads
rocky dag
Section titled “rocky dag”Print the whole project as one graph. Every pipeline stage is a node, the dependencies between stages are edges, and the nodes are grouped into the layers they execute in. rocky run --dag executes that same graph.
rocky dag [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
each pipeline’s own configured location | Override the models directory. Omit it and every transformation pipeline uses its own configured models location. rocky run --dag does the same, which keeps the two in agreement. Passing it overrides the location for every transformation pipeline, so Rocky refuses a project that defines more than one rather than build each model twice. |
--seeds <PATH> |
PathBuf |
Seeds directory. | |
--contracts <PATH> |
PathBuf |
Contracts directory. | |
--column-lineage |
bool |
false |
Also emit column-level lineage edges. This one requires a compile, so it costs more than the plain graph. |
What a node carries
Section titled “What a node carries”Each node has an id of the form {kind}:{name}, for example transformation:stg_orders. The kind is one of source, load, transformation, quality, snapshot, seed, test, and replication.
A node also carries the fields that apply to its kind:
- the pipeline name from
rocky.toml; - the target table;
- the materialization strategy;
- the per-model freshness expectation;
- the partition shape;
- the ids it depends on.
How execution layers work
Section titled “How execution layers work”execution_layers is the graph sorted into runnable groups.
execution_layers what the grouping means ──────────────── ──────────────────────────────────────── layer 0 [ A , B ] A and B depend on nothing, so Rocky can │ run them at the same time ▼ layer 1 [ C , D ] C and D depend only on earlier layers, │ never on each other ▼ layer 2 [ E ] E waits for every layer before itNodes inside one layer have no dependency on each other, so an orchestrator can run them in parallel. Each layer waits for the layer before it.
Reading column_lineage correctly
Section titled “Reading column_lineage correctly”column_lineage is empty unless you pass --column-lineage. An empty list on its own does not mean the project has no lineage.
- You did not pass
--column-lineage: nothing was computed, and you can conclude nothing from the empty list. - You passed it and
column_lineage_unavailableis absent: the list is the complete answer, empty included. - You passed it and
column_lineage_unavailableis present: it carries a human-readable reason, and the empty list must not be read as “no lineage”.
Examples
Section titled “Examples”Print the graph for the whole project:
rocky dagInclude column-level lineage edges:
rocky dag --column-lineageRelated Commands
Section titled “Related Commands”rocky emit-sql– render the SQL for the models this graph ordersrocky catalog– the same lineage, written to disk as a queryable snapshotrocky run --dag– execute every pipeline as one graph
rocky emit-sql
Section titled “rocky emit-sql”Render the runnable SQL each transformation model would emit, without a warehouse connection and without running anything. The SQL is generated through the same path rocky run uses, including declared surrogate-key columns wrapped exactly as they are at materialization.
rocky emit-sql [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing .sql and .toml model files. |
--model <NAME> |
string |
Render a single model by name instead of the whole project. | |
--out-dir <PATH> |
PathBuf |
Write one <model>.sql file per model into this directory, in dependency order. When omitted, the concatenated SQL is printed to stdout (also in dependency order). |
Dialect and the runnable guarantee
Section titled “Dialect and the runnable guarantee”The dialect is the project’s configured target adapter type, resolved from rocky.toml without credentials. With no resolvable config it defaults to DuckDB. All models render in this one resolved dialect, so for a project whose models target more than one adapter, the emitted SQL matches rocky run only for the models whose target uses that dialect.
Full-refresh models emit a complete CREATE OR REPLACE TABLE … AS … that runs as-is against a fresh warehouse and matches what a run executes in the resolved dialect. Incremental and merge models emit their steady-state statement instead: a bare INSERT or MERGE that operates on an existing target. rocky run bootstraps the target table on first build and threads the incremental watermark from state, neither of which a static emit can reproduce, so those files carry a leading note to that effect:
-- NOTE: incremental/merge statement — operates on an existing target.-- `rocky run` bootstraps the table on first build and threads the-- incremental watermark from state; this static SQL does neither.MERGE INTO ...Models that produce no standalone SQL are reported on stderr rather than silently dropped, so you never mistake the emitted set for the complete project. Two cases are skipped this way: ephemeral models (inlined as CTEs upstream, so they have no statement of their own) and strategies that cannot render offline, such as a Snowflake dynamic table that needs a live compute-warehouse name.
Examples
Section titled “Examples”Print the whole project’s SQL to stdout in dependency order:
rocky emit-sql-- model: stg_ordersCREATE OR REPLACE TABLE main.stg_orders ASSELECT order_id, customer_id, total_amount FROM raw.orders;
-- model: fct_revenueCREATE OR REPLACE TABLE main.fct_revenue ASSELECT customer_id, SUM(total_amount) AS revenue_amount FROM main.stg_orders GROUP BY customer_id;Write one file per model, ready to commit or hand to another tool:
rocky emit-sql --out-dir build/sql/emit-sql: wrote 2 model(s) to build/sql/ in dependency orderRender a single model, and capture the project-wide SQL into one file:
rocky emit-sql --model fct_revenuerocky emit-sql > build/all.sqlWhen some models cannot be emitted as standalone SQL, the skip report goes to stderr:
emit-sql: 1 model(s) not emitted: - dim_session (ephemeral — inlined as a CTE)Related Commands
Section titled “Related Commands”rocky dag– inspect the dependency orderemit-sqlrenders inrocky catalog– the same compiled graph, exported as a lineage snapshot rather than runnable SQL- No lock-in – the full fallback recipe for stepping away from the engine
rocky test
Section titled “rocky test”Run local model tests via DuckDB without needing warehouse credentials. Validates model SQL, contract compliance, and user-defined test assertions.
rocky test [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing model files. |
--contracts <PATH> |
PathBuf |
Directory containing data contract definitions. | |
--model <NAME> |
string |
Run tests for a single model only. |
Examples
Section titled “Examples”Run all model tests:
rocky test{ "version": "1.6.0", "command": "test", "total": 14, "passed": 12, "failed": 2, "failures": [ { "name": "fct_orders.not_null(order_id)", "error": "found 3 null values" }, { "name": "fct_orders.unique(order_id)", "error": "found 1 duplicate" } ]}Test a single model with contracts:
rocky test --model fct_revenue --contracts contracts/{ "version": "1.6.0", "command": "test", "total": 1, "passed": 1, "failed": 0, "failures": []}The default rocky test path also runs fixture-driven [[test]] unit tests declared in model sidecars. Each [[test]] block mocks upstream inputs with inline rows (given) and asserts the model’s output rows (expect), executed in-memory against DuckDB. When at least one model declares a [[test]] block, the JSON output gains a unit_tests summary; the key is omitted entirely when no model declares one. A failing unit test makes rocky test exit non-zero, the same as a failing model assertion.
{ "version": "1.11.0", "command": "test", "total": 3, "passed": 3, "failed": 0, "failures": [], "unit_tests": { "total": 2, "passed": 1, "failed": 1, "results": [ { "model": "fct_revenue", "test": "discount_caps_at_total", "passed": true, "error": null, "mismatches": [] }, { "model": "fct_revenue", "test": "refunds_subtract", "passed": false, "error": "ordered output mismatch (1 expected vs 1 actual row(s))", "mismatches": [ { "row_index": 0, "expected": "customer_id=7, revenue_amount=80", "actual": "customer_id=7, revenue_amount=100", "kind": "value_diff" } ] } ] }}Each results entry carries the model name, the [[test]] block’s test name, a passed flag, an error message (null when the test passed), and the mismatches array of row-level diffs. Each mismatch renders its row as col=val, col=val. A mismatch kind is missing (expected but not produced), extra (produced but not expected), or value_diff (same positional row, differing values, from an ordered expectation).
--declarative is a separate surface: it adds a declarative block summarising [[tests]] (plural) declared in model sidecars, run against the configured warehouse adapter rather than DuckDB. See Testing and Contracts for both surfaces.
A selector that matches nothing fails
Section titled “A selector that matches nothing fails”rocky test exits 1 when a selector names something the project does not have. This holds with and without --declarative.
| What you passed | Message on stderr |
|---|---|
--model <NAME> naming no model in the project |
model '<NAME>' not found (no transformation model with that name) |
--models <PATH> naming a missing or empty directory |
no models found in <PATH> |
Rocky refuses before it writes any output. Under --output json stdout stays empty, so read the exit code and stderr rather than the payload.
A model that exists but declares no tests is not an error. It still exits 0 and reports total: 0.
Related Commands
Section titled “Related Commands”rocky compile– compile models before testingrocky ci– compile + test in one steprocky ai-test– generate test assertions from model intent
rocky ci
Section titled “rocky ci”Run the full CI pipeline: compile all models and run all tests. Designed for use in CI/CD environments where no warehouse credentials are available. Returns a non-zero exit code if any compilation error or test failure occurs.
rocky ci [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing model files. |
--contracts <PATH> |
PathBuf |
Directory containing data contract definitions. |
Examples
Section titled “Examples”Run CI with default paths:
rocky ci{ "version": "1.6.0", "command": "ci", "compile_ok": true, "tests_ok": true, "models_compiled": 14, "tests_passed": 14, "tests_failed": 0, "exit_code": 0, "diagnostics": [], "failures": []}Run CI with contracts in a GitHub Actions workflow. On a compile error, tests_passed / tests_failed are 0 because tests don’t run, and CI short-circuits and returns a non-zero exit_code:
rocky ci --models src/models --contracts src/contracts{ "version": "1.6.0", "command": "ci", "compile_ok": false, "tests_ok": false, "models_compiled": 13, "tests_passed": 0, "tests_failed": 0, "exit_code": 1, "diagnostics": [ { "severity": "error", "code": "E001", "model": "fct_revenue", "message": "unknown column 'total' in model 'stg_orders'", "span": null, "suggestion": "did you mean 'total_amount'?" } ], "failures": []}Related Commands
Section titled “Related Commands”rocky compile– compile step onlyrocky test– test step onlyrocky ci-diff– structural diff of changed models vs a base git refrocky validate– validate config (often run before CI)
rocky ci-diff
Section titled “rocky ci-diff”Detect which models changed between a base git ref and HEAD, compile both sides, and report added/modified/removed columns. Emits both JSON (for CI pipelines) and a pre-rendered Markdown block suitable for posting as a PR comment.
rocky ci-diff [base_ref] [flags]Arguments
Section titled “Arguments”| Argument | Type | Default | Description |
|---|---|---|---|
base_ref |
string |
main |
Git ref to compare against. Rocky shells out to git diff --name-only <base_ref> HEAD to find changed .sql, .rocky, and sidecar .toml files. |
| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing model files. |
--semantic |
flag | off | Also run the typed-IR semantic breaking-change classifier and surface findings under breaking_findings in the JSON output. Informational only — even a Breaking finding does not change ci-diff’s exit code. The hard gate lives on rocky branch promote. |
Examples
Section titled “Examples”Diff the current branch against main:
rocky ci-diff{ "version": "1.31.0", "command": "ci-diff", "base_ref": "main", "head_ref": "HEAD", "summary": { "added": 1, "modified": 2, "removed": 0 }, "models": [ { "model": "fct_orders", "status": "modified", "columns": [ { "name": "order_status", "change": "added" }, { "name": "amount_cents", "change": "type_changed", "from": "INT", "to": "BIGINT" } ] } ], "markdown": "### Model diff vs `main`\n\n| Model | Status | ... |"}Diff against a feature-branch base and a non-default models directory:
rocky ci-diff release/2026-04 --models src/modelsThe markdown field holds a ready-to-post report; in a GitHub Actions workflow you can jq -r .markdown the JSON output and feed it into gh pr comment.
Run with --semantic to surface classified breaking-change findings alongside the structural diff:
rocky ci-diff --semantic{ "version": "1.31.0", "command": "ci-diff", "base_ref": "main", "head_ref": "HEAD", "summary": { "added": 0, "modified": 1, "removed": 0 }, "models": [ /* ... */ ], "markdown": "...", "breaking_findings": [ { "change": { "kind": "column_type_changed", "model": "analytics.marts.fct_orders", "column": "amount_cents", "old_type": "BIGINT", "new_type": "INT", "narrowing": true }, "severity": "breaking" } ]}The breaking_findings array is omitted from JSON output when empty or when --semantic is not set. Each finding carries a tagged change object (kind discriminator) and a severity (breaking / warning / info). Use --semantic in ci-diff to surface findings on every PR; rely on rocky branch promote to block promotion when severity == "breaking".
The breaking_findings field is JSON-only: --output table still renders the structural diff but does not print the semantic findings list. Use --output json (and pipe through jq) to inspect them.
Related Commands
Section titled “Related Commands”rocky ci– full compile + test for CIrocky compile– compile a single branch without diffingrocky preview– pruned re-run + sampled data diff + cost delta on top ofci-diff’s structural diffrocky branch promote– promote a branch’s tables to production with a hard semantic breaking-change gate
rocky preview
Section titled “rocky preview”Preview a change before it merges. Rocky re-executes only the changed models and their downstream column lineage on a per-pull-request branch, and copies everything else from the base ref.
Three subcommands compose into one review artifact. preview create runs the workflow, preview diff reports the structural and sampled row-level diff, and preview cost reports the cost delta against base. A fourth, preview rows, is separate: it samples the output rows of a single model.
For the design (why CTAS today and warehouse-native clones tomorrow, how the column-level pruner works, what the sampling window’s correctness ceiling is), see the How Preview Works concept page. For a step-by-step walkthrough on a feature branch, see the Preview a PR how-to.
rocky preview create --base <ref> [--name <branch_name>]rocky preview diff --name <branch_name> [--base <ref>] [--sample-size <N>]rocky preview cost --name <branch_name> [--base <ref>]rocky preview rows --model <name> [--cte <name>] [--limit <N>]preview create, preview diff, and preview cost accept --output json|markdown. The Markdown form is pre-rendered for posting to a PR comment; the JSON form embeds the same Markdown in a top-level markdown field for orchestrator use.
rocky preview create
Section titled “rocky preview create”Compute the prune set, copy the rest from the base schema, run only the prune set against a per-PR branch.
| Flag | Type | Default | Description |
|---|---|---|---|
--base <REF> |
string |
main |
Git ref the change-set is computed against. Rocky shells out to git diff --name-only <base>...HEAD against the models directory. |
--name <NAME> |
string |
derived from current branch | Branch name to register in the state store. The branch’s schema_prefix becomes branch__<name> and is the target schema for the pruned run. |
--models <PATH> |
PathBuf |
models |
Directory containing model files. |
Example. Diff against main and create a preview branch:
rocky preview create --base main{ "version": "1.18.0", "command": "preview-create", "branch_name": "preview-fix-price", "branch_schema": "branch__preview-fix-price", "base_ref": "main", "head_ref": "HEAD", "prune_set": [ { "model_name": "fct_revenue", "reason": "changed", "changed_columns": ["amount_cents"] }, { "model_name": "rev_by_region", "reason": "downstream_of_changed" } ], "copy_set": [ { "model_name": "stg_orders", "source_schema": "main", "target_schema": "branch__preview-fix-price", "copy_strategy": "ctas" }, { "model_name": "stg_customers", "source_schema": "main", "target_schema": "branch__preview-fix-price", "copy_strategy": "ctas" } ], "skipped_set": [], "run_id": "run-20260428-141033-002", "run_status": "succeeded", "duration_ms": 4321}copy_strategy reports "ctas" for every successful copy regardless of which SQL primitive the adapter actually emitted. As of engine-v1.19.1, Databricks uses SHALLOW CLONE and BigQuery uses CREATE TABLE … COPY (both metadata-only) under the hood; DuckDB and Snowflake fall through to the portable CTAS default. Surfacing the per-adapter strategy in the wire output is a follow-up.
rocky preview diff
Section titled “rocky preview diff”Sampled row-level diff plus structural (column-level) diff for every model in the prune set.
| Flag | Type | Default | Description |
|---|---|---|---|
--name <NAME> |
string |
(required) | Branch name created by preview create. |
--base <REF> |
string |
main |
Git ref to compare against. Must match what preview create was invoked with. |
--sample-size <N> |
usize |
1000 |
Number of rows to sample per model for row-level diffing. Larger windows reduce false-negative risk; see coverage warning. |
Example. Render a Markdown report ready to post on a PR:
rocky preview diff --name preview-fix-price --output markdownThe JSON shape (PreviewDiffOutput) carries the same data plus the per-model sampling_window block with coverage_warning, and rocky preview diff --output json | jq -r .markdown reproduces the --output markdown report.
rocky preview cost
Section titled “rocky preview cost”Per-model cost delta between the branch run and the latest base-schema RunRecord.
| Flag | Type | Default | Description |
|---|---|---|---|
--name <NAME> |
string |
(required) | Branch name created by preview create. |
--base <REF> |
string |
main |
Git ref the base run is identified by. |
Example.
rocky preview cost --name preview-fix-price --output markdownThe JSON shape (PreviewCostOutput) reports per-model delta_usd, branch_duration_ms, base_duration_ms, and bytes scanned, plus an aggregate summary.delta_usd, summary.savings_from_copy_usd, and models_skipped_via_copy. Underlying cost math is identical to rocky cost (Databricks / Snowflake duration × DBU rate; BigQuery bytes × $/TB; DuckDB zero); fields fall back to null when no base RunRecord exists or when the adapter does not surface USD.
rocky preview rows
Section titled “rocky preview rows”Sample the result rows of one transformation model, or of one CTE inside it. Classified columns are masked inline in the output.
| Flag | Type | Default | Description |
|---|---|---|---|
--model <NAME> |
string |
(required) | Model to preview. |
--cte <NAME> |
string |
Preview one named CTE inside the model instead of the model’s final output. | |
--limit <N> |
u32 |
100 |
Maximum number of rows to return. |
--allow-warehouse |
bool |
false |
Permit execution against a warehouse other than DuckDB. That may cost money, so it is opt-in. A local DuckDB project does not need the flag. |
--pipeline <NAME> |
string |
Pipeline whose target adapter to run against. Required when the project defines more than one pipeline. | |
--models <PATH> |
PathBuf |
models |
Models directory. |
--sql-file <PATH> |
PathBuf |
Preview an ad-hoc SQL snippet read from this file instead of the model’s compiled SQL. This backs the editor’s “Preview Selection”. Mutually exclusive with --cte. |
--sql-file still needs --model, which names the enclosing model. If that model has any masked column, Rocky refuses the ad-hoc preview rather than risk leaking a pre-mask value.
Example. Peek at a model’s rows during local development:
rocky preview rows --model customer_orders --limit 20Output shapes
Section titled “Output shapes”Wire contracts for all four subcommands are exported by rocky export-schemas:
schemas/preview_create.schema.jsonschemas/preview_diff.schema.jsonschemas/preview_cost.schema.jsonschemas/preview_rows.schema.json
These back the autogenerated Pydantic and TypeScript bindings. See JSON Output for the codegen pipeline and version compatibility contract.
Related Commands
Section titled “Related Commands”rocky ci-diff– structural diff alone, without the pruned re-run or row-level samplingrocky branch– the schema-prefix branchespreview createregistersrocky cost– the per-run cost rolluppreview costdiffs across base and branchrocky compare– ad-hoc shadow comparison;preview diffextends the same kernel with sampled row-level diffing
rocky imports
Section titled “rocky imports”Maintain the producer-contract baselines declared in [imports.<name>]. An import is a vendored snapshot of another team’s compiled IR. Your rocky compile diffs the baseline you accepted against the snapshot you vendored, and fails when the producer made a breaking change.
rocky imports update # advance every baseline to its snapshotrocky imports update --check # CI guard: report and exit non-zero, write nothingrocky imports update
Section titled “rocky imports update”Advance the vendored baselines to the current snapshot. This is the explicit “I reviewed the producer’s current state and accept it” gesture. It also reports any stale pin. It never rewrites rocky.toml.
| Flag | Type | Default | Description |
|---|---|---|---|
--check |
bool |
false |
Read-only CI guard. Report what is out of date, exit non-zero, and write nothing. |
Where the baseline sits
Section titled “Where the baseline sits” producer project consumer project ──────────────── ───────────────────────────────────── rocky publish-ir [imports.<name>] in rocky.toml │ baseline = the IR you already accepted │ writes an IR snapshot = the vendored file ▼ snapshot pin = optional recipe hash snapshot file ─ vendored ─► │ ▼ rocky compile diffs baseline against snapshot, fails on a breaking change │ ▼ rocky imports update moves the baseline up to the snapshotRelated Commands
Section titled “Related Commands”rocky compile– the command that reads the baselines and raises the diagnostics- Cross-team contracts – the full producer and consumer workflow