TypeDB Fundamentals Lecture Series: from Nov 16 to Jan 9

TypeDB in 25 queries

To explore the capabilities of TypeDB and TypeQL, check below the list of top 25 TypeQL queries on TypeDB.

Use our Quickstart guide to find out how to create a new database and send a query.

#1: Schema definition

TypeDB uses the Polymorphic Entity-Relation-Attribute (or PERA) Model for its schemas and data.

Entities, relations, and attributes are all first-class citizens in a data model allowing for expressive modeling without normalization or reification. There are three root types: entity, relation, and attribute; and they can be subtyped to create user-defined types.

Consider the following schema diagram:

TypeDB Studio GUI

Let’s see how to define the schema in TypeQL with the PERA model in mind:

Schema example
define

id sub attribute, abstract, value string;
name sub id;
username sub id;
email sub id;
path sub id;
size-kb sub attribute, value long;
updated sub attribute, value datetime;

person sub entity,
    owns name;
user sub person,
    owns email @unique,
    owns username @key,
    plays permission:subject;
file sub entity,
    owns path @key,
    owns size-kb,
    plays permission:object;

permission sub relation,
    owns updated,
    relates subject,
    relates object;

The above query defines seven attribute types, three entity types, and a relation type.

Note the user type, that owns email and username attribute types.

To try extending the schema, see the #4: Extending a schema section.

#2: Data insertion

Run this query after query #1.

All data stored in a database must be instantiated from types defined in a schema of a database.

Let’s insert two users, two files, and set permissions:

Insert query example
insert
$p isa person, has name "Charlie";
$u1 isa user,
    has name "Bob",
    has username "bob_93",
    has email "bob@vaticle.com";
$u2 isa user, has username "al-capucino";
$f1 isa file, has path "README.md";
$f2 isa file,
    has path "docs/quickstart-guide.adoc",
    has size-kb 3458761;
$p1(subject:$u1, object:$f1) isa permission, has updated 2023-10-27T12:04:36;
$p2(subject:$u2, object:$f2) isa permission;

The above query inserts one instance of the person type, two user entities, two file entities, two relations of the permission type, and some attributes owned by the entities and one of the relations.

To try matching existing data before inserting, check the #5: Data insertion with matching section.

#3: Data retrieval

Run this query after query #1 and query #2.

Patterns for data queries are based on types defined in a schema. To retrieve usernames of all users that have permission for the file with the path README.md:

Fetch query example
match
$f isa file, has path "README.md";
$u isa user;
($u, $f) isa permission;
fetch
$u: username;

The above Fetch query matches the file entity by the path attribute it has ownership of. Then it finds all users ($u) that participate in a relation of a permission type with the file. Finally, it fetches values of username type attributes owned by such users.

Fetch query result example
{
    "u": {
        "username": [ { "value": "bob_93", "value_type": "string", "type": { "label": "username", "root": "attribute" } } ],
        "type": { "label": "user", "root": "entity" }
    }
}

All queries are validated both syntactically and semantically. Try adding to the query above a constraint of user having a path or file having a username. The modified query will not pass validation and will result in an error instead of showing no matched results.

#4: Extending a schema

Run this query after query #1 and query #2.

A schema of a TypeDB database can be extended at any time without the need to rewrite existing queries. The easiest way to extend the schema is to add a new type, add an ownership of an attribute type, add a role, or add an ability to play a new role with a Define query.

Let’s add a new subtype and a new role to our schema:

Schema extensions example
define

action sub entity,
    owns name,
    plays permission:permitted-action;

admin sub user;

permission relates permitted-action;

The above Define query extends the existing schema by adding two new entity types and a role to an existing relation type. Note that you can still run query #2 or query #3 with the extended schema.

#5: Data insertion with matching

Run this query after query #1 and query #2.

Now let’s do a match insert by adding a new file and a relevant permission for an existing user:

Match insert example
match
$u isa user;
$f isa file, has path "README.md";
not { ($u, $f) isa permission; };
insert
($u, $f) isa permission;

The above query matches the file with path README.md and all users, that are not in a permission relation. Then it inserts such a relation for every matched user and file.

The Insert query type is the only one that can be used without a match clause. See query #2.

#6: Data deletion

Deleting data requires matching the data to delete first:

Delete permissions
match
$u isa user, has username "al-capucino";
$p(subject:$u) isa permission;
delete
$p isa permission;

The above query will delete all permission relations for the user with username al-capucino.

#7: Composable patterns

Run this query after query #1 and query #2.

TypeQL statements are fully declarative. They can be combined in any order to form a query pattern. You can think of these patterns as connecting individual statements with a logical AND.

Every statement is a constraint to be satisfied by the query engine. You declare constraints for the results, and the query engine will deal with the implementation, including planning and optimizing execution.

Variables indicate unknowns that should be used somewhere else in the pattern or retrieved by the query. Every matched result is a solution for the match clause pattern: it includes a single concept (a type or an instance of a type) for every variable.

For a Fetch query we use a match clause to declare a pattern for the data we are looking for and a fetch clause to retrieve values.

Composable pattern example
match
$p isa person;
$p has name $p-name;
$p-name == "Bob";
$p has $x;
not {$x == $p-name;};
fetch $x;

This query matches only the person that owns the attribute with type name and value Bob and all their owned attributes ($x), excluding the name Bob. Then the fetch clause retrieves all matched attributes ($x). For the data from query #2 it should return username bob_93 and email bob@vaticle.com.

For the match clause pattern we included five simple statements. All five of them must be met for the matching.

Note that while the query uses the person type in its pattern, the matched entity is of the user type. This is due to the fact, that we used isa keyword, that takes into account all subtypes of the person type, including the user type. Try the same query with the isa! keyword instead. It is used for an exact type without its subtypes.

For more information on using subtypes, see query #10: Inheritance polymorphism and query #12: Inheritance.

#8: Abstract types

Run this query after query #1 and query #2.

Abstract types can’t be instantiated (no data can be inserted for the abstract type), but can be subtyped.

In schema from query #1 we defined id as an abstract type to subtype it with many different attributes, that have the same value type and can be called an id.

Now we can do the following query:

Abstract type example
match $x has id $id;
get $x, $id;

The above Get query can’t retrieve any instances of id type because it is an abstract type. Instead, it returns instances of all its subtypes, see the visualization below.

Abstract type

We use the Get query type, because it returns all concepts from a database as a ConceptMap, so that TypeDB Studio can build a graph visualization of the response (see above), while Fetch returns only values in a JSON.

#9: Parametric polymorphism

Run this query after query #1 and query #2.

You can use polymorphic queries, including:

Parametric polymorphism lets us retrieve results of different types for the same variable in a query. Querying without constraining a type can return any type that matches a pattern.

Let’s try one of the most generic patterns in TypeQL:

Parametric polymorphism query example
match $x isa! $t;
get $x, $t;

The above Get query returns pairs of $x and $t concepts, where $x should be an instance of data and $t should be its type. Note how the type in the isa! statement is variablized. Effectively this query returns all data from a database (as $x) and all corresponding types of the data (as $t). The number of results should match the number of instances the database has after query #1 and query #2.

#10: Inheritance polymorphism

Run this query after query #1 and query #2.

Inheritance polymorphism lets us get more results using type hierarchy. Match a supertype can match its subtypes.

Inheritance polymorphism query example
match $x isa entity;
get $x;

The Get query above matches $x by its type. The type must be the entity type or any of its subtypes. This query should return all entities in a database. Since entity is a root type, it’s an abstract type and can’t be directly instantiated. So the query returns instances of every subtype of the entity type.

You can easily avoid inheritance polymorphism by restricting subtypes to be used in the query by using isa! (with an exclamation mark).

#11: Interface polymorphism

Run this query after query #1 and query #2.

Interface polymorphism lets us query for everything with a specific property, for example, owning an attribute.

Interface polymorphism query example
match $x has name $name;
get $x, $name;

The above query matches every instance, that has any name. It returns pairs of owning instance ($x) of any type and the name attribute it owns ($name). The same approach could be used with playing a role in a relationship.

#12: Inheritance

Run this query after query #1 and query #2.

Inheritance lets subtypes use roles and attributes, defined for their supertype (direct or nested).

Like in the following example:

Inherited attributes example
match
$u isa user, has name "Bob";
fetch
$u as Bob: attribute;

The above query matches the user with name Bob. And then fetches all attributes owned by the user. The result should look like the following JSON:

Inherited attributes result example
{
    "Bob": {
        "attribute": [
            { "value": "bob@vaticle.com", "value_type": "string", "type": { "label": "email", "root": "attribute" } },
            { "value": "Bob", "value_type": "string", "type": { "label": "name", "root": "attribute" } },
            { "value": "bob_93", "value_type": "string", "type": { "label": "username", "root": "attribute" } }
        ],
        "type": { "label": "user", "root": "entity" }
    }
}

The user type inherits the ability to own the name attribute type from its supertype — the person type. See the schema from the query #1.

#13: Overriding inheritance

You can override an inherited ownership or a role with a new name. Override uses the keyword as with the new type preceding the keyword, and old (inherited) type following the keyword. The new type should be defined in the schema as a subtype of the overriden (inherited) type.

Schema
define

name sub attribute, abstract, value string;
full-name sub name;
path sub attribute, value string;

person sub entity, abstract,
    owns name;
user sub person,
    owns full-name as name,
    plays permission:subject;
file sub entity,
    owns path,
    plays permission:object;

permission sub relation,
    relates subject,
    relates object;

user-permission sub permission,
    relates user as subject;

In the query above we override the inherited name attribute type with the full-name attribute type. We also subtype permission relation type with the user-permission, which overrides the inherited subject role with the user role. The object role is inherited without overriding in this case.

In this particular example we have to make name an abstract type as only abstract attribute types can be subtyped. And only abstract types can own abstract attribute types, hence, person is also an abstract type in this example.

#14: N-ary relations

Run this query after query #1, query #2, and query #4.

Relations in TypeDB work elegantly and naturally, without the need for joins, tables, foreign keys, or any other tricks needed to "just make it work".

A database schema defines relation types and their roles, as well as types that can play a role. Creating a relation is just instantiating a relation type with the exact role players that you want.

We can create an n-ary relation with just one role, for example, a friendship relation with three role players for the role friend.

In #4: Extending a schema section we extended the schema by adding a third role to the permission relation. Now let’s add a role player for this role for a relation, that was inserted before the extension of the schema:

Relation example
match
$u isa user, has name "Bob";
$f isa file, has path "README.md";
$p($u, $f) isa permission;
insert
$a isa action, has name "edit";
$p(permitted-action:$a);

The above query adds action entity with name edit to the permission for the user with name Bob and the file with the path README.md. See the visualization of the result below.

Relation

#15: Globally unique immutable attributes

Run this query after query #1, query #2, and query #4.

Storage space and memory consumption are optimal as data is naturally deduplicated.

Attribute type is any subtype of the attribute root type, including nested subtypes. An instance of an attribute type is called attribute.

Only an attribute can have a value. An attribute can be identified by its type and value. Hence, there can be no other attribute of the same type with the same value.

Attribute values are immutable. We can’t change the value of an attribute, but we can delete ownership of one attribute and insert ownership of another. For example, a person can change its name by deleting the ownership of the old name attribute and adding a new ownership of the attribute of type name with the new value.

Insert data
match
$p isa person, has name "Charlie";
insert
$p has name "Bob";
$b isa person, has name "Bob", has name "Another Bob";
$a isa action, has name "Bob";

The above query matches the person with name Charlie, and then inserts ownership of the name Bob to the person. The same query also inserts new person with name Bob and another name Another Bob. It also creates an action with the name Bob.

The trick is, there is only one attribute of the type name and value Bob and all owners from the query above have ownership of this attribute. As well as the original Bob inserted in the query #2. The resulted data should look like the image below.

Relation

#16: Attribute annotations

Run this query after query #1 and query #2.

When defining an ownership of an attribute type, we can add one of possible annotations: @key or @unique. These annotation constraints are limiting what can be inserted into a database.

The @unique annotation makes the ownership constrained by uniqueness. Among all instances of the owner type only one can own any specific attribute of the attribute type marked with @unique annotation.

The @key annotation makes the owned attribute a key for the owner type. That applies the uniqueness constraint and, in addition, imposes a cardinality of exactly one — all instances of the owner type must have exactly one (no less and no more) instance of the owned attribute.

Consider the schema from the query #2.

In the schema we defined the username type to be a key for the user type, and the email to be unique, when owned by the user type.

Now let’s see what will happen, if we’ll try to violate these annotations by using the following Insert query:

Insert data
insert
$u1 isa user;
$u2 isa user, has username "Bob";
$u3 isa user,
    has username "Totally-not-Bob",
    has email "bob@vaticle.com";

The above query fails as it violates the following annotation constraints:

  • the first inserted user doesn’t have a username, which violates the @key constraint from the schema;

  • the second user has username of the value Bob, which happens to be non-unique, as there is another user owning the same attribute in the database already;

  • the third user has correct username, but also has non-unique email, which is forbidden by the @unique constraint.

As a result, we have the following error:

Error message
[THW04] Invalid Thing Write: Attempted to assign a key of type 'name' onto a(n) 'user' that already has one.

#17: Limiting values with regex

We can limit possible values that an attribute can take by using a regular expression in the type definition.

Schema with regex example
define

status sub attribute, value string, regex "^(STARTED|STOPPED|DELETED)$";

task sub entity,
    owns status;

The status attribute type has a value type of string, but its value is limited by a regular expression. That regular expression permits only the following exact variants of values: STARTED, STOPPED, or DELETED.

If a query tries to create an instance of the status type with, for example, the value Created, it invokes the following error:

Error message
Error> [THW11] Invalid Thing Write: Attempted to put an instance of 'status' with value 'Created' that does not satisfy the regular expression '^(STARTED|STOPPED|DELETED)$'.

#18: Matching values with regex

Run this query after query #1 and query #2.

Use the like keyword with a regular expression to set constraints for an attribute value in a match clause.

Query with regex example
match
$f isa file, has path $p;
$p like "^docs/.*$";
fetch $p;
users: {
    match ($u,$f) isa permission;
    fetch $u: username;
};

The query above matches all files with a path fitting the RegEx ^docs/.*$ (any string, starting with the docs/ substring) and fetches their path ($p).

It also has users subquery that fetches usernames of all users, that have a permission to access the matched file.

Result example
{
    "p": { "value": "docs/quickstart-guide.adoc", "value_type": "string", "type": { "label": "path", "root": "attribute" } },
    "users": [
        {
            "u": {
                "username": [ { "value": "al-capucino", "value_type": "string", "type": { "label": "username", "root": "attribute" } } ],
                "type": { "label": "user", "root": "entity" }
            }
        }
    ]
}

#19: Arithmetic

Run this query after query #1 and query #2.

Arithmetic expressions let you perform basic math operations on any value in a match clause.

See the list of available arithmetic operations.

Let’s insert a few files with different sizes, that are calculated:

Insert files with file sizes
match
$f isa file, has path "docs/quickstart-guide.adoc", has size-kb $s;
?x = ($s * 2) + 200;
?y = abs ((?x * 3) - 1);
insert
$f1 isa file, has path "config.yaml", has size-kb ?x;
$f2 isa file, has path "logs.zip", has size-kb ?y;

The above query inserts two files with size-kb values calculated based on the value of the size-kb of the matched existing file. See the resulted data visualization on the image below.

Arithmetics

#20: Rule-based inference

Run this query after query #1 and query #2.

TypeDB reasoning engine can perform rule-based inference when you read data. To use inference, we need to Define rules in the schema, open a read transaction with the infer option enabled, and send a Get or a Fetch query.

Schema with a rule
define

rule every-person-is-a-dude: when {
    $p isa person;
} then {
    $p has name "Dude";
};

The above query adds exactly one rule: it should add an ownership of the attribute of type name and value Dude for any person. Effectively, all instances of person type or its subtypes will have name Dude.

#21: Inferring new data

Run this query after query #1, query #2, and query #20: Rule-based inference.

Let’s use the every-person-is-a-dude rule from the previous query #20: Rule-based inference:

Retrieve all names for the person
match
$p isa person,
    has name "Bob",
    has name $p-name;
get $p, $p-name;

The above query produces the following result with inference enabled:

Inference

The inferred data is highlighted with green color.

#22: Rules chaining

Run this query after query #1 and query #2.

Rules can be applied one after another in a chain.

Schema
define

size-mb sub attribute, value long;
size-gb sub attribute, value long;

file owns size-mb,
    owns size-gb;

rule compute-mb: when {
    $f isa file,
        has size-kb $kb;
    ?mb = round($kb / 1024);
} then {
    $f has size-mb ?mb;
};

rule compute-gb: when {
    $f isa file,
        has size-mb $mb;
    ?gb = round($mb / 1024);
} then {
    $f has size-gb ?gb;
};

The above schema adds two rules and necessary attributes. The first rule uses arithmetic to set value for size-mb depending on the value of size-kb. The second rule uses the same approach, but based on the results from first rule to compute size-gb.

#23: Inferring with chaining

Run this query after query #1, query #2, and query #22: Rules chaining.

Let’s use the rules from the previous query #22: Rules chaining:

Retrieve the result of a rule chain
match
$f isa file,
    has size-gb $g,
    has path $p;
get $f, $g, $p;

The above query produces the following result with inference enabled:

Inference with rule chaining

The inferred data is highlighted with green color.

#24: Relation transitivity

Run this query on an empty database.

Rules can enable transitivity of relations.

Transitivity lets you expand relations when certain conditions are met. The most general case is: given a relation from A to B, and a relation from B to C, we can imply a relation from A to C.

Using rules for transitivity can greatly simplify your TypeQL queries.

Schema
define

name sub attribute, value string;

group-membership sub relation,
    relates group,
    relates member;

user sub entity,
    owns name,
    plays group-membership:member;

user-group sub entity,
    owns name,
    plays group-membership:group,
    plays group-membership:member;

rule transitive-group-membership: when {
  (group: $g1, member: $g2) isa group-membership;
  (group: $g2, member: $u) isa group-membership;
} then {
  (group: $g1, member: $u) isa group-membership;
};

The above schema defines group-membership relation with two roles: group and member. It also defines user that can play a member role and a user-group, that can play both roles. Finally, we have a rule transitive-group-membership that adds transitivity for the group-membership relation.

We can include Group A in Group B, so that every member of Group A will become a member of Group B through the transitivity of membership, as follows:

userGroup AGroup B

Transitivity rules work for any number of steps/nested levels.

For more information on rule inference, see the Define rules page in TypeQL documentation.

#25: Inserting transitive relations

Run this query after query #1, query #2, and query #24: Relation transitivity.

To try relation transitivity, insert the following sample data for the schema in the previous query:

Insert sample data for relation transitivity
insert
$u1 isa user, has name "Alice";
$u2 isa user, has name "Bob";
$u3 isa user, has name "Charlie";
$uga isa user-group, has name "Group A";
$ugb isa user-group, has name "Group B";
$ugc isa user-group, has name "Group C";
(group:$ugb, member:$uga) isa group-membership;
(group:$ugc, member:$ugb) isa group-membership;
(group:$uga, member:$u1) isa group-membership;
(group:$ugb, member:$u2) isa group-membership;
(group:$ugc, member:$u3) isa group-membership;

In the query above, we insert three users, three user groups and assign a membership in the following way:

  • Group A is a member of Group B

  • Group B is a member of Group C

  • User Alice is a member of Group A

  • User Bob is a member of Group B

  • User Charlie is a member of Group C

By the transitive-group-memmbership rule from the query #24: Relation transitivity, the user Alice should be a member of all groups: A, B, and C.

AliceGroup AGroup BGroup C

Using inference, we can infer the following data from a database with the inserted data from the query above:

Relation transitivity

Notice the four inferred relations in green color.

Provide Feedback