Crash Course
TypeDB brings together powerful programming concepts with the features and performance of modern database systems. We believe the result is a novel database experience - and after taking this 15-minute crash course, we hope you’ll agree.
Storing data in types
In TypeDB, data is stored in types, which can be either
-
entity types (e.g., a type of all the
user
s in your app) -
relation types (e.g., all the
friendship
s of users) -
attribute types (e.g., the
username
s ofuser
s, or thestart_date
s offriendship
s)
Data objects (instances) may reference other instances. A relation references other entities or relations, which we call its role players. An attribute is owned by a single entity or relation, which we call its owner. Makes sense, right?
TypeDB’s type system unifies concepts from relational, document, and graph DBMSs, blending them with a modern programming paradigm. Let’s see how this plays out for writing queries.
“Query programming” with TypeDB
The query language of TypeDB is TypeQL. It combines declarative and functional programming concepts with the simplicity of natural language. Here’s a simple TypeQL query to get us started:
match
$alex isa user, has username "grothendieck_25";
$friendship isa friendship (friend: $alex, friend: $friend-of-alex);
reduce $alex-friend-count = count;
This query returns the count of how many friends the user $alex
has (identified by their username "grothendieck_25"
). Note that the query works in two stages:
-
First, the
match
stage outputs all answers from the database that satisfy the statements in the body of the stage. This means we output all possible data combinations from the database for the three used variables! -
Second, the
reduce
stage counts these answer combinations, outputs a single number, and assigns it to the variable$alex-friend-count
.
The pattern TypeQL has special statements to capture references between instances, like |
Here’s an example of a slightly more complex query.
match
$alex isa user, has username "grothendieck_25";
friendship (friend: $alex, friend: $friend-of-alex); # can omit variable
reduce $alex-friend-count = count;
match
$other-user isa user, has username $other-name;
friendship (friend: $other-user, friend: $friend-of-other-user);
reduce $other-friend-count = count
groupby $other-name, $alex-friend-count;
match
$alex-friend-count < $other-friend-count;
select $other-name;
The goal of this query is to find users with more friends than Alex. It has 6 stages:
-
Stage 1 and 2: Count Alex' friends - and assign the output to
$alex-friend-count
. -
Stage 3 and 4: Count all other users' friends. Match all
$other-user
s with their respective usernames,$other-name
, and their friends. Group the answers by unique combinations of$other-name
and$alex-friend-count
. Finally,count
them. -
Stage 5 and 6: List users with more friends than Alex. Introduce a condition -
$alex-friend-count < $other-friend-count
- and return only the results that satisfy it.select
the username:$other-name
.
Isn’t that neat? Combining multiple query stages into a single query pipeline is powerful. However, the resulting query now has quite a bit of repetitive code. Let’s optimize it using a helper function.
(Don’t worry. This is not an exercise for the reader. We’ve optimized it for you.)
with fun friend_count($user: user) -> integer:
match friendship (friend: $user, friend: $friend-of-user);
return count;
match
$alex isa user, has username "grothendieck_25";
$other-user isa user, has username $other-name;
friend_count($alex) < friend_count($other-user);
select $other-name;
We’ve just defined a helper function using the fun
keyword that abstracts a key part of our query logic. Functions use the same syntax as queries, so they’re easy to write.
Defining your type schema
The types user
and username
in the above queries don’t come out of nowhere: in order to query data in types, you first need to define them. Types are defined with define
queries like so:
define
entity user, owns username, owns status;
attribute username, value string;
attribute status, value string;
Similarly, if we were to add a friendship
type to our schema we would define:
define
relation friendship, relates friend @card(2);
user plays friendship:friend;
-
Line 1: Introduce the relation type
friendship
with afriend
role (programmer-lingo: think “trait” that can then be “implemented” by other types). Declare that each friendship requires exactly two friends (card
is short for “cardinality”, and is a first example of a annotation: annotations make schema statements more specific - more on that later). -
Line 2: Users can play the role of friends in friendships (more programmer-lingo: the type
user
implements the “friend-role” trait).
Relation types, like entity types, are types of “first-class objects” in our type system. In particular, they too can have attributes (or may play roles in other relation types).
define
attribute start-date, value datetime;
friendship owns start-date @card(0..1);
This defines a start-date
attribute and then declares each friendship may own a start_date
(yet more programmer-lingo: the type friendship
implements the “start-date-owner” trait). Moreover, the @card
annotation specifies that friendships own between zero or one start dates. This is the default cardinality, so you don’t actually need @card
here at all.
The next example illustrates usage of “unbounded” cardinality together with another useful annotation, @key
(which, in this case, specifies that the username of a user should uniquely identify a user, i.e., it is a key attribute).
define
user owns status @card(0..); # as many status attributes as you wish
user owns username @key;
Importantly, different types may own the same attributes; and, similarly, different types may play the same role of a relation type. This flexibility of connecting types is a central feature of data modeling with TypeDB.
define
entity organization,
owns username,
owns status,
plays friendship:friend;
Finally, yet another fundamental and powerful feature of TypeDB’s type system is subtyping: analogous to modern programming language features, this gives us the ability to organize our types hierarchically:
define
entity company sub organization; # subtypes will inherit ownership and role
entity university sub organization; # playing traits from their supertype
TypeDB’s type system re-thinks data models from first principles: it modularizes schemas into their "atomic" components. For example, you can add or remove ownership at any point in time, or edit specific annotations. This makes it easy to migrate and combine data, and programmatically re-structure your database if necessary. There is much more to explore, but we refer to the Schema Manual for further details.
CRUD operations
Having defined the types in our schema, we are ready to write data to our database. To begin, let’s insert a user with the following insert
query:
insert $x isa user;
This works, but is pretty useless: it inserts an entity with type user
, but gives no information about that entity (in fact, the query will fail if user owns username @key
).
The following is more meaningful:
insert $x isa user, has username "user_0";
We can insert multiple values in a single insert
query like so:
insert
$x isa user, has username "user_1";
$y isa user;
$y has username "user_2"; # we can separate `isa` and `has`
This inserts two users at the same time. Recall, that we set usernames to be a @key
attribute of our users, so you would - rightfully - expect the following query to fail:
insert
$x isa user, has username "user_3";
$y isa user, has username "user_3";
And it does fail, because the key 'user_3' is now claimed by two distinct user
instances.
So far so good. Next question: how do we insert friendships? We need to refer to users to instantiate our friendship (recall: each friendship takes two friends). That’s where variables come into play.
insert
$x isa user, has username "grothendieck_25";
$y isa user, has username "hartshorne";
friendship (friend: $x, friend: $y); # alternatively: $z isa friendship (friend: $x, friend: $y);
And now we’ve inserted two new users with a friendship
relation between them.
What if we want to create a friendship
relation between two existing users already in the database? That’s a two-stage query pipeline - (1) retrieve them with match
; (2) insert
the new friendship
.
match
$u0 isa user, has username "user_0";
$u1 isa user, has username "user_1";
$u2 isa user, has username "user_2";
insert
friendship (friend: $u0, friend: $u1);
friendship (friend: $u0, friend: $u2);
friendship (friend: $u1, friend: $u2);
Easy!
Deleting and updating data work in a similar vein: all data write operations can be used as pipeline stages, and they all use essentially the same declarative statement-by-statement syntax. For example, the following pipeline adds "VIP" to a user’s status, then traverses all friendships of that user, and marks friends as VIPs themselves if they now have more than five VIP friends.
match
$user isa user, has username "user_0";
insert $user has status "VIP";
match
friendship (friend: $user, friend: $friend);
friendship (friend: $friend, friend: $friend-of-friend);
$friend-of-friend has status "VIP";
reduce $VIP-friend-count = count groupby $friend;
match $VIP-friend-count > 5;
insert $friend has status "VIP";
See the CRUD manual for more.
Database management 101
The time has come to actually run our queries. Recall from the Quickstart, the first two steps of the TypeQB workflow are always as follows:
-
Ensure that your TypeDB cluster is running.
-
From the TypeDB client of your choice, establish a connection to the server.
Next up, let us create a database. The workflow here will depend on your client, but is easy in either case.
-
Console
-
Python
-
Rust
-
Studio
To list existing databases use:
$ database list
To create a new database called my-db
use
$ database create my-db
To delete a database use
$ database delete my-db
After connecting with
driver = TypeDB.core_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
you can create a database with:
driver.databases.create(database)
and delete a database with:
driver.databases.get(database).delete()
After connecting with
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
you can create a database with:
driver.databases().create(db_name).await?;
and delete a database with:
driver.databases().get(db_name).await?.delete().await?;
After opening a project, use the database management button () to create and manage databases.
See the Studio Manual for more.
Running queries with transactions
In order to run our queries, the next step is to open a transaction for an existing database. We can run multiple queries in single a transaction, and then commit the transaction to persist our changes when done. Transactions are required in TypeDB - you can’t run a query without one. There are three types of transactions:
-
Schema transactions allow you to run schema definition queries (like the
define
queries that we’ve seen above). -
Write transactions allow you to send query pipelines that may write data (i.e., they contain “write stages” like the
insert
ordelete
stages that we have seen above) -
Read transactions allow you to send query pipelines that only read data (i.e., they contain only “read stages” like the
match
,reduce
, orselect
stages that we have seen above).
How to open a transaction and run a query will depend on your client.
-
Console
-
Python
-
Rust
-
Studio
Open the transaction with
transaction my-db <TYPE>
where <TYPE>
can be one of schema
, write
, or read
.
Then run your queries in the newly opened prompt. Use
commit
if you want to commit changes.
Open a transaction and send a query with
with driver.transaction(database, TransactionType.<TYPE>) as tx:
response = tx.query(<QUERY>).resolve()
where <TYPE>
can be one of SCHEMA
, WRITE
, or READ
. Use
tx.commit()
if you want to commit your changes.
Open a transaction and send a query with
let tx = driver.transaction(db_name, TransactionType::<TYPE>).await?;
tx.query(<QUERY>).await?;
where <TYPE>
can be one of SCHEMA
, WRITE
, or READ
. Use
tx.commit().await?;
if you want to commit your changes.
Select your database using the database selector (), then select your transaction type in the top menu.
Open your query file. Then click the run button to run your query. Finally, use the commit button () to persist any changes made.
Do you take a hands-on approach to learning? If so, try running queries in this course in the following order:
-
Define a schema as outlined in the schema section. Use
schema
transactions for your queries and don’t forget to commit any changes made. -
Insert data instances into the types of your schema as explained in the CRUD section. Use
write
transactions for your queries and don’t forget to commit any changes you want to persists. -
Read your data using query pipelines like those in the pipeline section.