Skip to content

Statements

/

DEFINE

DEFINE TYPE

Available since: v4.0.0

The DEFINE TYPE statement gives a type a name in the current database. A field, a function argument or return value, or a cast can then use that name in the same places as a built-in type such as int or string. A write is coerced to the type and checked against its ASSERT clauses. See Named types for how a named type behaves in a query.

  • You must be authenticated as a root owner or editor, namespace owner or editor, or database owner or editor before you can use the DEFINE TYPE statement.

  • You must select your namespace and database before you can use the DEFINE TYPE statement.

SurrealQL Syntax
DEFINE TYPE [ OVERWRITE | IF NOT EXISTS ] @name AS @type
    [ [ CONSTRAINT @name ] ASSERT @expression ] ...
    [ DEFAULT @expression ]
    [ SQL TYPE POSTGRESQL @string ]
    [ COMMENT @string ]
    [ PERMISSIONS [ NONE | FULL | WHERE @condition ] ]

The body after AS is a type, in one of three forms:

BodyExampleA value of the type
A union of stringsAS 'low' \| 'medium' \| 'high'Is one of the strings, and sorts in the order they are listed
An objectAS { street: string, city: string }Is an object with those fields
Any other typeAS int ASSERT $value > 0Is a value of that type that passes the checks

The body can name another type defined with DEFINE TYPE. It cannot name a type that does not exist yet, and it cannot lead back to the type being defined, either directly or through other types. A type cannot take the name of a built-in type such as string. PERMISSIONS defaults to FULL.

A union of string literals defines a fixed list of values, from the lowest to the highest. A field of the type accepts only those values, and sorts and compares them in that order.

DEFINE TYPE priority AS 'low' | 'medium' | 'high' | 'urgent' COMMENT 'Ticket priority, lowest first';
DEFINE FIELD priority ON ticket TYPE priority DEFAULT 'medium';

(INFO FOR DB).types;
Output
{
	priority: "DEFINE TYPE priority AS 'low' | 'medium' | 'high' | 'urgent' COMMENT 'Ticket priority, lowest first' PERMISSIONS FULL"
}

A value listed twice is kept once, in its first position:

DEFINE TYPE status AS 'open' | 'closed' | 'open';
(INFO FOR DB).types.status;
Output
"DEFINE TYPE status AS 'open' | 'closed' PERMISSIONS FULL"

ALTER TYPE adds values to a union at any position and renames them. The enum functions read the values of a union in order.

ASSERT takes an expression over $value, which holds the value after it has been coerced to the body. A write fails when a check returns false, and the checks run in the order they are written. CONSTRAINT names the check that follows it, and an error then reports the name instead of the expression.

DEFAULT is the value that a field of the type takes when a write gives it no value. A DEFAULT on the field itself takes precedence over the type's DEFAULT.

DEFINE TYPE percent AS int
    CONSTRAINT in_range ASSERT $value >= 0 AND $value <= 100
    DEFAULT 50;

DEFINE TABLE survey SCHEMAFULL;
DEFINE FIELD score ON survey TYPE percent;

CREATE survey:1;
//- [{ id: survey:1, score: 50 }]

CREATE survey:2 SET score = 140;
//- "Couldn't coerce value for field `score` of `survey:2`: Expected `percent (constraint in_range failed)` but found `140`"

A type built on another type runs the checks of both, those of the inner type first:

DEFINE TYPE grade AS percent ASSERT $value % 10 = 0;

<grade> 45;
//- "Could not cast into `grade`: Expected `grade (ASSERT $value % 10 = 0 failed)` but found `45`"

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

An object body lists the fields of an object, in the same way as an object literal type on a field. A field or a cast of the type checks each object against it, and refuses an object with a missing field or an extra one.

DEFINE TYPE address AS { street: string, city: string, postcode: option<string> };
DEFINE FIELD home ON person TYPE option<address>;

CREATE person:1 SET home = { street: '1 Main St' };
Error output
"Couldn't coerce value for field `home` of `person:1`: Expected `address ({ city: string, postcode: none | string, street: string })` but found `{ street: '1 Main St' }`"

Defining a type that already exists fails. With IF NOT EXISTS, the statement leaves the existing type unchanged and succeeds. With OVERWRITE, it replaces the type.

-- Uses the priority type defined above
DEFINE TYPE priority AS 'low' | 'high';
//- "The type 'priority' already exists"

DEFINE TYPE IF NOT EXISTS priority AS 'low' | 'high';
enum::values('priority');
//- ['low', 'medium', 'high', 'urgent']

OVERWRITE can change the body, the checks, the default, the comment and the permissions of a type that is in use. It does not check or rewrite stored records, so a record that the new definition refuses keeps its value, and its next write fails until the value is changed.

The values of a union are the exception. OVERWRITE with a different list, or with the same values in another order, fails while a field or a function names the type. To add or rename a value while the type is in use, use ALTER TYPE.

DEFINE TYPE OVERWRITE priority AS 'low' | 'high';
Error output
"Invalid statement: Cannot change the values of the type 'priority': the field 'priority' on the table 'ticket' has a type that names it"

Once no field or function names the type, OVERWRITE can set any list of values.

A type that names a type which does not exist, or that leads back to itself, is refused when it is defined. The same applies to a field or a function that names a missing type.

DEFINE TYPE node AS { next: option<node> };
//- "Invalid statement: The type 'node' cannot be defined: its body leads back to itself through the type 'node'"

DEFINE FIELD score ON survey TYPE missing;
//- "Invalid statement: The field 'score' on the table 'survey' cannot be defined: the type 'missing' it names does not exist"

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

INFO FOR DB lists each type under the types key as its DEFINE TYPE statement. INFO FOR DB STRUCTURE lists each type as an object, with the labels of a union in order, and each check with the name it was given:

-- In a database that has only the percent and priority types
(INFO FOR DB STRUCTURE).types;
Output
[
	{
		asserts: [{ check: '$value >= 0 AND $value <= 100', name: 'in_range' }],
		default: '50',
		kind: 'int',
		name: 'percent',
		permissions: true
	},
	{
		asserts: [],
		comment: 'Ticket priority, lowest first',
		kind: "'low' | 'medium' | 'high' | 'urgent'",
		labels: ['low', 'medium', 'high', 'urgent'],
		name: 'priority',
		permissions: true
	}
]

SQL TYPE POSTGRESQL records the type that a client of the PostgreSQL wire protocol sees for the type. SurrealQL does not read it. A CREATE DOMAIN or a CREATE TYPE sent over that protocol defines a type with this clause set.

DEFINE TYPE price AS decimal ASSERT $value >= 0dec SQL TYPE POSTGRESQL 'numeric(10,2)';

Was this page helpful?