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.
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.
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.
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.
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.
Read transaction | Write transaction | |
---|---|---|
Schema session |
||
Data session |
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
.
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.
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:
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:
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:
{
"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
:
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:
{
"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
:
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 rolesgroup
andmember
(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 rolessubject
,object
, andaction
played by data instances of typesuser
,file
, andaction
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:
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:
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:
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.