New ACM paper, free-tier cloud, and open-source license

Crash course

This crash course will give you a quick overview of the main ingredients for building your application with TypeDB. For an in-depth dive into these topics, check out our TypeDB Learning Course.

Key ingredients

TypeDB is a new kind of database that combines ideas from classical databases and modern high-level programming languages. To get started with TypeDB, let’s have a look at three key components and ideas underlying TypeDB’s design.

1. The type system. TypeDB is built on a rich type system enabling us to directly capture the structure and flow of the data we work with.

A simple schema with six inter-dependent types
define
  file sub entity, owns name, owns creation-date;
  image sub file, plays project:asset;
  video sub file, plays project:asset;
  project sub relation, relates asset, owns name;
  creation-date sub attribute, value datetime;
  name sub attribute, value string;

TypeDB’s conceptual data model can express polymorphic dependencies of types, which means we can easily combine various data structures including, e.g., relational, graph, and document-like data.

2. The querying paradigm. TypeDB approaches database management through a simple, pattern-based querying paradigm.

Find images and videos with the same creation date part of the same project, and output JSON
match
  $thumbnail isa image, has creation-date $d;
  $video isa video, has creation-date $d;
  ($thumbnail, $video) isa project;
fetch
  $thumbnail: name; $video: name;

Building on TypeDB’s conceptual data model, this provides us with a highly versatile toolset that resolves a range of deep engineering challenges. No tables, no joins, no nulls!

3. The power of patterns. Patterns are used in TypeDB to interact with data and types of all sorts.

Add files older than 1970 to the "archive" project
match
  $archive isa project, has name "archive";
  $old_file isa image, has creation-date <= 1970-01-01;
insert
  $archive (asset: $f);

Patterns often read just like statements in natural language, and can be easily composed to form new patterns. This makes many tasks that are arduous in traditional databases simple and elegant in TypeDB.

In the next sections, we dive deeper into each of the above topics. But note: this crash course covers only the very basics of TypeDB! For a more comprehensive overview, check out our Learning Course.

To follow the examples on this page, make sure to create a TypeDB database with sample schema and data for a permission system, as instructed in the Quickstart guide.

The type system

TypeDB is based on a novel conceptual data and querying model, called the PERA model. This modern database model was introduced in recent research, and aims to provide higher levels of flexibility and expressivity when working with structured data.

Schema and types

A TypeDB database has a schema, that describes type definitions. A new type can be added by subtyping an existing type. To begin, a new database has only three built-in types, called root types: these are the entity, relation, and attribute root type. Types subtyping these root types are called, respectively, entity types, relation types, and attribute types.

A schema will create a type hierarchy with three distinct trees and the root types as their roots. The properties of these types are summarized by the table below.

Table 1. Root types and their specific properties
Entity types Relation types Attribute types

Contain

objects

objects

values

Dependencies

none

roleplayers

owners

Interfaces

none

roles

ownership

Can implement interfaces

Can be subtyped

if abstract

Can be abstract

You will find an in-depth explanation of this table and much more in Lesson 9 of the Learning Course.

Data instances and values

Data can be inserted into a TypeDB database as instances of types from the database’s schema. The instances of entity types are called entities: they represent the independent objects in a business domain.

Relations (i.e., the instances of relation types) represent linkages between objects; which data can be linked to which other data is specified by the schema via roles of the relation type that other types can play.

Attributes (i.e., the instances of attribute types) represent properties that can be owned by other data instances, and always have an underlying value associated to them (e.g., and integer, string, or datetime). Which data instances can own which attributes is again specified by the schema, via ownerships.

The querying paradigm

All TypeQL queries use fully declarative and composable patterns to express the constraints on the database you need, without considering implementation details. This likewise applies to reading or writing either data instances or type definitions, and follows the spirit of "(infra)structure as code": you declare it, TypeDB does it!

Unlike other databases, there is no need to explicitly specify "data transformations" (like joins or other table/document transformations) and no need for thinking too hard about query optimizations, as the query engine will take care of query planning and execution.

Query types

TypeDB comes with a handful of basic query types.

These queries fall into TypeDB’s schema/transaction model in a natural way, as the table below illustrates.

Table 2. Query types
Read transaction Write transaction

Schema session

Fetch, Get

Define, Undefine

Data session

Fetch, Get

Insert, Delete, Update

Fetch queries

A Fetch query projects types and values of concepts matched by selected variables into JSON output. For example, let’s match a user by its username and fetch its name:

match
$u isa user, has username "bob_93";
fetch 
$u as Bob: name as "Name";

The above example uses customized output labels for the JSON output.

A Fetch query can be used with a subquery. For example, let’s add a subquery to fetch paths for all files that the user has access to:

match
$u isa user, has username "bob_93";
fetch 
$u as Bob: name as "Name";
permitted-files: {
    match
    $f isa file;
    ($u,$f) isa permission;
    fetch
    $f as file: path;
};

Get queries

A Get query returns stateful objects representing concepts matched by selected variables for you to programmatically manipulate them with driver API methods. For example, you can send the following Get query to get all file entities from a database:

match
$f isa file;
get $f;

Then, you can use ConceptMap returned for every matched result to retrieve concepts and manipulate them with driver API.

match
$f isa file, has size-kb $s;
get $f, $s;
mean $s;

Insert queries

An Insert query inserts data into a database. It can be used with or without a match clause, which can be used to retrieve existing data from the database.

We can use an insert clause without a match clause to insert completely new data in a database, that is not related to existing data. For example:

insert
$u isa user, has username "tom", has name "Tom", has email "tom@typedb.com";

The above example inserts a new user entity, with username, name, and email attributes owned. The inserted user has no connection to existing data in a database.

To insert new data that is connected to existing data, we may use a match clause. For example, to match a user from a database and insert a new email for this user, we can write the following:

match
$u isa user, has username "bob_93";
insert
$u has email "bob@gmail.com";

It is important to note that an insert clause is executed once for every tuple of results from the match clause, and inserts data using the context of these results.

Delete queries

A Delete query deletes data from a database. In a Delete query we first retrieve data using a match clause, and then, based on the matched results, specify a pattern that is to be deleted. For example, let’s match a specific user by its username and delete that the user has a given email:

match
$u isa user, has username "bob_93", has email $email;
$email == "bob@gmail.com";
delete
$u has $email;

In the delete clause, we specify a pattern of data that is to be deleted: the ownership of an email of a user. This does not delete the user or the attribute itself, however! Indeed, the email attribute can be potentially owned by some other data instances. In contrast, deleting the attribute will automatically delete all ownerships of it.

Like for Insert queries, the delete clause of a Delete query is executed exactly once for every matched result of the preceding match clause. If a match clause matches no results from a database, then there is nothing to delete.

Update queries

Update queries combine match, delete, and insert clauses to replace or otherwise update data stored in a database. For example, let’s match a user with the username bob_93 that has some email to replace such an email by a new one.

match
$u isa user, has username "bob_93", has email $email;
delete
$u has $email;
insert
$u has email "mr.bob@typedb.com";

Note, that if the user doesn’t have any email, then there will be no deletes or inserts. And if the user has multiple emails, each of them will be matched, deleted, and replaced by the same new email. Since there can be only one email attribute with the same value and it can be owned only once, any subsequent insert will produce the same result as the first one!

Schema extensions made easy

We can extend our schema by adding new types or adding new capabilities to existing types. Due to the typed pattern-based approach to database operations TypeDB, often query need to be manually adjusted to these changes but instead "adapt automatically".

First, we can modify or extend the schema of a database with a Define query. For example, let’s extend our sample schema for the user entity type to be able to own the new attribute type: rating.

Extending schema
define

rating sub attribute, value double;
user has rating;

Now, we add some rating data: let’s add one attribute for existing user with username bob_93 and one for a new user.

Adding rating data
match
$u1 isa user, has username "bob_93";
insert
$u1 has rating 0.99;
$u2 isa user, has username "not_bob", has rating "0";

Check one of the previous queries, for example, the Fetch query to get name of the user with the username bob_93. The results of existing queries are not affected in any way. We can easily modify the query to retrieve the new attribute:

Fetch rating
match
$u isa user, has username "bob_93";
fetch
$u: name, rating;

Of course, this example is quite simplistic. But the "modular" approach of type dependencies, via relation roles and attribute ownership, often makes schema modification extremely convenient. Moreover, this can be combined with built-in support for inheritance polymorphism or the usage of type variablization (both enabled by TypeDB’s type inference) to organically incorporate schema changes into existing queries.

The power of patterns

TypeQL uses declarative patterns to seek and manipulate data in a database. Every statement is a constraint to be satisfied by the query engine.

Variables indicate unknowns that can be matched by a concept from a database. Solving a pattern is like solving a system of equations, where every equation (statement) must be satisfied, and a solution (result) is a set of values for variables that make every equation in the system a True statement.

Matching patterns

Let’s see a simple example of a query that matches all data instances of the user type in a database and, for every matched instance, retrieves all their names and emails:

TypeQL query example
match
$u isa user;
fetch
$u: name, email;

The above is a Fetch query, with a match that matches data by the provided pattern and a fetch that projects every matched result as a JSON with types and values:

Output example
{
    "u": {
        "email": [  ],
        "name": [ { "value": "Alex", "type": { "label": "name", "root": "attribute", "value_type": "string" } } ],
        "type": { "label": "user", "root": "entity" }
    }
}
{
    "u": {
        "email": [ { "value": "bob@typedb.com", "type": { "label": "email", "root": "attribute", "value_type": "string" } } ],
        "name": [ { "value": "Bob", "type": { "label": "name", "root": "attribute", "value_type": "string" } } ],
        "type": { "label": "user", "root": "entity" }
    }
}

Composing patterns

TypeQL patterns consist of TypeQL statements, which can be combined in any order by an implicit conjunction (i.e., a logical "and") and other pattern operations like or (a logical "or") or not (a logical negation). Let’s modify the previous query’s pattern to retrieve a specific user who has name Bob:

Composable pattern example
match
$u isa user, has name $fn;
$fn == "Bob";
fetch
$u: name, email;

The first line of the pattern inside the match clause now specifies that user must have a name, while the second line adds a constraint for the value of the name to be equal to Bob. This is an example of a composition of patterns with a logical "and", but simply reading the query out loud should make it clear what is going on!

The result of the above query now includes only one user that matches the new pattern:

Output example
{
    "u": {
        "email": [ { "value": "bob@typedb.com", "type": { "label": "email", "root": "attribute", "value_type": "string" } } ],
        "name": [ { "value": "Bob", "type": { "label": "name", "root": "attribute", "value_type": "string" } } ],
        "type": { "label": "user", "root": "entity" }
    }
}

Relation patterns

Relations in TypeDB make expressing data dependencies simple and efficient, without the need for joins, tables, foreign keys, or any other “hacks” to model complex data. A relation type collects “linkages” between objects of other types.

The specification of these linkages is given by the schema: a relation type has roles which can be played by other types. For example, in the sample schema for this crash course, we specified: .Inserting relation permission

permission sub relation,
    relates object,
    relates subject,
    relates action;
user plays permission:subject;
file plays permission:object;
action plays permission:action;

Based on this, let’s insert a relation of the permission type, between a user with username bob_93 and file with the path README.md:

Inserting relation permission
match
$u isa user, has username "bob_93";
$f isa file, has name path "README.md";
insert
(subject: $u, obejct: $f) isa permission;

In the above example, we start by matching the user and file in the database first, and then we insert the permission relation with the user playing the subject role and the file playing the object role.

Note, that the permission relation type is defined in the sample schema of the Quickstart guide as having _three roles. An incomplete relation that has no players for some of its roles is considered an incomplete data state, but is permitted in the database.

N-ary relations

Relations in TypeDB are simple but flexible. For example, you can create an N-ary relation by defining N roles in the relation’s type definition. N-ary relation generalize basic binary relations.

  • A binary relation has two roles, for example, group-membership with roles group and member (or a single role played by muliple objects). Binary relations are a frequently occurring structure in data, and have been popularized in the context of graph databases.

  • A ternary relation is a relation with three roles: for example, a permission relation with roles subject, object, and action played by data instances of types user, file, and action respectively.

  • Similarly, an N-ary relation is a relation with $n$ roles for any number N > 0!

Every role has a set of types capable of playing it, defined in a schema. For example, a user type could be defined to be able to play the role of a member, and user-group may play the role of a group role player. Then a user can’t play the group role, unless you specify such an ability in the schema with the plays statement.

Let’s say how we can write a query with a N-ary relation for N > 2. For example, what files does the user with name "Bob" has full access to?

match
$u isa user;
$f isa file;
$act isa action;
($u,$f,$act) isa permission;
fetch
$u as "User": name;
$f as "File": path;
$act as "Action": name;

Attribute patterns

Attributes in TypeDB are unique in their ability to store a value. Every attribute type is defined in the schema of a database to be of a specific value type (e.g. integers or strings). The schema also specifies which other types own a given attribute type.

Creating an attribute is just instantiating an attribute type with a value of the appropriate value type. You can create an attribute explicitly by using an isa statement with value assignment:

Explicit attribute insertion
insert
$name "James" isa name;

We refer to such as attribute as independent: a priori, it is not owned by any object!

Alternatively, you can assign ownership of an attribute, and if the attribute doesn’t exist in the database, it will be created implicitly:

Ownership assignment with optional implicit attribute insertion
insert
$p isa person, has name "Sam";

Owning multiple attributes

By default, multiple attributes of the same type can be owned by the same data instance. That makes it possible for a data instances to have multiple values of the same property owned simultaneously.

For example, let’s insert a new person that owns two names: Bob and Another Bob:

insert
$p isa person, has name "Bob", has name "Another Bob";

You can limit behavior this by using the key statement, which adds a constraint on the number of owned attributes to be equal to exactly one.

Globally unique attributes

In TypeDB’s data model, any attribute can be uniquely addressed by its type and value. Thus, there can be no other attribute of the same type with the same value. Storage space and memory consumption are optimized in this way as data is naturally deduplicated.

match
$u isa person, has name $name;
get
$u, $name;

For example, if multiple people have the same name, they have ownership of the same attribute:

Multiple Bobs

It is useful to think of attribute values as immutable: i.e. you can’t change the value of an attribute (since the attribute is its value and type). What you can do is to delete ownership of one attribute and insert ownership of another, for example, with an Update query.

Polymorphic queries

Full power of patterns and type inference comes to fruition by using polymorphism in queries. This allows us to relax and combine type constraints in the matching pattern of a query in novel ways. There are three types of polymorphism in TypeQL queries:

Inheritance polymorphism

Inheritance polymorphism is enabled by type inference. Using type hierarchy, you can query for a supertype to include all of its subtypes in the results. For a simple example, let’s match all entities in a database and fetch all their attributes in a single query:

match 
$x isa entity;
fetch 
$x as "Entity": attribute as "Attributes";

The output of such a query includes all entities in a database, regardless of their exact type because they all are subtypes of the entity root type. In the same way, we can match data instances of the person type and include data instances of the user type due to type inference. To avoid type inference in matching, use the isa! keyword instead. For more information on the isa and isa! keywords, see the isa / isa! page.

Interface polymorphism

Interface polymorphism lets us query for any types that implement a given interface. By implementing a given interface, we mean owning an attribute type or playing a role in a relation type (see the root types table). For example, let’s match all data instances that have a name, without specifying their types:

match 
$x has name $name;
fetch 
$x as "Something with a name": attribute as "Attributes";

The above example can match any type, as long as it’s data instance owns a name attribute.

Parametric polymorphism

Parametric polymorphism lets us look for a value of an attribute regardless of its type. Querying without constraining a type can return any type that matches a pattern. For example, let’s match any data instance that has any attribute with a value bigger than 100:

match
$data has $attr;
$attr > 100;
fetch
$data: attribute;

The result of the above query sent to our sample database from the Quickstart guide includes a file entity owning a size-kb attribute, that is bigger than 100:

{
    "data": {
        "attribute": [
            { "value": 3458761, "type": { "label": "size-kb", "root": "attribute", "value_type": "long" } },
            { "value": "docs/quickstart-guide.adoc", "type": { "label": "path", "root": "attribute", "value_type": "string" } }
        ],
        "type": { "label": "file", "root": "entity" }
    }
}

Rule-based inference

TypeDB can perform rule-based inference in read queries (Fetch and Get). Rules are defined in the schema of a database. When you retrieve data in a read transaction with the inference option enabled, TypeDB can add inferred "virtual" data to the results. This inferred data is never persisted in the database and only exists for this particular transaction. When you close the transaction and open a new one, the data needs to be inferred again to ensure that it is always up-to-date.

For example, let’s add a rule to infer permission with action read if a permission with actions write or full already exist in the database for the file and person:

define

rule view-permission: when {
    $p isa person;
    $f isa file;
    $act isa action, has name $an;
    {$an == "write";} or {$an == "full";};
    (object: $f, subject: $p, action: $act) isa permission;
    $read-act isa action, has name "read";
} then {
    (object: $f, subject: $p, action: $read-act) isa permission;
};

To use the inference, enable the infer transaction option and use the following fetch query:

match
(subject: $p, object: $f, action: $act) isa permission;
$act isa action, has name "read";
$p isa person, has name $name;
$f isa file, has path $path;
fetch 
$name as "Name";
$path as "Filepath";

The above query matches all files and people that participate in a permission relation with the action role played by an instance of the action type with the name read.

Since we are matching only files with a read permission and there is no such permission inserted in our database directly, all results returned must be obtained though inference with the rule we inserted earlier.

Combining rules

A single rule can infer a single relation or a single attribute ownership. But a rule can be applied many times, as long as it’s producing new results. You can use multiple rules in a schema, and combine them with advanced technics, like chaining or branching rules!

Provide Feedback