Constraints
Constraints give users fine-grained control to ensure data consistency. They can be defined on properties, links, object types, and custom scalars.
Standard constraints
Gel includes a number of standard ready-to-use constraints:
Enforce uniqueness (disallow duplicate values) | |
Custom constraint expression (followed by keyword | |
A list of allowable values | |
Maximum value numerically/lexicographically | |
Maximum value numerically/lexicographically (exclusive range) | |
Minimum value numerically/lexicographically | |
Minimum value numerically/lexicographically (exclusive range) | |
Maximum length ( | |
Minimum length ( | |
Regex constraint ( |
Constraints on properties
Example: enforce all User
objects to have a unique username
no longer than 25 characters:
type User {
required username: str {
# usernames must be unique
constraint exclusive;
# max length (built-in)
constraint max_len_value(25);
};
}
Constraints on object types
Constraints can be defined on object types. This is useful when the constraint logic must reference multiple links or properties.
Example: enforce that the magnitude of ConstrainedVector
objects
is no more than 5
type ConstrainedVector {
required x: float64;
required y: float64;
constraint expression on (
(.x ^ 2 + .y ^ 2) ^ 0.5 <= 5
# or, long form: `(__subject__.x + __subject__.y) ^ 0.5 <= 5`
);
}
The expression
constraint is used here to define custom constraint logic.
Inside constraints, the keyword __subject__
can be used to reference the
value being constrained.
Note that inside an object type declaration, you can omit __subject__
and simply refer to properties with the
leading dot notation (e.g. .property
).
Also note that the constraint expression are fairly restricted. Due
to how constraints are implemented, you can only reference single
(non-multi) properties and links defined on the object type:
# Not valid!
type User {
required username: str;
multi friends: User;
# ❌ constraints cannot contain paths with more than one hop
constraint expression on ('bob' in .friends.username);
}
Abstract constraints
You can re-use constraints across multiple object types by declaring them as abstract constraints. Example:
abstract constraint min_value(min: anytype) {
errmessage :=
'Minimum allowed value for {__subject__} is {min}.';
using (__subject__ >= min);
}
# use it like this:
scalar type posint64 extending int64 {
constraint min_value(0);
}
# or like this:
type User {
required age: int16 {
constraint min_value(12);
};
}
Computed constraints
Constraints can be defined on computed properties:
type User {
required username: str;
required clean_username := str_trim(str_lower(.username));
constraint exclusive on (.clean_username);
}
Composite constraints
To define a composite constraint, create an exclusive
constraint on a
tuple of properties or links.
type User {
username: str;
}
type BlogPost {
title: str;
author: User;
constraint exclusive on ((.title, .author));
}
Partial constraints
Constraints on object types can be made partial, so that they are not enforced
when the specified except
condition is met.
type User {
required username: str;
deleted: bool;
# Usernames must be unique unless marked deleted
constraint exclusive on (.username) except (.deleted);
}
Constraints on links
You can constrain links such that a given object can only be linked once by
using exclusive
:
type User {
required name: str;
# Make sure none of the "owned" items belong
# to any other user.
multi owns: Item {
constraint exclusive;
}
}
Link property constraints
You can also add constraints for link properties:
type User {
name: str;
multi friends: User {
strength: float64;
constraint expression on (
@strength >= 0
);
}
}
Link's "@source" and "@target"
You can create a composite exclusive constraint on the object linking/linked
and a link property by using @source
or @target
respectively. Here's
a schema for a library book management app that tracks books and who has
checked them out:
type Book {
required title: str;
}
type User {
name: str;
multi checked_out: Book {
date: cal::local_date;
# Ensures a given Book can be checked out
# only once on a given day.
constraint exclusive on ((@target, @date));
}
}
Here, the constraint ensures that no book can be checked out to two User
s
on the same @date
.
In this example demonstrating @source
, we've created a schema to track
player picks in a color-based memory game:
type Player {
required name: str;
multi picks: Color {
order: int16;
constraint exclusive on ((@source, @order));
}
}
type Color {
required name: str;
}
This constraint ensures that a single Player
cannot pick two Color
s at
the same @order
.
Constraints on custom scalars
Custom scalar types can be constrained.
scalar type username extending str {
constraint regexp(r'^[A-Za-z0-9_]{4,20}$');
}
Note: you can't use exclusive
constraints on custom scalar
types, as the concept of exclusivity is only defined in the context of a given
object type.
Use expression
constraints to declare custom constraints
using arbitrary EdgeQL expressions. The example below uses the built-in
str_trim()
function.
scalar type title extending str {
constraint expression on (
__subject__ = str_trim(__subject__)
);
}
Constraints and inheritance
If you define a constraint on a type and then extend that type, the constraint will not be applied individually to each extending type. Instead, it will apply globally across all the types that inherited the constraint.
type User {
required name: str {
constraint exclusive;
}
}
type Administrator extending User;
type Moderator extending User;
gel> .... ....
insert Administrator {
name := 'Jan'
};
{default::Administrator {id: 7aeaa146-f5a5-11ed-a598-53ddff476532}}
gel> .... ....
insert Moderator {
name := 'Jan'
};
gel error: ConstraintViolationError: name violates exclusivity constraint Detail: value of property 'name' of object type 'default::Moderator' violates exclusivity constraint
gel> .... ....
insert User {
name := 'Jan'
};
gel error: ConstraintViolationError: name violates exclusivity constraint Detail: value of property 'name' of object type 'default::User' violates exclusivity constraint
As this example demonstrates, if an object of one extending type has a value for a property that is exclusive, an object of a different extending type cannot have the same value.
If that's not what you want, you can instead delegate the constraint to the
inheriting types by prepending the delegated
keyword to the constraint.
The constraint would then be applied just as if it were declared individually
on each of the inheriting types.
type User {
required name: str {
delegated constraint exclusive;
}
}
type Administrator extending User;
type Moderator extending User;
gel> .... ....
insert Administrator {
name := 'Jan'
};
{default::Administrator {id: 7aeaa146-f5a5-11ed-a598-53ddff476532}}
gel> .... ....
insert User {
name := 'Jan'
};
{default::User {id: a6e3fdaf-c44b-4080-b39f-6a07496de66b}}
gel> .... ....
insert Moderator {
name := 'Jan'
};
{default::Moderator {id: d3012a3f-0f16-40a8-8884-7203f393b63d}}
gel> .... ....
insert Moderator {
name := 'Jan'
};
gel error: ConstraintViolationError: name violates exclusivity constraint Detail: value of property 'name' of object type 'default::Moderator' violates exclusivity constraint
With the addition of delegated
to the constraints, the inserts were
successful for each of the types. We did not hit a constraint violation
until we tried to insert a second Moderator
object with the same
name as the existing one.
Declaring constraints
This section describes the syntax to declare constraints in your schema.
Syntax
[{abstract | delegated}] constraint name [ ( [argspec] [, ...] ) ]
[ on ( subject-expr ) ]
[ except ( except-expr ) ]
[ extending base [, ...] ]
"{"
[ using constr-expression ; ]
[ errmessage := error-message ; ]
[ annotation-declarations ]
[ ... ]
"}" ;
where argspec is:
[ argname: ] {argtype | argvalue}
Description
This declaration defines a new constraint with the following options:
- abstract
-
If specified, the constraint will be abstract.
- delegated
-
If specified, the constraint is defined as delegated, which means that it will not be enforced on the type it's declared on, and the enforcement will be delegated to the subtypes of this type. This is particularly useful for
exclusive
constraints in abstract types. This is only valid for concrete constraints. - name
-
The name (optionally module-qualified) of the new constraint.
- argspec
-
An optional list of constraint arguments.
For an abstract constraint argname optionally specifies the argument name and argtype specifies the argument type.
For a concrete constraint argname optionally specifies the argument name and argvalue specifies the argument value. The argument value specification must match the parameter declaration of the abstract constraint.
- on ( subject-expr )
-
An optional expression defining the subject of the constraint. If not specified, the subject is the value of the schema item on which the concrete constraint is defined.
The expression must refer to the original subject of the constraint as
__subject__
. The expression must be Immutable, but may refer to__subject__
and its properties and links.Note also that
<subject-expr>
itself has to be parenthesized. - except ( exception-expr )
-
An optional expression defining a condition to create exceptions to the constraint. If
<exception-expr>
evaluates totrue
, the constraint is ignored for the current subject. If it evaluates tofalse
or{}
, the constraint applies normally.except
may only be declared on object constraints, and otherwise follows the same rules ason
. - extending base [, ...]
-
If specified, declares the parent constraints for this abstract constraint.
The valid SDL sub-declarations are listed below:
- using constr_expression
-
A boolean expression that returns
true
for valid data andfalse
for invalid data. The expression may refer to the subject of the constraint as__subject__
. This declaration is only valid for abstract constraints. - errmessage := error_message
-
An optional string literal defining the error message template that is raised when the constraint is violated. The template is a formatted string that may refer to constraint context variables in curly braces. The template may refer to the following:
-
$argname
– the value of the specified constraint argument -
__subject__
– the value of thetitle
annotation of the scalar type, property or link on which the constraint is defined.
If the content of curly braces does not match any variables, the curly braces are emitted as-is. They can also be escaped by using double curly braces.
-
- annotation-declarations
-
Set constraint annotation to a given value.
DDL commands
This section describes the low-level DDL commands for creating and dropping constraints and abstract constraints. You typically don't need to use these commands directly, but knowing about them is useful for reviewing migrations.
Create abstract constraint
Define a new abstract constraint.
[ with [ module-alias := ] module module-name ]
create abstract constraint name [ ( [argspec] [, ...] ) ]
[ on ( subject-expr ) ]
[ extending base [, ...] ]
"{" subcommand; [...] "}" ;
where argspec is:
[ argname: ] argtype
where subcommand is one of
using constr-expression
set errmessage := error-message
create annotation annotation-name := value
Description
The command create abstract constraint
defines a new abstract constraint.
If name is qualified with a module name, then the constraint is created in that module, otherwise it is created in the current module. The constraint name must be distinct from that of any existing schema item in the module.
Parameters
Most sub-commands and options of this command are identical to the SDL constraint declaration, with some additional features listed below:
- [ module-alias := ] module module-name
-
An optional list of module alias declarations to be used in the migration definition. When module-alias is not specified, module-name becomes the effective current module and is used to resolve all unqualified names.
- set errmessage := error_message
-
An optional string literal defining the error message template that is raised when the constraint is violated. Other than a slight syntactical difference this is the same as the corresponding SDL declaration.
- create annotation annotation-name := value;
-
Set constraint annotation
<annotation-name>
to<value>
. Seecreate annotation
for details.
Example
Create an abstract constraint "uppercase" which checks if the subject is a string in upper case:
create abstract constraint uppercase {
create annotation title := "Upper case constraint";
using (str_upper(__subject__) = __subject__);
set errmessage := "{__subject__} is not in upper case";
};
Alter abstract constraint
Alter the definition of an abstract constraint.
[ with [ module-alias := ] module module-name ]
alter abstract constraint name
"{" subcommand; [...] "}" ;
where subcommand is one of
rename to newname
using constr-expression
set errmessage := error-message
reset errmessage
create annotation annotation-name := value
alter annotation annotation-name := value
drop annotation annotation-name
Description
The command alter abstract constraint
changes the definition of an
abstract constraint item. name must be a name of an existing
abstract constraint, optionally qualified with a module name.
Parameters
- [ module-alias := ] module module-name
-
An optional list of module alias declarations to be used in the migration definition. When module-alias is not specified, module-name becomes the effective current module and is used to resolve all unqualified names.
- name
-
The name (optionally module-qualified) of the constraint to alter.
Subcommands allowed in the alter abstract constraint
block:
- rename to newname
-
Change the name of the constraint to newname. All concrete constraints inheriting from this constraint are also renamed.
- alter annotation annotation-name := value
-
Alter constraint annotation
<annotation-name>
. Seealter annotation
for details. - drop annotation annotation-name
-
Remove annotation
<annotation-name>
. Seedrop annotation
for details. - reset errmessage
-
Remove the error message from this abstract constraint. The error message specified in the base abstract constraint will be used instead.
All subcommands allowed in a create abstract constraint
block are also
valid here.
Drop abstract constraint
Remove an abstract constraint from the schema.
[ with [ module-alias := ] module module-name ]
drop abstract constraint name ;
Description
The command drop abstract constraint
removes an existing abstract
constraint item from the database schema. If any schema items depending
on this constraint exist, the operation is refused.
Parameters
- [ module-alias := ] module module-name
-
An optional list of module alias declarations to be used in the migration definition.
- name
-
The name (optionally module-qualified) of the constraint to remove.
Create constraint
Define a concrete constraint on the specified schema item.
[ with [ module-alias := ] module module-name ]
create [ delegated ] constraint name
[ ( [argspec] [, ...] ) ]
[ on ( subject-expr ) ]
[ except ( except-expr ) ]
"{" subcommand; [...] "}" ;
where argspec is:
[ argname: ] argvalue
where subcommand is one of
set errmessage := error-message
create annotation annotation-name := value
Description
The command create constraint
defines a new concrete constraint. It can
only be used in the context of create scalar
,
alter scalar
, create property
,
alter property
, create link
, or alter link
.
name must be a name (optionally module-qualified) of a previously defined abstract constraint.
Parameters
Most sub-commands and options of this command are identical to the SDL constraint declaration, with some additional features listed below:
- [ module-alias := ] module module-name
-
An optional list of module alias declarations to be used in the migration definition.
- set errmessage := error_message
-
An optional string literal defining the error message template that is raised when the constraint is violated. Other than a slight syntactical difference, this is the same as the corresponding SDL declaration.
- create annotation annotation-name := value;
-
An optional list of annotations for the constraint. See
create annotation
for details.
Example
Create a "score" property on the "User" type with a minimum value constraint:
alter type User create property score: int64 {
create constraint min_value(0)
};
Create a Vector with a maximum magnitude:
create type Vector {
create required property x: float64;
create required property y: float64;
create constraint expression ON (
__subject__.x^2 + __subject__.y^2 < 25
);
}
Alter constraint
Alter the definition of a concrete constraint on the specified schema item.
[ with [ module-alias := ] module module-name [, ...] ]
alter constraint name
[ ( [argspec] [, ...] ) ]
[ on ( subject-expr ) ]
[ except ( except-expr ) ]
"{" subcommand; [ ... ] "}" ;
-- or --
[ with [ module-alias := ] module module-name [, ...] ]
alter constraint name
[ ( [argspec] [, ...] ) ]
[ on ( subject-expr ) ]
subcommand ;
where subcommand is one of:
set delegated
set not delegated
set errmessage := error-message
reset errmessage
create annotation annotation-name := value
alter annotation annotation-name
drop annotation annotation-name
Description
The command alter constraint
changes the definition of a concrete
constraint. Both single- and multi-command forms are supported.
Parameters
- [ module-alias := ] module module-name
-
An optional list of module alias declarations for the migration.
- name
-
The name (optionally module-qualified) of the concrete constraint that is being altered.
- argspec
-
A list of constraint arguments as specified at the time of
create constraint
. - on ( subject-expr )
-
An expression defining the subject of the constraint as specified at the time of
create constraint
.
The following subcommands are allowed in the alter constraint
block:
- set delegated
-
Mark the constraint as delegated, which means it will not be enforced on the type it's declared on, and enforcement is delegated to subtypes. Useful for
exclusive
constraints. - set not delegated
-
Mark the constraint as not delegated, so it is enforced globally across the type and any extending types.
- rename to newname
-
Change the name of the constraint to
<newname>
. - alter annotation annotation-name
-
Alter a constraint annotation.
- drop annotation annotation-name
-
Remove a constraint annotation.
- reset errmessage
-
Remove the error message from this constraint, reverting to that of the abstract constraint, if any.
All subcommands allowed in create constraint
are also valid in
alter constraint
.
Drop constraint
Remove a concrete constraint from the specified schema item.
[ with [ module-alias := ] module module-name [, ...] ]
drop constraint name
[ ( [argspec] [, ...] ) ]
[ on ( subject-expr ) ]
[ except ( except-expr ) ] ;
Description
The command drop constraint
removes the specified constraint from
its containing schema item.
Parameters
- [ module-alias := ] module module-name
-
Optional module alias declarations for the migration definition.
- name
-
The name (optionally module-qualified) of the concrete constraint to remove.
- argspec
-
A list of constraint arguments as specified at the time of
create constraint
. - on ( subject-expr )
-
Expression defining the subject of the constraint as specified at the time of
create constraint
.
Example
Remove constraint "min_value" from the property "score" of the "User" type:
alter type User alter property score
drop constraint min_value(0);