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. | |
--select <SELECTOR>..., -s / --exclude <SELECTOR>... / --state-ref <REF> |
string |
Report and fail on the selected models only. Cannot be combined with --model. |
|
--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. The flag also selects the warehouse for the E042/E043 operand checks, ahead of the adapter type. See Aggregate and comparison operands. |
|
--deny-warnings <CODES> |
string (comma-separated, repeatable) |
Report the listed warning codes as errors and exit non-zero, such as --deny-warnings W042,W043. Other warnings stay warnings. A code that is not a warning code (W999, W42, E042) is refused before the compile, with the list of valid codes. |
|
--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). |
--strict-sources |
bool |
false |
Treat every known source schema as current. A reference to a column the source lacks is the E041 error, even when the schema came from a seed or an old cache entry. Without the flag, those schemas give the W041 warning. Same as [cache.schemas] strict_sources = true. See Missing columns in external sources. |
--dbt-project <DIR> |
PathBuf |
Experimental. Compile a dbt project in place (attach mode). See Attach to a dbt project. Conflicts with --models and --with-seed. |
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", "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.
Three 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.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 type inference gets concrete types instead of RockyType::Unknown. Bails if data/seed.sql is missing or fails to execute.
A seed can be out of date. So a reference to a column the seed lacks is the W041 warning, and the compile still exits 0. Add --strict-sources to refuse it with the E041 error instead:
rocky compile --with-seed --strict-sourcesAttach to a dbt project (experimental)
Section titled “Attach to a dbt project (experimental)”Compile a dbt project without migrating it:
dbt compile --full-refreshrocky compile --dbt-project path/to/dbtEach run does these steps:
<DIR>/target/manifest.json ──▶ version check ──▶ import-dbt rules ──▶ temp dir ──▶ compile (+ run_results.json) (v12 only) (in memory) (removed)- Rocky reads
<DIR>/target/manifest.jsonand its siblingrun_results.jsonon every run. - Rocky writes nothing under
<DIR>. The translated project lives in a private temp directory. Rocky removes it when the command ends. - Rocky refuses the same models that
rocky import-dbtrefuses, with the same reason. The error names each model and its construct, and the command exits non-zero. - An incremental model needs a matching
run_results.jsonfromdbt compile --full-refresh. Without it, Rocky refuses the model and names that command. - Rocky accepts manifest schema
v12(dbt 1.8 and later). It refuses any otherdbt_schema_versionby name. --configis ignored. The adapter comes from<DIR>/profiles.yml, as inrocky import-dbt.--output jsonprints the normalrocky compileJSON. Attach notes and warnings go to stderr. Diagnostic file paths point into the temp directory.
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.
Value derivation and row selection
Section titled “Value derivation and row selection”A column trace reports two kinds of edge, labelled separately:
- Value derivation (
trace): the source column feeds the output value.SUM(s.amount) AS totalderivestotalfromamount. - Row selection (
row_selection): the source column decides which rows or groups exist. It feeds no output value directly.
SELECT c.region, SUM(s.amount) AS totalFROM stg s JOIN cust c ON s.customer_id = c.customer_id <- join_key: stg.customer_id, cust.customer_idWHERE s.status = 'paid' <- filter: stg.statusGROUP BY c.region <- group_by: cust.region SUM(s.amount) ─────────────────────▶ value: stg.amount -> totalrocky lineage fct.total -o json | jq '.row_selection'[ { "source": { "model": "cust", "column": "customer_id" }, "target_model": "fct", "kind": "join_key" }, { "source": { "model": "stg", "column": "status" }, "target_model": "fct", "kind": "filter" }]Each entry names the source column, the target_model whose rows it affects, and a kind: join_key, filter, group_by, having, qualify, window_partition, or window_order. A window key also carries target_column, the one output column its window feeds. Without target_column, the edge affects every column of target_model.
Upstream, row_selection lists the row-selection inputs of every model on the value trace. Downstream (--downstream), it lists the models whose rows the traced column, or a column derived from it, filters, joins, groups, or partitions. The table output prints the same edges under a Row selection heading. trace and edges stay value-only, so existing consumers see no change. JSON omits row_selection when it is empty.
Row selection is read from the model’s top-level SELECT. These constructs produce no row-selection edge yet:
- predicates inside a
WITHbody, a derived table, or a subquery expression (IN (SELECT …),EXISTS) - correlated references
GROUP BY ALL,NATURALjoins,DISTINCT ON, andORDER BY … LIMIT- set operations (
UNION,INTERSECT,EXCEPT) - a named window that references another named window
- an unqualified column in a multi-table query, because its table cannot be determined statically
GROUP BY 1 and GROUP BY <alias> resolve to the projected expression’s source columns.
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 lineage-diff
Section titled “rocky lineage-diff”Report the downstream blast radius of a change between two git refs, for PR review. It combines the structural diff from rocky ci-diff with the downstream consumers from rocky lineage --downstream. Together they show which downstream columns each changed column reaches.
By default the report describes the HEAD commit. Uncommitted edits are ignored. Pass --working-tree to include them. See snapshot modes under rocky ci-diff.
rocky lineage-diff [base_ref] [flags]Arguments
Section titled “Arguments”| Argument | Type | Default | Description |
|---|---|---|---|
base_ref |
string |
main |
Git ref to compare against. Uses the same git-diff mechanism as rocky ci-diff. |
| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing model files. |
--working-tree |
flag | off | Compare the working tree instead of the HEAD commit, including staged, unstaged, untracked, renamed, and deleted files. |
-o, --output <FORMAT> |
json | table | md |
terminal-aware | json emits the full payload, including the pre-rendered report in a markdown field. table and md print that same report directly. |
Examples
Section titled “Examples”Diff the current branch against main and print the Markdown report:
rocky lineage-diff main --output tableRocky Lineage Diff (main...HEAD)
### Rocky Lineage Diff
**2 row(s) changed** (2 modified, 0 added, 0 removed, 0 unchanged)
<details><summary><b>fct_revenue</b> — modified (3 column changes)</summary>
| Column | Change | Old Type | New Type | Downstream consumers ||--------|--------|----------|----------|----------------------|| `total_revenue` | added | - | Unknown | _none_ || `total_tax` | added | - | Unknown | _none_ || `total` | removed | Unknown | - | _(removed; not traceable on HEAD)_ |
</details>
<details><summary><b>stg_orders</b> — modified (3 column changes)</summary>
| Column | Change | Old Type | New Type | Downstream consumers ||--------|--------|----------|----------|----------------------|| `amount_usd` | added | - | Unknown | `fct_revenue.total_revenue` || `tax_amount_usd` | added | - | Unknown | `fct_revenue.total_tax` || `amount` | removed | Unknown | - | _(removed; not traceable on HEAD)_ |
</details>The same diff as JSON, for a CI pipeline:
rocky lineage-diff main -o json{ "version": "1.74.0", "command": "lineage-diff", "base_ref": "main", "head_ref": "HEAD", "summary": { "total_models": 2, "unchanged": 0, "modified": 2, "added": 0, "removed": 0 }, "results": [ { "model_name": "fct_revenue", "status": "modified", "column_changes": [ { "column_name": "total_revenue", "change_type": "added", "new_type": "Unknown" }, { "column_name": "total_tax", "change_type": "added", "new_type": "Unknown" }, { "column_name": "total", "change_type": "removed", "old_type": "Unknown" } ] }, { "model_name": "stg_orders", "status": "modified", "column_changes": [ { "column_name": "amount_usd", "change_type": "added", "new_type": "Unknown", "downstream_consumers": [{ "model": "fct_revenue", "column": "total_revenue" }] }, { "column_name": "tax_amount_usd", "change_type": "added", "new_type": "Unknown", "downstream_consumers": [{ "model": "fct_revenue", "column": "total_tax" }] }, { "column_name": "amount", "change_type": "removed", "old_type": "Unknown" } ] } ], "markdown": "### Rocky Lineage Diff\n\n..."}A removed column has no downstream trace on HEAD, because it no longer exists there. JSON omits downstream_consumers when it is empty, so default a missing key to an empty list.
Consumers of a removed column
Section titled “Consumers of a removed column”A removed column carries consumer_impact instead. Rocky compares the base and HEAD lineage graphs and classifies every model that read the column directly. A renamed column shows up as one removed and one added column, so its consumers are classified the same way.
status |
Meaning |
|---|---|
newly_broken |
HEAD still reads the removed column, or a model now reads it. This consumer breaks. |
unknown |
Rocky cannot decide. HEAD did not compile, or the consumer mentions the column where lineage cannot tell which relation it reads. |
deleted |
The consumer model no longer exists on HEAD. |
repaired |
The consumer still exists on HEAD and provably no longer reads the column. |
A read counts through either edge kind: a value read, or a row-selection read such as a join key or filter (see rocky lineage). Each entry carries model, status, columns (the consumer’s output columns involved), via (value or a row-selection kind), and a one-line reason:
{ "column_name": "amount", "change_type": "removed", "consumer_impact": [ { "model": "fct_broken", "status": "newly_broken", "columns": ["amount"], "via": ["value"], "reason": "HEAD still reads `stg_orders.amount`, which no longer exists" }, { "model": "fct_filtered", "status": "newly_broken", "via": ["filter"], "reason": "HEAD still reads `stg_orders.amount`, which no longer exists" }, { "model": "fct_deleted", "status": "deleted", "columns": ["deleted_amount"], "via": ["value"], "reason": "consumer model was removed on HEAD" }, { "model": "fct_repaired", "status": "repaired", "columns": ["amount"], "via": ["value"], "reason": "HEAD no longer reads `stg_orders.amount`" } ]}The Markdown report adds a Consumers of removed columns table to each model with a classified removal. unknown is the conservative answer: Rocky never reports repaired unless HEAD’s lineage proves it. A model that reads the upstream with SELECT *, while HEAD cannot list the upstream’s columns, is unknown. So is an unqualified column name in a join.
rocky lineage-diff reports; it does not fail a build. Finding changed columns, however many, does not change the exit code. Only an error makes it exit non-zero: an invalid base_ref, a git diff that fails, or invalid or unreadable project configuration.
Related Commands
Section titled “Related Commands”rocky ci-diff– the structural diff alone, without the downstream tracerocky lineage– trace a single column’s lineage directly
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 value-lineage coverage: CTEs, set operations, and
CASE WHENprojections are not yet surfaced as edges. Row-selection edges (join keys, filters, group and window keys) are reported byrocky lineage --columnbut are not yet written to the catalog. 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. | |
--select <SELECTOR>..., -s / --exclude <SELECTOR>... / --state-ref <REF> |
string |
Render only the selected models. | |
--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, read from rocky.toml. No credentials are needed and no connection is opened. 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.
Three outcomes, and the middle one is the point of the no-credentials promise:
rocky.toml |
What happens |
|---|---|
| No file at the given path | Renders in DuckDB, the default dialect. |
Present, with ${VAR} placeholders in an adapter’s connection fields you have not exported |
Renders in the configured dialect. A credential is never sent anywhere, so it does not have to resolve. An unset placeholder anywhere else — an adapter type, an [imports] path — still refuses, because it changes what the config means. |
| Present but malformed, or it breaks a config rule | Refuses, and names the file. Rendering a broken Snowflake project in DuckDB would answer a question you did not ask. |
One exception sits under row two: a placeholder written as a bare value, such as port = ${PORT}, is not valid TOML whether or not the variable is set. That is row three, and the error names PORT.
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. Merge and delete_insert models emit their steady-state statement instead, which operates on an existing target. rocky run bootstraps the target table on first build, which a static emit cannot reproduce, so those files carry a leading note:
-- NOTE: merge/delete_insert statement — operates on an existing target.-- `rocky run` creates the table on first build; this static SQL does not.MERGE INTO ...emit-sql refuses a project with any compile error, before it filters by model. type = "incremental" on a transformation model fails with E037, and an invalid ephemeral use fails with E038. Either stops the whole export, even when --model names a different model.
An ephemeral model gets no file of its own and is reported as skipped. Each consumer’s statement carries it as a __rocky_ephemeral__<model> CTE.
A model whose SQL cannot be rendered offline is reported on stderr rather than silently dropped. So you never mistake the emitted set for the complete project. A Snowflake dynamic table is one: it 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 (cannot render offline: <the dialect's reason>)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. | |
--select <SELECTOR>..., -s / --exclude <SELECTOR>... / --state-ref <REF> |
string |
Report only the selected models. Every model still runs. Not with --declarative. |
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-status <base_ref>...HEAD to find changed .sql, .rocky, and sidecar .toml files. |
| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing model files. |
--working-tree |
flag | off | Compare the working tree instead of the HEAD commit. See snapshot modes. |
--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.
Snapshot modes
Section titled “Snapshot modes”The changed-file list and the compiled files always come from the same snapshot. The JSON output reports which one ran in mode, and the commit the base side was read from in base_commit.
mode |
Changed files | Head side compiled from | Base side compiled from |
|---|---|---|---|
head (default) |
git diff <base_ref>...HEAD |
the HEAD commit, read from git | the merge base of base_ref and HEAD |
working_tree (--working-tree) |
git diff -M <merge base>, plus untracked files that git does not ignore |
the files on disk | the merge base of base_ref and HEAD |
In head mode, uncommitted edits never reach the report. A CI run and a dirty local checkout of the same commit produce the same diff. --working-tree covers staged, unstaged, untracked, renamed, and deleted files, for a local preview before you commit. Its head_ref is WORKTREE.
The base side is the merge base, the same commit base_ref...HEAD selects files against. Later commits on the base branch do not show up as changes in your branch. In a shallow clone without the merge base, both selection and the base compile fall back to base_ref itself, and base_commit is omitted.
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 identifies changed model files and downstream models through sidecar depends_on. It copies the other models from the base schema into a per-PR branch.
Three subcommands compose into one review artifact. preview create prepares the branch, preview diff reports what changed, 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 prune set, adapter copy methods, and diff coverage, see How Preview Works. For a walkthrough, see Preview a PR.
rocky preview create --base <ref> [--name <branch_name>]rocky preview diff --name <branch_name> [--base <ref>]rocky preview cost --name <branch_name>rocky preview rows --model <name> [--cte <name>] [--limit <N>]preview diff and preview cost put a pre-rendered Markdown report in the markdown field of their JSON output. It is ready to post as a PR comment. preview create has no such field. There is no --output markdown: the valid values are json, table, and md, and md behaves like table here.
rocky preview create
Section titled “rocky preview create”Compute the prune set and copy the rest from the base schema into a per-PR branch. It does not run the prune set: it reports run_status: "planned" with an empty run_id. Run rocky run --branch <name> before preview diff or preview cost. That command has no selector for a set of models: it builds the whole pipeline, or one model with --model. Both preview commands read only the newest branch run. They find it by its recorded rocky_branch, the literal --branch value, not by the git branch you have checked out. The base run must be an ordinary production run, without --branch or --shadow.
| 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 |
pr_preview_<current git branch> |
Branch name to register in the state store. Use 1 to 64 characters from [A-Za-z0-9_]. The default is pr_preview_ plus the git branch name, with every other character replaced by _, cut to 64 characters. On a detached HEAD, it is pr_preview_ plus a timestamp. 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": "pr_preview_fix_price", "branch_schema": "branch__pr_preview_fix_price", "base_ref": "main", "head_ref": "HEAD", "prune_set": [ { "model_name": "fct_revenue", "reason": "changed" }, { "model_name": "rev_by_region", "reason": "downstream_of_changed" } ], "copy_set": [ { "model_name": "stg_orders", "source_schema": "main", "target_schema": "branch__pr_preview_fix_price", "copy_strategy": "ctas" }, { "model_name": "stg_customers", "source_schema": "main", "target_schema": "branch__pr_preview_fix_price", "copy_strategy": "ctas" } ], "skipped_set": [], "run_id": "", "run_status": "planned", "duration_ms": 4321}changed_columns exists in the output type, but Rocky leaves it empty and omits it from JSON today. copy_strategy reports "ctas" for every successful copy. Databricks uses SHALLOW CLONE, BigQuery uses CREATE TABLE … COPY, and Snowflake uses CREATE TABLE … CLONE. DuckDB uses CTAS. The output does not distinguish these methods yet.
rocky preview diff
Section titled “rocky preview diff”Compare the branch run with the base run, for every model the branch run executed.
Rocky finds both runs in the state store. The branch run is the newest run recorded with rocky run --branch <name>. The base run is the newest run made without --branch on the git branch or commit that --base names. If there is no such base run, the diff stays empty and base_note says why.
By default this compares the rows_affected the two run records hold. It reports rows_added and rows_removed, leaves rows_changed at 0, returns no samples and no column-level delta, and sets coverage: "not_yet_sampled" with coverage_warning: true.
Two limits follow. A change that rewrites values without changing row counts shows nothing. And an ordinary transformation run records no rows_affected at all.
A model with no recorded row count has an unknown delta. When it ran on both sides, rows_added and rows_removed are null. When it ran only on the branch, rows_added is null and rows_removed is 0. The Markdown shows ? for null. summary.models_unknown counts these models. Rocky counts them as neither changed nor unchanged, and leaves them out of total_rows_added and total_rows_removed.
Pass --algorithm bisection to compare row content. It needs a Merge model whose single unique_key holds whole numbers: the bounds are parsed as integers, so a decimal key falls back to the default comparison without saying so. Read each model’s algorithm.kind before you treat its result as a content comparison.
| Flag | Type | Default | Description |
|---|---|---|---|
--name <NAME> |
string |
(required) | Branch name created by preview create. Rocky matches it against the rocky_branch recorded on each run. |
--base <REF> |
string |
main |
Git branch name or commit that the base run was recorded on. A commit can be a full sha or a prefix of at least 7 characters. It must differ from --name. |
--models <PATH> |
PathBuf |
models |
Models directory. Bisection reads each model’s primary-key column from here. |
The old --sample-size flag is gone. Nothing read it, so Rocky removed it. A script that still passes it now fails to parse.
Example. Print a Markdown report ready to post on a PR:
rocky preview diff --name pr_preview_fix_price --output json | jq -r .markdownThere is no --output markdown. The report lives in the markdown field of the JSON output (PreviewDiffOutput). The same JSON also carries the per-model sampling_window block with coverage_warning.
rocky preview cost
Section titled “rocky preview cost”Per-model cost delta between the branch run and a base run. The branch run is the newest run recorded with rocky run --branch <name>. The base run is the newest run other than the branch’s own. A run made without --branch on a git branch with the same name counts as the branch’s own. Any other run can be the base, including a run made with another --branch name. preview cost has no --base flag.
| Flag | Type | Default | Description |
|---|---|---|---|
--name <NAME> |
string |
(required) | Branch name created by preview create. Rocky matches it against the rocky_branch recorded on each run. |
--models <PATH> |
PathBuf |
models |
Models directory. Rocky reads per-model [budget] blocks from the sidecars here, so a projected breach can name a single model. |
Example.
rocky preview cost --name pr_preview_fix_price --output json | jq -r .markdownThe JSON shape (PreviewCostOutput) carries the Markdown report in its markdown field. It 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). USD fields are left out when the adapter does not surface USD. With no base run, base_run_id is left out and per_model is empty.
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