Defining annotations
This page explains how to use define queries to define annotations for types and type traits.
Understanding annotations
Annotations define constraints and extra behavior for types and their traits, enabling more precise control over the data in the schema.
Think of type declarations as blueprints for constructing a building—they define the overall structure. Annotations, on the other hand, are the building codes and regulations that ensure every room, window, and door meets specific standards for safety and functionality.
For example, this schema definition is an overall description of what the database is about and what’s the structure of the data is expected:
define
entity content owns id;
entity page sub content,
owns page-id,
owns name;
entity profile sub page,
owns username;
entity user sub profile,
owns email,
owns phone,
owns language,
owns karma;
attribute id value string;
attribute page-id sub id;
attribute username sub page-id;
attribute name value string;
attribute email value string;
attribute phone value string;
attribute language value string;
attribute karma value double;
However, this schema allows the data to be quite messy:
-
There can coexist general
content
s and more generalpage
s,profile
s, anduser
s. -
id
s can be duplicated. -
Generally, there can be an unspecified number of
name
s for apage
and, thus, auser
. -
phone
andemail
values are not regulated. -
… and some other possible unanswered questions.
Different databases have different approaches for solving these issues. Some expect external coding for complex validations.
TypeDB aims to simplify the complexities of schema declaration and data control and uses its constraint system to cover the most necessary data limitations, allowing it to be a part of the schema: direct, flexible, and easy to read.
Annotations for constraints definition
Using annotations, it’s possible to define an unlimited number of cardinality, modality, and value constraints directly in the schema:
define
content @abstract, owns id @key;
page @abstract, owns name @card(0..);
profile @abstract, owns name @card(0..3);
user owns phone @regex("^\d{8,15}$") @unique;
attribute id @abstract;
attribute page-id @abstract;
attribute email value string @regex("^.*@\w+\.\w+$");
attribute language @independent;
Try referring to TypeQL Annotations to access the whole list of data constraints available in TypeDB and understanding what each of the annotations above means.
See the details
-
@abstract
makes a type abstract, not allowing it to have instances. -
@key
makesid
and all its subtypes unique, single, and mandatory identifiers of any instance of thecontent
. -
@card
puts a cardinality constraint, restricting the ofname
s for aprofile
. -
@regex
enforces a format for string data stored in allemail
s in the schema and allphone
s owned byuser
s. -
@unique
enforces alluser
'sphone
s to have unique values. -
@independent
allows storinglanguage
s without references from owners, preserving this limited (by the number of languages in the world) data for statistics.
By applying annotations, you can ensure that your database not only organizes information but also guarantees its correctness, consistency, and integrity.
Default constraints
Some constraints are mandatory and are generated automatically if no overriding value is specified.
Currently, the only auto-generated constraint is cardinality
, which is required for any owns
, relates
, or plays
trait.
Refer to @card annotation for more details.
Combining constraints
Constraints in TypeDB are cumulative, meaning that every constraint must be respected simultaneously. Constraints defined through annotations do not relax or interact with one another in a way that reduces their enforcement.
For instance, consider an attribute type’s value type with the following constraints (whether explicitly declared or inherited through subtyping):
-
Regular expression:
^.*@\w+\.\w+$
-
Regular expression:
^.*@typedb.com$
-
Values set:
(alice@typedb.org, bob@typedb.com, cal)
Combining these constraints, the only permissible value is bob@typedb.com
.
Redefining and overriding constraints
A single declaration cannot have multiple constraints defined of the same kind, which is also true for annotations.
Constraints and annotations can be combined as a result of subtyping and specialization, but a statement like type @card(X) @card(X) @card(X)
will always result in type @card(X)
, and type @card(X) @card(Y)
is an invalid definition.
A defined annotations, which value should be changed, can be redefined.
Overriding is an operation of hiding a default constraint value by defining explicit constraints.
Annotation kinds
Type annotations
The previous example contains a number of annotations, and some of them follow type declarations:
define
content @abstract;
page @abstract;
profile @abstract;
attribute id @abstract;
attribute page-id @abstract;
attribute language @independent;
These annotations relate to types as the whole and are generally simple: instances of these types should respect the defined constraints.
@abstract description
When defined for a type, the @abstract
annotation enforces the abstract
constraint, making the type abstract.
An abstract type cannot be a direct type for an instance.
Thus, a concrete subtype is required for instances creation, and an abstract type can be used as a query target to retrieve information about its subtypes.
@independent description
The @independent
annotation enforces the independent
constraint for an attribute type, allowing instances of the type to exist independently of their owner.
Value type annotations
Value type annotations are similar to type constraints, but are set for value types of attribute types:
define
attribute email value string @regex("^.*@\w+\.\w+$");
These annotations act similar to type annotations: values of these attributes should respect the defined constraints.
@regex description
The @regex
annotation adds a constraint on values of attributes of a given attribute type to be valid strings according to the regular expression.
Trait annotations
Similarly, trait annotations follow traits (owns
, relates
, plays
) definitions:
define
content owns id @key;
page owns name @card(0..);
profile owns name @card(0..3);
user owns phone @regex("^\d{8,15}$") @unique;
These are a little more complicated, but are more powerful. While type annotations and value type annotations target any instance of a type, these annotations target:
-
Owners and attributes in
has
. -
Relations, roles, and roleplayers in
links
.
By defining these annotations, only specific pairs of entities, relations, and attributes are affected or restricted, and you are flexible in combining these annotations to have an elegant and robust schema.
@card description
When defined, the @card
annotation overrides the default cardinality
with the specified arguments.
The cardinality
constraint regulates the number of traits a type instance can have.
@unique description
The @unique
annotation can be defined for an ownership to put the unique
constraint on it.
The unique
constraint means that no two instances (owners) of the owner type can have instances (attributes) of the attribute type with the same value.
That makes every owned attribute unique by value among all instances of the owner type and its subtypes.
Subtyping
Subtyping is a powerful instrument in TypeDB and becomes even more powerful with annotations. While it is possible to predict the effect of an annotations on a subtype based on its description, it can be a little tricky at first.
It is recommended to refer to type definition to understand how subtyping generally works before proceeding.
Please note that more details and examples of subtyping behavior is available on each annotation’s page. Visit these pages in case of struggles in understanding of the explanations below.
Type annotations
An instance should comply to all the annotations of its types (type and supertypes). The following are the explanations of subtyping behaviour based on the type annotations descriptions and the example from the first section:
@abstract subtyping
Let’s consider an instance of the user
.
It should comply to the abstract
constraints of:
-
content
-
page
-
profile
The constraint’s description restricts direct instances of types, and user
, while being a content
, a page
, and a profile
, has the user
as its direct type.
Thus, the constraint is satisfied, and this instance can exist.
At the same time, there can not be instances of the mentioned supertypes.
@independent description
Let’s consider 2 instances of a slavic-language sub language
.
They should comply to the independent
constraint of the language
.
The first attribute is owned by multiple owners, while the second is left unowned.
The constraint’s description allows independent attributes to exist without owners (or restricts the attributes to get deleted without owners). Thus, both attributes can exist.
However, the second attribute will get deleted if there will be no independent
constraints affecting its types.
Value type annotations
A value should comply to all the constraints of its attribute’s types (type and supertypes). The following are the explanations of subtyping behaviour based on the value type constraints descriptions and the example from the first section:
@regex subtyping
Let’s consider an instance of a typedb-email sub email, value string @regex("^.*@typedb\.com$")
and an instance of a personal-email sub email
.
In order to comply to the regex
constraints, values of both instances should be valid email addresses based on the email
's regular expression.
Additionally, the domain of typedb-email
's value should be "typedb.com", while personal-email
can be any ".", including "typedb.com".
Trait annotations
For a trait annotation <type label 1> <trait> <type label 2> @<annotation>
:
-
Every instance of
<type label 1>
or its subtypes owning/relating/playing<type label 2>
should comply to the<annotation>
's constraint. -
Every instance of
<type label 2>
or its subtypes owned/related/played by<type label 1>
should comply to the<annotation>
's constraint.
The details of the constraint define the actual meaning of this rule. The following are the explanations of subtyping behaviour based on the trait constraints descriptions and the example from the first section:
@card subtyping
Let’s rewind the schema for user owns name
ownership:
define
entity content;
entity page sub content, owns name @card(0..);
entity profile sub page, owns name @card(0..3);
entity user sub profile;
attribute name value string;
It is possible to extend this schema to any direction (e.g., by creating new concrete subtypes of the page
and the profile
), so let’s describe general rules based on this schema:
-
Not any
content
can have aname
. -
Only
page
s can have aname
. -
page
s generally can have an infinite number ofname
s. -
A
profile
can have an infinite number ofname
s based on thepage
s constraint. However, it should also comply to the constraint ofprofile
, which limits this number to up to threename
s. -
A
user
should comply to both cardinality constraints fromprofile
andpage
. Thus, it can have only up to three names.
Refer to @card annotation for a more complex example.
Common issues
Default constraints are implicitly set for defined traits whenever there is no explicitly defined constraint
E.g., the following schema requires @card(0..)
specification for page owns name
, because the default cardinality would not allow page
s and, thus, profile
s to have 2 or 3 name
s.
At the same time, user
s will only have two cardinality constraints for their names: 0..
and 0..3
as the ownership is inherited.
However, if you were to set a user owns name
again, a default cardinality constraint would be generated for this ownership, and user
would need to comply to three cardinality constraints for name
s.
define
entity page sub content, owns name @card(0..);
entity profile sub page, owns name @card(0..3);
entity user sub profile;