Officially out now: The TypeDB 3.0 Roadmap >>

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 general page s, profile s, and user s.

  • id s can be duplicated.

  • Generally, there can be an unspecified number of name s for a page and, thus, a user.

  • phone and email 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 makes id and all its subtypes unique, single, and mandatory identifiers of any instance of the content.

  • @card puts a cardinality constraint, restricting the of name s for a profile.

  • @regex enforces a format for string data stored in all email s in the schema and all phone s owned by user s.

  • @unique enforces all user 's phone s to have unique values.

  • @independent allows storing language 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):

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 a name.

  • Only page s can have a name.

  • page s generally can have an infinite number of name s.

  • A profile can have an infinite number of name s based on the page s constraint. However, it should also comply to the constraint of profile, which limits this number to up to three name s.

  • A user should comply to both cardinality constraints from profile and page. 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;