surformSurrealDB schemas, declared

The linter

surform lint is the advisory pass: warnings about files that are legal but suspicious. Findings never change what check and build emit — but like a clippy step, surform lint exits 1 when anything fires, so it drops straight into CI:

sh
surform lint schema.surdl && surform build schema.surdl > schema.surql

Findings render exactly like diagnostics — code, snippet, help line, or --json — at warning severity. Codes are SDL-Lxxxx; this page is the catalog.

The warnings in action

A table wide open (permissions: full; — anyone, even unauthenticated users, can read and write every record; omitting the clause is the secure default and never warns):

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

table public_profile {
  schemafull;
  field name: string;
  permissions: full;
};
what Surform suggests
text
SDL-L0001 (spec/table.md)

  ! table `public_profile` in database `acme/shop`: permissions are `full` — anyone, even unauthenticated users, can access every record
   ,-[4:1]
 3 |     
 4 | ,-> table public_profile {
 5 | |     schemafull;
 6 | |     field name: string;
 7 | |     permissions: full;
 8 | |-> };
   : `---- `permissions: full` on this table
   `----
  help: `full` grants everyone — including unauthenticated connections — access to every record; omit the clause for the secure default (implicit `none`), or scope operations in a `permissions`
        block

A plaintext password (the file is going into git — so is your password):

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

user admin {
  on: root;
  password: 'change-me-immediately';
  roles: owner;
};
what Surform suggests
text
SDL-L0002 (spec/user.md)

  ! user `admin` in the root scope carries a plaintext `password:` — the literal rides in the file and in any emitted DDL
   ,-[4:1]
 3 |     
 4 | ,-> user admin {
 5 | |     on: root;
 6 | |     password: 'change-me-immediately';
 7 | |     roles: owner;
 8 | |-> };
   : `---- plaintext `password:`
   `----
  help: prefer `passhash:` with a pre-computed argon2 hash, or inject the secret at apply time

An ngram window that can only degrade (min larger than max — the database accepts it silently):

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

analyzer fuzzy {
  filters: ngram(5, 2);
};
what Surform suggests
text
SDL-L0004 (spec/analyzer.md)

  ! analyzer `fuzzy`: `ngram(5, 2)` has min > max — SurrealDB accepts it and silently degrades
   ,-[4:1]
 3 |     
 4 | ,-> analyzer fuzzy {
 5 | |     filters: ngram(5, 2);
 6 | |-> };
   : `---- `min` exceeds `max`
   `----
  help: swap the bounds so min ≤ max — SurrealQL accepts any pair and silently degrades

A relation that accepts dangling edges (enforced; missing):

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

table user;
table post;

table user->wrote->post;
what Surform suggests
text
SDL-L0006 (spec/table.md)

  ! relation `wrote` in database `acme/shop` is not `enforced;` — RELATE accepts endpoints that do not exist
   ,-[7:1]
 6 | 
 7 | table user->wrote->post;
   : ^^^^^^^^^^^^|^^^^^^^^^^^
   :             `-- not `enforced;`
   `----
  help: add `enforced;` to make SurrealDB verify both endpoints at write time

And delete: full; on a table — legal, but the database never reports it back, so the definition cannot round-trip or survive the migrate compare:

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

table audit {
  permissions {
    delete: full;
  };
};
what Surform suggests
text
SDL-L0014 (spec/table.md)

  ! table `audit` in database `acme/shop` carries `delete: full;` — INFO elides `FOR delete FULL`, so the clause cannot round-trip or drift
   ,-[4:1]
 3 |     
 4 | ,-> table audit {
 5 | |     permissions {
 6 | |       delete: full;
 7 | |     };
 8 | |-> };
   : `---- `delete: full;` cannot round-trip
   `----
  help: the clause still applies on execution; INFO elides `FOR delete FULL`, so the differ converges silently — verify the database's actual delete permission if it matters

A reference to something no file defines — here a typo'd table in a permission filter (orgs for org; SurrealDB's catalog is flat, so the schema stays legal — the warning is the catch):

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

table org {
  field name: string;
};

table page {
  permissions {
    select: (SELECT * FROM orgs WHERE org = $auth.org);
  };
};
what Surform suggests
text
SDL-L0024 (spec/README.md#the-linter)

  ! table `orgs` is referenced in this table permission in `page` in database `acme/shop` but no linted file defines it
    ,-[8:1]
  7 |     
  8 | ,-> table page {
  9 | |     permissions {
 10 | |       select: (SELECT * FROM orgs WHERE org = $auth.org);
 11 | |     };
 12 | |-> };
    : `---- `orgs` is not defined
    `----
  help: define the target, check the spelling, or excuse the statement with `// surform-lint-allow: SDL-L0024` — a target outside the linted files is legitimate (SurrealDB's catalog is flat)

The reference warnings stay conservative on purpose: a param read in a permission or default can be bound by the client at query time, so only fn:: bodies (whose context the engine isolates from client-bound values) flag unknown params — a param definition resolves inside a body, and so do the session params and the record trio on a document-bound call. mod:: calls are covered by their module definitions, silo::/ml:: names by their version tails, and builtin paths (like an api middleware head named string::uppercase) resolve without any definition.

Ordering is not a lint subject: the compiler owns emission order — definitions may stand anywhere, a true cycle is the SDL-E0015 diagnostic, a dependency that crosses a scope-section boundary is SDL-E0016, and a duplicate definition is SDL-E1062 (see Who owns the order). The linter's remaining reference concern is the one the compiler cannot decide: a target that nothing in the document defines.

The catalog

codewarning
SDL-L0001table permissions: full; — anyone, even unauthenticated users, can access every record
SDL-L0002plaintext password:
SDL-L0003secret-less user — define-once, probably a placeholder
SDL-L0004edgengram/ngram min > max
SDL-L0005duplicate entry in roles / tokenizers / filters
SDL-L0006relation without enforced;
SDL-L0007top-level field/index/event that could nest inside its table
SDL-L0008param without a value
SDL-L0009api with no effective any catch-all (or no groups)
SDL-L0010record-typed in/out field rewriting a relation's endpoints
SDL-L0011access duration side the access's capability silently drops
SDL-L0012raw body the pinned SurrealQL parser rejects
SDL-L0013applying a '[REDACTED]' sentinel as real credential material
SDL-L0014table delete: full; — cannot round-trip
SDL-L0015computed field shadowed by a field defined under it
SDL-L0016reference; on a field whose type isn't record-shaped
SDL-L0017id field type can't be a record-id key
SDL-L0018flexible; on a non-SCHEMAFULL host (where it does nothing)
SDL-L0019inline; field whose host isn't a relation table
SDL-L0020table carrying more than 255 inline; fields
SDL-L0021mixin no table spreads — it emits nothing anywhere
SDL-L0022a later spread's member shadows an earlier spread's — the later definition wins at expansion
SDL-L0023a surform-lint-allow: comment's own problem — unknown code, misspelled marker, or no statement to attach to
SDL-L0024a referenced table, analyzer, or fn:: path (or a fn::-body param) that nothing in the linted files defines
SDL-L0025RETIRED (2026-10-06) — the compiler owns emission order (see the basics); the number keeps its gap
SDL-L0026a diamond include — a file the composition already spliced, declared again (first inclusion wins)

Suppressing a warning at its site

A deliberate exception does not need to live with the noise: a // surform-lint-allow: comment names the codes it excuses, and it excuses them on ONE statement — the statement it precedes (a same-line comment after the ; works too, the one-liner's natural home):

verified: the linter reports nothing
this file lints
sdl
namespace acme;
database shop;

// A public reference table: world-readable by design.
// surform-lint-allow: SDL-L0001
table countries {
  field code: string;
  field name: string;
  permissions: full;
};

The allow is statement-scoped, not file-wide — moved onto another statement, it excuses nothing:

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

// surform-lint-allow: SDL-L0001
table settings {
  field key: string;
};

table orders {
  field id: int;
  permissions: full;
};
what Surform suggests
text
SDL-L0001 (spec/table.md)

  ! table `orders` in database `acme/shop`: permissions are `full` — anyone, even unauthenticated users, can access every record
    ,-[9:1]
  8 |     
  9 | ,-> table orders {
 10 | |     field id: int;
 11 | |     permissions: full;
 12 | |-> };
    : `---- `permissions: full` on this table
    `----
  help: `full` grants everyone — including unauthenticated connections — access to every record; omit the clause for the secure default (implicit `none`), or scope operations in a `permissions`
        block

The orders table warns — its statement carries no allow. Codes are comma-listed on one line (// surform-lint-allow: SDL-L0001, SDL-L0006), and the comment rides its anchor through fmt and normalize, so the suppression survives reformatting. The same-line form after a one-liner's ;:

verified: the linter reports nothing
this file lints
sdl
namespace acme;
database shop;

table person {
  field name: string;
};

field nickname: string { on: person; }; // surform-lint-allow: SDL-L0007

A typo'd code suppresses nothing, so it gets its own warning instead of silence:

real Surform lint findings
this file lints
sdl
namespace acme;
database shop;

// surform-lint-allow: SDL-L0099
table countries {
  permissions: full;
};
what Surform suggests
text
SDL-L0023 (spec/README.md#the-linter)

  ! this allow comment names unknown lint code `SDL-L0099` — not in the registry (SDL-L0001 through SDL-L0026), so it suppresses nothing
   ,-[4:3]
 3 | 
 4 | // surform-lint-allow: SDL-L0099
   :   ^^^^^^^^^^^^^^^|^^^^^^^^^^^^^^
   :                  `-- unknown lint code
 5 | table countries {
   `----
  help: spell a registered code exactly, comma-separating several — the registry is SDL-L0001 through SDL-L0026

SDL-L0001 (spec/table.md)

  ! table `countries` in database `acme/shop`: permissions are `full` — anyone, even unauthenticated users, can access every record
   ,-[5:1]
 4 |     // surform-lint-allow: SDL-L0099
 5 | ,-> table countries {
 6 | |     permissions: full;
 7 | |-> };
   : `---- `permissions: full` on this table
   `----
  help: `full` grants everyone — including unauthenticated connections — access to every record; omit the clause for the secure default (implicit `none`), or scope operations in a `permissions`
        block

SDL-L0099 is not a code — the linter reports SDL-L0023 at the comment, and the table's SDL-L0001 stands (only registered codes suppress). Misspelled markers (surform-lint-alow:) and allow comments with no statement after them report SDL-L0023 too. Two limits are by design: a REGISTERED code that fires nothing on its statement stays silent (it may be guarding a future change), and findings the file carries no statement for (an adoptee-only table under --info) have no site to excuse — they never suppress. SDL-L0023 itself cannot be allowed away.

On a composition root the allow stays file-scoped: it excuses findings in its own file only, even though the other files' spans overlap numerically. Findings render against the file they came from.

Resolved state, not source text

Some warnings only make sense once the database's state is available. With --info tags, lint evaluates the resolved state — the merged, placed, elided file — so a table the database itself holds wide open warns even though the schema file never spells a permissions clause: the reader adopts the stored PERMISSIONS FULL into the merged file, and SDL-L0001 evaluates that:

sh
surform lint schema.surdl --info db=acme/shop=info-db.txt

Where next