surformSurrealDB schemas, declared

Fields

Fields decide what a record may hold — and what the database does with values on the way in. The type sits right in the header (name: type); everything else is a clause in the block.

A field, standalone and nested

Inside a table block you write name: type. At top level the same field takes an on: line naming its table — both spellings emit the same DEFINE FIELD:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table account {
  schemafull;
  field email: string;
};

/// Contact address for the account owner.
field email: string {
  on: account;
  assert: string::is_email($value);
  default: 'unknown@example.com';
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE account TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE email ON account TYPE string;
DEFINE FIELD OVERWRITE email ON account TYPE string
    ASSERT string::is_email($value)
    DEFAULT 'unknown@example.com'
    COMMENT 'Contact address for the account owner.';

The nested spelling is the everyday one (the table page shows it in context); the standalone form is how the database itself reports fields, and it reads back into nested form on normalize.

Controlling values: default, value, assert

  • default: fills the value in when the create leaves it out.
  • default always: fills it in even when a value was supplied.
  • value: overrides whatever was supplied, every time.
  • assert: is a predicate every write must satisfy — $value is the incoming value, and throw inside lets you customize the error.
applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table ticket {
  schemafull;
  status: string {
    assert: $value inside ['open', 'closed'];
    default: 'open';
  };
  locked: bool {
    readonly;
    value: false;
  };
  tags: array<string> {
    default always: [];
  };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE ticket TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE status ON ticket TYPE string
    ASSERT $value INSIDE ['open', 'closed']
    DEFAULT 'open';
DEFINE FIELD OVERWRITE locked ON ticket TYPE bool READONLY VALUE false;
DEFINE FIELD OVERWRITE tags ON ticket TYPE array<string> DEFAULT ALWAYS [];

$value inside ['open', 'closed'] is ordinary SurrealQL — the full function library is available in every expression clause.

Computed fields

A computed field has no storage: its value is derived at read time. The short form is a header assignment — typed or typeless:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table account {
  schemafull;
  field name: string;

  shout: string = string::uppercase(name);
  derived = string::uppercase(name);
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE account TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE name ON account TYPE string;
DEFINE FIELD OVERWRITE shout ON account TYPE string COMPUTED string::uppercase(name);
DEFINE FIELD OVERWRITE derived ON account COMPUTED string::uppercase(name);

When a computed field needs other clauses, use the block form with computed: — a block can't follow the = form:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table account {
  schemafull;
  field name: string;

  secret_shout: string {
    computed: string::uppercase(name);
    permissions {
      select: none;
    };
  };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE account TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE name ON account TYPE string;
DEFINE FIELD OVERWRITE secret_shout ON account TYPE string
    COMPUTED string::uppercase(name)
    PERMISSIONS FOR select NONE;

Computed is mutually exclusive with default, value, assert, reference, and readonly — Surform rejects the combination:

real Surform diagnostics
this schema is wrong on purpose
sdl
namespace acme;
database shop;

table account {
  schemafull;
  field name: string;
  shout: string {
    computed: string::uppercase(name);
    readonly;
  };
};
what Surform reports
text
SDL-E1049 (spec/field.md)

  x invalid computed field: `computed` is mutually exclusive with `readonly` — a computed field is derived and takes no other clauses
    ,-[9:5]
  8 |     computed: string::uppercase(name);
  9 |     readonly;
    :     ^^^^|^^^
    :         `-- computed fields take nothing else
 10 |   };
    `----
  help: a computed field takes nothing else — drop the other clauses, or move the expression into a `computed:` field and keep `value`/`assert` on a regular one

References: what happens when the target dies

Any field whose type is a record can declare how SurrealDB reacts when the referenced record is deleted:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table post {
  schemafull;
  field author: record<user> {
    reference {
      on delete: cascade;
    };
  };
  field editor: record<user> {
    reference;
  };
  field parent: record<post> {
    reference {
      on delete: { create cleanup_log set at = time::now() };
    };
  };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE `post` TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE author ON `post` TYPE record<user> REFERENCE ON DELETE CASCADE;
DEFINE FIELD OVERWRITE editor ON `post` TYPE record<user> REFERENCE;
DEFINE FIELD OVERWRITE parent ON `post` TYPE record<post>
    REFERENCE ON DELETE THEN { CREATE cleanup_log SET at = time::now() };

reference; alone means ignore (the engine's default, and what it always prints back). The strategies are reject, cascade, ignore, unset — or a custom SurrealQL block that runs instead. References must sit on top-level record fields; the back-traversal <~ idiom lets the referenced side query its referrers, even as a computed field:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table person {
  schemafull;
  field comics: option<array<record<comic_book>>> {
    reference;
  };
};

table comic_book {
  schemafull;
  owners = <~person;
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE person TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE comics ON person TYPE option<array<record<comic_book>>> REFERENCE;
DEFINE TABLE OVERWRITE comic_book TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE owners ON comic_book COMPUTED <~person;

Permissions: fields start open

The mirror image of tables: unlisted operations are FULL for fields, and there is no delete operation (you can't delete one field of a record). Use the shorthand or per-operation groups:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table account {
  schemafull;
  secret: string {
    permissions: none;
  };
  audited: string {
    permissions {
      select: none;
      create, update: $auth.admin = true;
    };
  };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE account TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE secret ON account TYPE string PERMISSIONS NONE;
DEFINE FIELD OVERWRITE audited ON account TYPE string
    PERMISSIONS FOR select NONE,
    FOR create, update WHERE $auth.admin = true;

Inline payloads (SurrealDB 3.3)

inline; on a field of a relation table embeds that value into the edge-adjacency payloads, so filtered traversals like ->likes WHERE score > 5 run without fetching edge records:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table user->likes->post {
  schemaless;
  score: number {
    inline;
  };
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE likes TYPE RELATION IN `user` OUT `post` SCHEMALESS;
DEFINE FIELD OVERWRITE score ON likes TYPE number INLINE;

The host must be a relation table, the name must be top-level (not id/in/out, not dotted), and computed fields can't be inline — there is nothing stored to embed:

real Surform diagnostics
this schema is wrong on purpose
sdl
namespace acme;
database shop;

table page {
  schemafull;
  meta: object;
  meta.rating: number {
    inline;
  };
};
what Surform reports
text
SDL-E1058 (spec/field.md)

  x invalid INLINE field: only a top-level field can be INLINE — a dotted or wildcard path cannot embed into the adjacency payloads
   ,-[8:5]
 7 |   meta.rating: number {
 8 |     inline;
   :     ^^^|^^
   :        `-- this field cannot be INLINE
 9 |   };
   `----
  help: `inline;` embeds a field's value into its table's edge-adjacency payloads — it needs a TYPE RELATION host, a top-level non-`in`/`out` name, and a stored (not computed) value

A table carries at most 255 inline fields; the linter counts them for you (SDL-L0020).

GraphQL hints and paths

Fields take the same two GraphQL hints tables do, and field names may be paths — dotted subfields and .* wildcards are ordinary fields to SurrealDB:

applied to SurrealDB 3.3.0
schema.surdl
sdl
namespace acme;
database shop;

table profile {
  schemafull;
  display: string {
    graphql {
      alias: 'displayName';
    };
  };
  legacy: string {
    graphql {
      deprecated: 'use display';
    };
  };
  stats: object;
  stats.views: int {
    default: 0;
  };
  tags: array;
  tags.*: int;
};
surform build
sql
DEFINE NAMESPACE OVERWRITE acme;
USE NS acme;
DEFINE DATABASE OVERWRITE shop;
USE NS acme DB shop;
DEFINE TABLE OVERWRITE profile TYPE NORMAL SCHEMAFULL;
DEFINE FIELD OVERWRITE display ON profile TYPE string GRAPHQL_ALIAS 'displayName';
DEFINE FIELD OVERWRITE legacy ON profile TYPE string GRAPHQL_DEPRECATED 'use display';
DEFINE FIELD OVERWRITE stats ON profile TYPE object;
DEFINE FIELD OVERWRITE stats.views ON profile TYPE int DEFAULT 0;
DEFINE FIELD OVERWRITE tags ON profile TYPE array;
DEFINE FIELD OVERWRITE tags.* ON profile TYPE int;

Where next