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:
Let’s see how to define the schema in TypeQL with the PERA model in mind:
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
$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
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
:
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.
{
"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
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:
define
action sub entity,
owns name,
plays permission:permitted-action;
admin sub user;
permission relates permitted-action;
#5: Data insertion with matching
Now let’s do a match insert by adding a new file and a relevant permission for an existing user:
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
.
#6: Data deletion
Run this query after query #1, query #2, and query #5: Data insertion with matching.
Deleting data requires matching the data to delete first:
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
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.
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
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:
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.
#9: Parametric polymorphism
You can use polymorphic queries, including:
-
Parametric polymorphism (see below)
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:
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
Inheritance polymorphism lets us get more results using type hierarchy. Match a supertype can match its subtypes.
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
Interface polymorphism lets us query for everything with a specific property, for example, owning an attribute.
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
Inheritance lets subtypes use roles and attributes, defined for their supertype (direct or nested).
Like in the following 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:
{
"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.
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
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:
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.
#15: Globally unique immutable attributes
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.
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.
#16: Attribute annotations
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
$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 ausername
, which violates the@key
constraint from the schema; -
the second
user
hasusername
of the valueBob
, which happens to be non-unique, as there is anotheruser
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:
[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.
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> [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
Use the like
keyword with a regular expression to set constraints for an attribute value in a match
clause.
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.
{
"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
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:
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.
#20: Rule-based inference
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.
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:
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:
The inferred data is highlighted with green color.
#22: Rules chaining
Rules can be applied one after another in a chain.
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:
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:
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.
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:
user
→ Group A
→ Group 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
$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 ofGroup B
-
Group B
is a member ofGroup C
-
User
Alice
is a member ofGroup A
-
User
Bob
is a member ofGroup B
-
User
Charlie
is a member ofGroup 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.
Alice
→ Group A
→ Group B
→ Group C
Using inference, we can infer the following data from a database with the inserted data from the query above:
Notice the four inferred relations in green color.