Skip to content

Linters

Rocky ships two built-in lints that catch the two most common silent-breakage classes in SQL projects: SQL that only runs on one warehouse dialect, and SELECT * expressions whose schema is actually consumed downstream. Both lints run as part of rocky compile (and surface as LSP diagnostics in the VS Code extension).

Code Severity What it catches Enabled
P001 error SQL constructs that don’t run on the configured target dialect Opt-in via --target-dialect or [portability]
P002 warning SELECT * where a downstream model references specific columns Always on

A SQL style linter such as SQLFluff checks the text of a query: indentation, alias conventions, ambiguous references, unused columns. That work is worth doing, and Rocky doesn’t replace it; run a style linter alongside Rocky if you want consistent formatting.

Rocky’s lints answer a different question. P001 and P002 are semantic: they read the compiled model graph, not a single query in isolation, so they catch breakage a style linter can’t see. P001 knows a construct won’t run on your target warehouse. P002 knows a SELECT * will silently propagate an upstream schema change to a specific downstream consumer. A query can be perfectly well-formatted and still fail both.

These compile-time lints are the first of three layers. Beyond them, Rocky enforces at run time: data-quality checks and schema-drift detection verify that a model’s actual output still matches what it declared.


Rejects SQL constructs that don’t exist on the chosen warehouse dialect. Detection is AST-based (sqlparser visitor over each model’s SQL), using the same catalog that backs Rocky’s SQL transpiler.

Construct Supported by
NVL, IFNULL Snowflake, Databricks
DATEADD Snowflake, Databricks
DATE_ADD BigQuery
TO_VARCHAR Snowflake
LEN, CHARINDEX Snowflake
ARRAY_SIZE Snowflake
DATE_FORMAT BigQuery, Databricks
QUALIFY Snowflake, Databricks, DuckDB
ILIKE Snowflake, Databricks, DuckDB, BigQuery
FLATTEN Snowflake

The catalog is conservative: anything not in it is assumed portable. False negatives (non-portable SQL that slips through) are expected; false positives (portable SQL flagged as non-portable) are not.

Either pass --target-dialect <dbx|sf|bq|duckdb> on the command line:

Terminal window
rocky compile --target-dialect bq

Or declare the target once in rocky.toml:

[portability]
target_dialect = "bigquery"

When both are set, the CLI flag wins. See [portability] for the full config.

{
"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"
}

Every P001 diagnostic names the construct, the supported dialects, and a one-line suggestion of the portable replacement.


Warns when a model uses SELECT * and at least one downstream model references specific columns of its output. The lint is blast-radius-aware: a leaf model with SELECT * is intentionally not flagged, because a leaf projection’s columns are never consumed by a named reference, so an upstream schema change can’t silently leak past the SELECT *.

-- models/stg_orders.sql
SELECT * FROM raw__orders.orders
-- models/fct_revenue.sql
SELECT order_id, total_amount -- explicit column reference
FROM stg_orders

If raw__orders.orders drops total_amount, stg_orders keeps compiling (it’s just SELECT *), but fct_revenue breaks at run time. P002 fires on stg_orders and names fct_revenue as the consumer that made the radius concrete.

{
"severity": "warning",
"code": "P002",
"model": "stg_orders",
"message": "SELECT * silently propagates upstream schema changes to 2 downstream consumers: fct_revenue (order_id, total_amount, status), mart_ltv (customer_id)",
"span": { "file": "models/stg_orders.sql", "line": 1, "col": 1 },
"suggestion": "replace SELECT * with an explicit column list to make schema dependencies visible"
}

The diagnostic caps each consumer’s listed columns at 3 for legibility on wide schemas.

P002 runs on every rocky compile and rocky ci invocation without any flag or config. Detection uses the semantic graph (ModelSchema::has_star + column_consumers), not a re-parse: the cost is a single pass over the already-compiled graph, so it’s cheap enough to run by default.


[portability] allow exempts a construct for every model in the project. Useful when a project standardizes on a non-portable extension:

[portability]
target_dialect = "bigquery"
allow = ["QUALIFY"]

Labels are case-insensitive and match PortabilityIssue::construct (e.g. QUALIFY, NVL, DATEADD).

For one-off exemptions, drop a pragma anywhere a line comment is legal in the model’s SQL:

-- rocky-allow: NVL, QUALIFY
SELECT NVL(a, b) AS c
FROM t
QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC) = 1

Pragmas:

  • Accept a comma-separated list of construct labels.
  • Are case-insensitive (nvl, NVL, Nvl all work).
  • Apply only to the model whose SQL contains them; they are not hoisted to the project.
  • Are silently ignored if the label doesn’t match any known construct, so adding a new lint later doesn’t require removing stale pragmas.

Prefer the pragma for targeted exemptions over expanding [portability] allow. The pragma lives next to the offending expression, which makes the decision reviewable in code.

P002 warnings cannot yet be silenced per-model; the shared pragma parser is in place for a future [lints]-block driven toggle.


Both lints emit the same diagnostic envelope as every other compiler check, so everything that reads CompileOutput.diagnostics handles them uniformly:

  • The CLI’s text-mode output renders them with miette spans pointing at models/<name>.sql.
  • rocky compile --output json includes them in the diagnostics array.
  • The VS Code extension surfaces them as inline squiggles via the LSP.

P001 diagnostics are errors: they fail rocky compile and rocky ci. P002 diagnostics are warnings: they show up in the output but do not fail the run.