// reference

Diagnostics & lints

Every construct in SurrealQL has a contract — what the author must mean for the statement to make sense. A diagnostic fires when that contract is provably violated, whether or not SurrealDB would run the query. This is the complete catalog: 90 codes across eight families, plus the parser's own 0xxx codes.

generated from crates/diagnostics/src/catalog.rs · regenerate: node scripts/gen-diagnostics-page.mjs

The contract-first model

Severity vs. level

Every code carries two things. Severity is intrinsic data — E error (provably fails or misbehaves), W warning (provably suspicious but executable), or I hint (analyzer limitation or style note). The default level is the rustc-style policy a consumer applies when no [lints] override names the code:

deny reported as an error — fails check, blocks CI warn reported as a warning — surfaced, non-blocking allow off by default — opt in per project

The two are independent: a code can be intrinsically an E error and still default to warn (the violation is real, but common enough that failing CI on it out of the box would be hostile), and a W warning can default to allow. Consumers apply policy at their edge: the CLI uses it for exit codes, the LSP for editor severities, host adapters for build failures. In rendered output the code's letter reflects the resolved severity (E/W/I); parse-level findings keep an S prefix.

In-source suppression

A suppression comment is source-authored intent, so it applies at analysis time, rustc-style — the directive covers the next line (or its own line when trailing a statement), and a suppressed finding never leaves the pipeline. It must name a real catalog code; blanket suppressions are rejected, and a directive that violates its own contract (unknown code, or a missing reason where one is required) is 7013.

-- surrealguard: allow(E1001) reason="table created by a migration tool at runtime"
SELECT * FROM external_table;

SELECT ssn FROM user; -- surrealguard: allow(1002) reason="column added out-of-band"

The directive accepts a code with or without its severity letter (allow(E1001) or allow(1001)). A reason="…" is required for any deny-level code so the waiver is self-documenting.

Flow-sensitive analysis. Several contracts are checked with occurrence typing rather than a purely local rule. The analyzer narrows option<T> and record<A|B> unions through = NONE guards and type::table() discriminants (including along field paths), so a branch sees the narrowed type — and a branch after a diverging one is flagged unreachable (4006), a provably-constant guard folds to a dead branch (4024). It also reads inside PERMISSIONS FOR clauses — undefined fields, undefined fn:: calls, non-boolean results, always-false predicates — and catches comparison footguns: = NONE/= NULL against a kind that excludes it, IN/CONTAINS element-kind mismatches, and equality between record links whose tables can't overlap.

Graph edge-filter WHERE

A graph step may carry an inline predicate that filters the edge (or target) rows during traversal. SurrealGuard supports both spellings and type-checks the predicate against the fields of the filtered table — exactly as it would a top-level WHERE:

-- parenthesised edge filter
SELECT ->(likes WHERE strength > 3)->post FROM user;

-- bracketed edge filter (equivalent)
SELECT ->likes[WHERE strength > 3]->post FROM user;

The predicate is resolved in the scope of the likes edge table, so:

The families

Codes are <family><3 digits>. Jump to a family, or read the full catalog below — every code, its severity, its default level, and what trips it.

0xxx Syntax 3 codes

Parse-level breakage, emitted by the parser before any analysis runs. These are not in the catalog table — they carry an S prefix and are never demotable, because a source that does not parse cannot be checked.

CodeLevelContract & what trips it
S0000alwaysThe source parses at all — a parser failure that produces no tree; the message is the parser's own
S0001alwaysNo unparsable region in the source — tree-sitter ERROR node — a stray token, a malformed clause, an unterminated literal
S0002alwaysNo required syntax node is missing — a MISSING node — an unclosed (, an absent keyword the grammar requires
1xxx Schema references 11 codes

Every name a query uses must resolve against the schema, wherever the schema constrains it.

CodeLevelContract & what trips it
E1001denyA table reference names a known table — FROM/targets, thin statements, RELATE endpoints, record<t> in DEFINE FIELD, relation IN/OUT tables, DEFINE ... ON table
E1002denyA field reference names a declared field of its row's table (schemafull only; FLEXIBLE subtrees exempt) — projections, WHERE/expressions, SET/UNSET targets, payload keys, RETURN, OMIT, FETCH, SPLIT, GROUP/ORDER keys, INSERT columns, index/event fields in DEFINE, const PATCH paths
E1012denyA schema-object reference names a known object of that kind — REBUILD/REMOVE INDEX, REMOVE EVENT, SEARCH ANALYZER refs, WITH INDEX hints
W1021warnREMOVE removes something that exists REMOVE TABLE ghost
W1022warnA definition does not silently redefine (OVERWRITE states intent) — two DEFINE TABLE person
E1023warnFETCH names something that can hold records FETCH age (int); an alias of a computed non-record value
E1024warnSPLIT names a collection field SPLIT age
E1025denyA subfield is declared under an object-shaped parent FIELD a TYPE int then FIELD a.b
E1027denyAn index-backed operator has its supporting index @@/search::* need a SEARCH index; <|k|> needs MTREE/HNSW
W1029warnEach index covers a distinct field set — two indexes on (email)
E1032denyDEFINE ANALYZER components name known tokenizers/filters/languages FILTERS snowball(klingon) — tokenizer names are parser-covered (the grammar hard-codes them); filter names/languages emit here
2xxx Types & nullability 23 codes

Values must inhabit the kinds their position demands; operators, casts, and clauses require operands they can act on.

CodeLevelContract & what trips it
E2001denyA value written to a field inhabits the field's declared type — SET values, CONTENT/MERGE/REPLACE payload values, INSERT tuple values and object payloads, DEFAULT and VALUE clauses in DEFINE, record-link targets (record<a>record<b>), NONE into non-optional (message points at option<>)
E2004denyThe operands make sense together for the operator — binary and unary, arithmetic and comparison, compound assignment (age += 'x'); SurrealDB kind-ordering instead of throwing changes nothing
W2005warnA condition position expects a boolean — IF conditions, ASSERT clauses, bare non-boolean WHERE
E2007denyA cast names a known type <ghost> x
E2008denyA conversion can succeed — kind-proven (<duration> true) or value-proven (<int> 'abc', type::int('x')) — the proof strength varies, the contract doesn't
E2012denyA body returns what it declares fn:: -> string { RETURN 1 }; closures |$x| -> string { RETURN 1 }
W2015warnA value-requiring position gets a value that is always present option<int> field in x + 1
E2017denyORDER BY keys name fields available on the result rows (or RAND()) — non-field key; explicit projections not containing the key
E2018denyLIMIT/START take a non-negative integer — wrong kind (via params; literals parse-rejected), negative constants
E2019denyTIMEOUT takes a duration — parser-covered today; emission exists for when params are grammatical
E2020denyKILL takes a live-query uuid — parser-covered; grammar-fork bug: KILL $id fails to parse
E2021denySHOW SINCE takes a versionstamp or datetime.
E2022denyFOR iterates something iterable FOR $x IN 42 — ranges must be modeled first or this false-positives
E2025denyREADONLY fields are written only at creation UPDATE t SET created = ...
W2026warnComputed (VALUE-clause) fields are not hand-assigned — the write is silently overwritten
E2030denyIndex/filter/splat apply to collections age[0], name[WHERE ..], age.*
E2031denyA regex literal compiles name ~ 'unclosed('
E2032denyLiteral content is valid for its kind d'2024-13-45', u'not-a-uuid'
E2033denyPATCH operations are well-formed — unknown op, path without /
E2034denyRequired fields are provided at creation CREATE person; with non-optional, no-DEFAULT name
E2035denyDEFINE ANALYZER filter arguments are valid edgengram(5, 2)
E2036denyGeoJSON literals have their declared shape {type: 'Pointt', ...}
E2037denyA field's DEFAULT satisfies its own ASSERT DEFAULT 'activ' ASSERT $value IN ['active','inactive']
3xxx Graph & relations 5 codes

A traversal or RELATE must use relations as declared — in->edge->out.

CodeLevelContract & what trips it
E3001denyA step traverses a relation table ->person-> in a chain; a RELATE edge that is a plain table
E3002denyThe usage matches the relation's declared shape (in->edge->out) — wrong-direction traversal, a hop landing off the far side, RELATE writing endpoints on the wrong sides — messages show declared vs written shape
E3004warnA FROM-position chain is complete (edge->target pairs) FROM user->writes
E3009denyA traversal starts from records age->writes->
W3011warnGraph recursion is bounded @{..} with no upper bound
4xxx Statement & clause / flow 23 codes

Clauses must be valid on their statement, and control flow must be well-formed and reachable.

CodeLevelContract & what trips it
E4001denyClause not valid on this statement SELECT ... RETURN NONE, CREATE ... WHERE
E4002denySELECT VALUE with multiple projections SELECT VALUE a, b FROM t — verified: both SurrealDB's parser *and* ours reject the syntax, so this is parse-level (0xxx); the code stays reserved, no analyzer emission
E4003denyONLY on a table-wide target without LIMIT 1 SELECT * FROM ONLY person, UPDATE ONLY person — deterministic runtime error (SingleOnlyOutput); CREATE is exempt (always one row)
E4004denyINSERT tuple column/value count mismatch (a, b) VALUES (1)
E4005denyBREAK/CONTINUE outside a loop — top-level BREAK
W4006warnUnreachable statements after RETURN/BREAK/THROW RETURN 1; SELECT ... in a block
E4007denyTransaction pairing contract: BEGIN opens exactly one transaction that COMMIT/CANCEL closes — unopened COMMIT/CANCEL, nested BEGIN, BEGIN never closed
E4009denyLIVE SELECT with unsupported clause LIVE SELECT ... GROUP BY
W4010warnDuplicate SET target in one statement SET age = 1, age = 2
W4011warnDuplicate projection key/alias SELECT age, age FROM t, two AS x
W4012warnOMIT without a wildcard projection — verified parser-covered: the grammar only accepts OMIT alongside * — code reserved, no emission
W4013warnGROUP BY field not in projections — SurrealDB aggregate rules
I4016allowEmpty block — verified unreachable: {} in value position is an empty *object* literal, and statement-position blocks don't have their value consumed — code reserved, no emission
W4017warnBlock ends with LET — its value is NONE { LET $x = f(); } consumed as a value
W4018warnSide-effecting subquery in read position SELECT (CREATE log) FROM t
W4019warnCREATE/INSERT on a relation table without in/out CREATE likes SET strength = 1
W4020warnRETURN mode meaningless for the statement CREATE ... RETURN BEFORE (always NONE)
E4021warnSHOW CHANGES on a table without CHANGEFEED.
W4022warnSELECT from a DROP table — rows are never retained
W4023warnCount() without GROUP BY yields 1 per row, not a total — add GROUP ALL for a total
W4024warnAn IF branch is unreachable — its guard provably folds to a constant IF false { ... }, the ELSE after IF true { ... }
E4025denyA wildcard projection cannot be aggregated by a GROUP clause SELECT * FROM t GROUP BY k, SELECT * FROM t GROUP ALL, SELECT *, count() FROM t GROUP BY k — 3.0.5 rejects all of them outright (Incorrect selector for aggregate selection, expression \*\ … cannot be aggregated in a group); 2.x silently drops the *, so the query never returns what its author asked for under either engine
W4026warnA filtered ONLY has no provable single-row target SELECT * FROM ONLY t WHERE status = 'open' — errors (Expected a single result output when using the ONLY keyword) the moment two rows match, but succeeds while one does; W, not E, because the filter may well be single-row for reasons the schema does not state. Silent when at most one row is provable: a record-id target, WHERE id = …, an equality covering every field of a UNIQUE index, or LIMIT 1. Sibling of 4003, which owns the *unfiltered* table-wide case
5xxx Functions & closures 5 codes

Calls must resolve, match their signature, and terminate.

CodeLevelContract & what trips it
E5001denyA call resolves to a function that exists — unknown builtins, undefined fn::, methods not available on the receiver's kind
E5002denyA call matches the function's signature — argument count, per-argument kinds (anchored per argument), fn:: declared params, a closure declaring more parameters than its consumer binds
E5005denyA const argument satisfies the function's value contract type::field('aeg') naming no field, non-string paths (type::field(42)), type::thing('ghost', ..) naming no table, out-of-range constants (math::fixed(x, -1))
W5009warnfn:: definitions terminate (no direct/mutual recursion cycles) fn::f calls fn::f
W5010warnEvents do not trigger themselves (directly or in a cycle) — event on person THEN mutates person; A→B→A
6xxx Parameters 7 codes

A $param in a checkable position produces a constraint host adapters discharge, not a finding. Codes here fire only on contradictions and misuse.

CodeLevelContract & what trips it
E6001denyConflicting constraints on one param WHERE $x > 3 AND $x = 'abc'
W6002warnParam shadows a DEFINE PARAM with a different kind LET $min_age = 'x' vs defined int
I6003allowUnresolvable dynamic construct (analyzer limitation) — current dynamic(6001) class
W6004warnParam used before its LET in source order RETURN $x; LET $x = 1;
E6005denyContext param used outside its context $before outside an event, $parent outside a subquery
E6006denyHost-declared type contradicts query constraint — host binds $age: string, query needs int
E6007denyAssignment to a protected parameter LET $auth = {...}
7xxx Lints & style 14 codes

Opinions, not contract violations — many ship allow so you opt into the ones your team wants. All configurable via [lints].

CodeLevelContract & what trips it
W7001allowUnused LET binding LET $x = 1; never read
I7002allowLET shadowing — inner LET $x over outer
I7003allowMixed-kind array literal [1, 'a']
W7004warnControl flow is decided by a constant IF true, WHERE 1 = 1, FOR $x IN []
W7005warnA comparison against a closed literal set must be able to match WHEN $event = 'CRATE'$event is 'CREATE' | 'UPDATE' | 'DELETE'; value-proven always-false (distinct from 2004: the kinds are comparable)
W7006warnEmpty IN/CONTAINS list WHERE x IN []
I7007warnSELECT * with explicit fields SELECT *, age FROM t
I7008allowSchemaless table in a typed workspace — queries against fieldless tables
W7009allowWhole-table UPDATE/DELETE without WHERE DELETE person;
W7011warnAssignment to id in SET SET id = ...
W7012warnBlocking or side-effecting call in a computed context http::get(...) / sleep() in a field VALUE or event body
W7013warnA suppression directive names a catalog code (with a reason when required) -- surrealguard: allow(ghost); missing reason under require_suppression_reasons
I7014allowWhole-table SELECT without WHERE/LIMIT SELECT * FROM person;
I7015allowBare SELECT * (over-fetch / schema-drift brittleness) SELECT * FROM person WHERE id = person:tobie;
8xxx Version compatibility 2 codes

Gated on [analysis] surrealdb_version. Checks resolve against a per-version registry of function availability and syntax support.

CodeLevelContract & what trips it
E8001denyEvery function used exists in the configured target version — unavailable (array::fold on 1.x) or renamed (string::endsWith — message suggests string::ends_with)
E8003denySyntax requires a newer version — closures / ?? on old targets

Parameter constraints

Every use of an unbound $param exports the kind and value domain its uses imply — SET age = $age$age: int; string::len($s)$s: string; LIMIT $n$n: int and n >= 0; type::field($f)$f: string and one of the table's field paths. Constraints from multiple uses intersect; an empty intersection is 6001. Host adapters (the Rust macro and TS codegen) enforce them at the call site.

Context parameters

SurrealQL injects parameters by context; the analyzer models them as implicitly bound, with kinds where the context defines them — so their bodies type-check and using one outside its context is 6005.

ParamContextKind
$thisany row scopecurrent row (table's object type)
$parentsubqueryenclosing row
$eventDEFINE EVENT'CREATE' | 'UPDATE' | 'DELETE'
$before, $afterDEFINE EVENTthe table's row type
$value, $inputEVENT / FIELD VALUE·ASSERTevent row / the field's declared type
$auth, $session, $token, $scopepermissions / anywheresession-shaped objects (open)