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:
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';
};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 —$valueis the incoming value, andthrowinside lets you customize the error.
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: [];
};
};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:
namespace acme;
database shop;
table account {
schemafull;
field name: string;
shout: string = string::uppercase(name);
derived = string::uppercase(name);
};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:
namespace acme;
database shop;
table account {
schemafull;
field name: string;
secret_shout: string {
computed: string::uppercase(name);
permissions {
select: none;
};
};
};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:
namespace acme;
database shop;
table account {
schemafull;
field name: string;
shout: string {
computed: string::uppercase(name);
readonly;
};
};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:
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() };
};
};
};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:
namespace acme;
database shop;
table person {
schemafull;
field comics: option<array<record<comic_book>>> {
reference;
};
};
table comic_book {
schemafull;
owners = <~person;
};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:
namespace acme;
database shop;
table account {
schemafull;
secret: string {
permissions: none;
};
audited: string {
permissions {
select: none;
create, update: $auth.admin = true;
};
};
};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:
namespace acme;
database shop;
table user->likes->post {
schemaless;
score: number {
inline;
};
};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:
namespace acme;
database shop;
table page {
schemafull;
meta: object;
meta.rating: number {
inline;
};
};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:
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;
};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
- Types (kinds) — every type the header accepts.
- Relations — the tables
inline;fields attach to. - Indexes — indexes over these fields.