Diagnostics reference¶
smelt emits named diagnostic codes through its compiler, LSP server, and CLI. Every diagnostic carries a severity level (Error, Warning, Info, Hint), a human-readable message, and a stable code you can use to look up remediation guidance.
Severity levels¶
| Severity | Meaning |
|---|---|
Error |
The model or function declaration is invalid; smelt will not compile the project until the error is resolved. |
Warning |
Likely incorrect or sub-optimal; the project compiles but may behave unexpectedly. |
Info / Hint |
Informational; no action required. |
Code catalogue¶
The complete DiagnosticCode catalogue — every variant, its severity, and the user-input condition that triggers it — is maintained in
docs/specs/diagnostics.md.
Codes are grouped by owning feature: models & core analysis, sources, seeds, timeseries, incremental & cumulative, types, Python models, functions & expansion, meta-language, records/maps/loaders, multi-model production, and property diff (PropertyDowngrade, PropertyDiffBaselineUnavailable — see smelt explain --diff).
Example: UnknownStructFieldType¶
Severity: Error
Emitted when a smelt.define or smelt.extern parameter or return-type annotation contains a Struct<{…}> shape whose field type text cannot be resolved to a recognised DataType. The diagnostic is anchored at the individual field's type-reference span, not the whole annotation, so the exact bad token is highlighted.
Example — the following emits UnknownStructFieldType on the Bogus span:
Fix: Replace the unrecognised type name with a concrete smelt DataType such as Integer, Text, Float, Boolean, Timestamp, or a nested Struct<{…}>.
Example: UnsupportedOnBackend¶
Severity: Error
A built-in's backend support can differ by where it's called — as a scalar expression, an
aggregate, a whole-partition window (OVER (PARTITION BY …) with no ORDER BY or frame), or a
running window (any narrower frame, including the common OVER (PARTITION BY … ORDER BY …)).
smelt transparently restructures a whole-partition window over an aggregate-only built-in — or an
aggregate over a window-only built-in — around a synthesised CTE. That restructure has no correct
form for a running window, so a running window over a built-in the target backend offers only
as an aggregate is refused at compile time. See Position-dependent aggregate
support for the full picture and the
rewrite to apply by hand.
Example — targeting DuckDB, a running PERCENTILE_CONT window emits UnsupportedOnBackend:
SELECT
id,
g,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x) OVER (PARTITION BY g ORDER BY t) AS running_med
FROM tbl
Fix: Rewrite the query so the affected aggregate is computed once per partition — for example, by grouping into a CTE keyed on the partition column and joining the constant value back onto each row of the partition, rather than asking the target backend for a running form it does not have.
The refusal names the construct, the reason it doesn't fit the backend, and the backend itself. For the query above, targeting DuckDB, the message reads:
UnsupportedOnBackend: this model uses 2 constructs the DuckDB backend cannot express:
`PERCENTILE_CONT` — DuckDB has the ordered-set aggregate but no running-window form of it; only a window covering the whole partition can be restructured around a grouped CTE
`PERCENTILE_CONT` — DuckDB has the ordered-set aggregate but no running-window form of it; only a window covering the whole partition can be restructured around a grouped CTE
A template's spelling cannot carry a modifier¶
A built-in whose target spelling is a fixed template over its own positional arguments — DuckDB's
DATE_SUB(d, i), spelled d - i — has no place in that spelling for a call modifier: DISTINCT,
FILTER (WHERE …), WITHIN GROUP (ORDER BY …), an ORDER BY inside the argument list, IGNORE
NULLS/RESPECT NULLS, a named (=>) argument, or a * argument. Dropping the modifier would
change the answer — a dropped DISTINCT counts duplicates the author excluded — so the call is
refused at compile time rather than silently stripped.
Example — targeting DuckDB, DATE_SUB carrying DISTINCT:
UnsupportedOnBackend: this model uses 1 construct the DuckDB backend cannot express:
`DATE_SUB` — this built-in's target spelling is a fixed template over positional arguments; DISTINCT cannot be expressed by a template (a dropped DISTINCT would count duplicates the author excluded) and is refused rather than silently dropped
Fix: Rewrite the query without the modifier — for example, deduplicate the input rows in a CTE before calling the function — or pick a target backend whose registry entry for that built-in is not a template.
A verdict that depends on operand type¶
Some built-ins lower differently depending on the type of the values passed to them, not only on
where they're called. // is DuckDB's native floor/true division operator, but Spark and BigQuery
have no infix //: on Spark, a // b lowers to a DIV b when both operands are integral and to
plain a / b when both are floating-point or decimal. When an operand's type cannot be resolved at
compile time, smelt refuses rather than guess — guessing wrong here would silently compute a
different number, not fail loudly.
Example — targeting Spark, // over a column whose type cannot be resolved:
UnsupportedOnBackend: this model uses 1 construct the Spark SQL backend cannot express:
`//` — Spark SQL has no infix `//`; use a typed FLOOR(a / b) or DIV(a, b)
Fix: Give the operands a resolvable type — for example, by declaring the upstream column's type
or wrapping the operand in an explicit CAST — or rewrite the expression as a typed FLOOR(a / b)
or DIV(a, b) call.
Source retention¶
A source may declare a rolling retention: bound (../guide/sources.md#bounded-history-retention).
smelt reasons about a model's required reach into that source against the declared bound, and
either refuses or records a downgrade — never silently computes over less history than the model
asked for. Full semantics: docs/specs/sources.md
§"Retention refusal".
| Code | Severity | Trigger |
|---|---|---|
SourceRetentionExceeded |
Error | A model's derived required reach into a declared-retention: source is proven to exceed the source's retained bound, or a whole-table recompute over a declared-retention: source is attempted with stored output already present and no license. |
SourceRetentionDowngraded |
Warning | A model's derived required reach into a declared-retention: source could not be proven to fit inside the bound (an unbounded or otherwise underivable reach) — admitted, but the model's pre-bound region stops being claimed replayable. |
smelt explain <model> renders both the bound and the required reach for every declared-retention:
source under Retention: (--json: the retention array) — see smelt explain.
Succession grain¶
The succession grain is recognised from a model's SQL shape, never
declared — every rejection below names the offending clause and a fix rather than falling back to
another grain silently. Full semantics: docs/specs/incremental_shapes.md §"Succession-grain
admission (no declaration)".
| Code | Severity | Trigger |
|---|---|---|
SuccessionWindowFunctionNotLead |
Error | A window function in the projection is not LEAD(t)/LAG(t) over the clock column at the default offset, or not a scalar expression over one. |
SuccessionPartitionKeyMismatch |
Error | Two or more window functions partition by different column sets, an unresolvable column set, or a column not proven NOT NULL. |
SuccessionOrderNotMonotoneClock |
Error | A window's ORDER BY column does not trace as a strictly monotone clock to the driving source's event_time_column, is not proven NOT NULL, or the sort is descending or carries a second key. |
SuccessionRowLocalColumnViolation |
Error | A projected column that is not a window function (or an expression over one) is itself an aggregate, a further window function, or otherwise not row-local. |
SuccessionIdentityNotProjected |
Error | A key column or the clock column is not projected row-locally, so (k, t) cannot be recovered from the presented table. |
SuccessionSingleSourceOnly |
Error | The FROM clause is not exactly one source reference — a join, CTE, subquery, or set operation is present. |
SuccessionDrivingSourceNotAppendOnly |
Error | The driving source does not declare mutation_profile.kind: append_only, or declares no timeseries: block. |
SuccessionPreFilterNotRowLocal |
Error | A filter precedes the window projection but is not one deterministic row-local predicate over the driving source's own columns. |
SuccessionDeleteFilterMisplaced |
Error | A QUALIFY clause exists but is not exactly QUALIFY NOT <row-local NOT NULL boolean column>, or a same-scope WHERE tests a window-derived column. |
SuccessionPreFilterNegatesFlag |
Warning | The pre-window WHERE is a bare negated boolean column — admitted unchanged, but named because a CDC delete flag filtered here never closes its predecessor's interval. |
SuccessionPatternUnrecognized |
Error | refresh: incremental with no unique_key, no timeseries:, and a SQL shape none of the rules above names — a stray DISTINCT/GROUP BY/HAVING/ORDER BY/LIMIT, or a model resembling no admitted grain. |
SuccessionClockTie |
Error | Runtime: a delta presents two non-identical events at the same (k, t), or a delete and a non-delete collide at one (k, t). The run's transaction rolls back. |
Example: SuccessionDeleteFilterMisplaced¶
Severity: Error
A CDC delete flag must be filtered with QUALIFY, not WHERE, so its event still contributes to
its predecessor's LEAD/LAG-derived column before it is dropped from the output.
Example — filtering the delete flag before the window computes:
SELECT
customer_id,
effective_ts AS valid_from,
LEAD(effective_ts) OVER (PARTITION BY customer_id ORDER BY effective_ts) AS valid_to
FROM smelt.sources.customer_changes
WHERE NOT is_deleted
Fix: Move the filter after the window, as a QUALIFY: