Skip to content

Language Primitives

/

Data types

Named types

Available since: v4.0.0

A named type is a type declared once in a database with DEFINE TYPE, and then used by its name wherever a type is written: a field, a function argument or return value, a cast, or inside option<> and array<>. A value of a named type is coerced to the type and checked against its ASSERT clauses each time it is written. This page covers the three kinds of named type, how a union of strings orders its values, and casting.

The following statements declare a whole percentage, a ticket priority and a postal address, and use them as the types of fields.

DEFINE TYPE percent AS int CONSTRAINT in_range ASSERT $value >= 0 AND $value <= 100 DEFAULT 50;
DEFINE TYPE priority AS 'low' | 'medium' | 'high' | 'urgent';
DEFINE TYPE address AS { street: string, city: string, postcode: option<string> };

DEFINE TABLE ticket SCHEMAFULL;
DEFINE FIELD title ON ticket TYPE string;
DEFINE FIELD priority ON ticket TYPE priority;
DEFINE FIELD progress ON ticket TYPE percent;
DEFINE FIELD site ON ticket TYPE option<address>;

A write that the type refuses fails, and the error names the type and the part of it that failed: the body, an unnamed ASSERT, or a named CONSTRAINT.

CREATE ticket:1 SET title = 'Login fails', priority = 'critical';
//- "Couldn't coerce value for field `priority` of `ticket:1`: Expected `priority ('low' | 'medium' | 'high' | 'urgent')` but found `'critical'`"

CREATE ticket:1 SET title = 'Login fails', priority = 'urgent', progress = 140;
//- "Couldn't coerce value for field `progress` of `ticket:1`: Expected `percent (constraint in_range failed)` but found `140`"

A field written with no value takes the type's DEFAULT, unless the field has a DEFAULT of its own:

CREATE ticket:1 SET title = 'Login fails', priority = 'urgent';
//- [{ id: ticket:1, priority: 'urgent', progress: 50, title: 'Login fails' }]

The stored value has the type of the body, so type::of() returns 'int' for a percent and 'string' for a priority. A function can take and return a named type in the same way as a field:

DEFINE FUNCTION fn::half($p: percent) -> percent { $p / 2 };

fn::half(90);
//- 45

fn::half(200);
//- "Incorrect arguments for function fn::half(). Failed to coerce argument `$p`: Expected `percent (constraint in_range failed)` but found `200`"

A cast to a named type, such as <percent>, coerces the value to the body and runs the checks. It returns the value or fails with the same error as a write.

<percent> '42';
//- 42

<percent> 420;
//- "Could not cast into `percent`: Expected `percent (constraint in_range failed)` but found `420`"

<array<percent>> ['1', '2', 3];
//- [1, 2, 3]

<address> { street: '1 Main St', city: 'Oslo' };
//- { city: 'Oslo', street: '1 Main St' }

<priority> 'critical';
//- "Could not cast into `priority`: Expected `priority ('low' | 'medium' | 'high' | 'urgent')` but found `'critical'`"

A cast to a name that no type has fails when it runs, with an error that says the type does not exist.

A union of strings lists its values from the lowest to the highest. ORDER BY, the comparison operators <, <=, > and >=, and a range written in the query such as IN 'low'..='medium' compare the values of a field of the type by their position in that list. The aggregates math::min(), math::max(), array::min() and array::max() return the value that ranks lowest or highest. Sorted by spelling, the same values would come back as 'high', 'low', 'medium', 'urgent'.

-- Uses the ticket:1 record created above
CREATE ticket:2 SET title = 'Typo on home page', priority = 'low';
CREATE ticket:3 SET title = 'Slow search', priority = 'medium';

SELECT VALUE priority FROM ticket ORDER BY priority DESC;
//- ['urgent', 'medium', 'low']

SELECT VALUE title FROM ticket WHERE priority >= 'medium' ORDER BY title;
//- ['Login fails', 'Slow search']

SELECT VALUE title FROM ticket WHERE priority IN 'low'..='medium' ORDER BY title;
//- ['Slow search', 'Typo on home page']

SELECT math::max(priority) AS top, math::min(priority) AS bottom FROM ticket GROUP ALL;
//- [{ bottom: 'low', top: 'urgent' }]

A comparison with a string that the type does not list fails, instead of returning no records.

SELECT VALUE title FROM ticket WHERE priority > 'missing';
Error output
"'missing' is not a value of the type 'priority'; its values are 'low', 'medium', 'high', 'urgent'"

Declaration order applies only where SurrealDB knows that the value has the type, which is a field defined with it. A value on its own is a string and compares as one, so 'low' < 'high' is false. A comparison between fields of two different types, and a range held in a parameter, such as LET $r = 'low'..='medium', also compare by spelling. To compare values in the order of a type anywhere, use enum::rank().

A field of any other named type, such as percent, orders by its value in the usual way.

An index on a field of a union type is used for = and IN. A query that orders by the field, or compares it with < or >, sorts the records instead of reading them in index order.

A field that alone needs an ordered list of strings can list them in the type itself, as enum<'s', 'm', 'l'>. An inline enum checks its values and orders them in the same way as a union type, but it has no name, so ALTER TYPE cannot change it, and the enum functions take its values as an array instead of a name.

DEFINE FIELD size ON ticket TYPE option<enum<'s', 'm', 'l'>>;
CREATE ticket:5 SET title = 'Export fails', priority = 'high', size = 'xl';
Error output
"Couldn't coerce value for field `size` of `ticket:5`: Expected `none | enum<'s', 'm', 'l'>` but found `'xl'`"

A literal union such as 's' | 'm' | 'l' written directly on a field also restricts it to a list of strings, but compares its values by spelling. Use a named union type or an inline enum when the order of the values matters.

ALTER TYPE adds a value to a union at any position, or renames a value. A rename also rewrites every stored record and index entry that holds the old value, so existing records keep passing the type check. DEFINE TYPE OVERWRITE changes anything else about a type, but it does not check or rewrite stored records, and it cannot change the values of a union while a field or a function uses the type.

REMOVE TYPE fails while a field, a function or another type names the type.

Was this page helpful?