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.
Database management 101
Before we get started, ensure that TypeDB is running as detailed in the Quickstart, and have the server’s address and user credentials at hand. Now, let’s connect to TypeDB and create your first database!
The workflow here will depend on your client, but is straight-forward in either case. If you are new to TypeDB, we recommend trying TypeDB via TypeDB Console. But you can also access TypeDB via various language drivers and TypeDB’s graphical user interface, TypeDB Studio, in this course.
-
Console
-
Python
-
Rust
-
Studio
Start TypeDB Console and connect to TypeDB with the following terminal command:
$ typedb console --core=<address> --username=<username> --password
You will be prompted for a password, after which the server-level interface of Console will be opened. To create a new database called my_test_db
type the command:
database create my_test_db
You can similarly delete a database with the command database delete my_test_db
. To see a list of all available databases use database list
.
To connect to TypeDB, use the core_driver
functionality from the TypeDB
module. This will return Driver
object that can be used to manage databases as shown in the script below.
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "test"
#}}
# Connect to TypeDB server
driver = TypeDB.core_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
# Create a database
driver.databases.create(database)
# Delete the database just created
driver.databases.get(database).delete()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
//}}
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
// Create a database
driver.driver.databases().create(db_name).await?;
// Delete the database just created
driver.databases().get(db_name).await?.delete().await?;
//{{
Ok(())
}
//}}
After opening a project, use the database management button () to create and manage databases.
See the Studio Manual for more.
Working with transactions
In order to run our queries in TypeDB, you need to open a transaction for an existing database. Transactions are the "units of work" that we apply to our database: we can run multiple queries in single a transaction, and may commit the transaction to persist the changes done by its queries. Transactions are always required in TypeDB - you can’t run a query without one, even if your queries do not modify the database.
There are three types of transactions:
-
Schema transactions allow you to run any type of queries, including queries that modify the database schema.
-
Write transactions allow you to send query pipelines that may write data.
-
Read transactions allow you to send query pipelines that only read data.
How to open a transaction and run a query in it will depend on your client, but is very straight-forward in either case. Let’s open a simple schema
transaction and then close
it again, without actually running any queries.
-
Console
-
Python
-
Rust
-
Studio
From Console’s server-level interface, you can open a schema
transaction with the command:
transaction schema my_test_db
This will open a new transaction-level interface. In it, you will be able to run queries. You can similarly open write
or read
transactions. To close a transaction use the transaction-level command:
close
Alternatively, you can use the command commit
to persist any changes you have made to the database during the transaction.
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
# Some transaction type
transaction_type = TransactionType.SCHEMA
# Sample query
query = "define entity user;"
#}}
# Open a transaction
with driver.transaction(database, transaction_type) as tx:
# Send a query
response = tx.query(query).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
// Sample transaction type
let transaction_type = TransactionType::Schema;
// Sample query
let query = "define entity user;";
//}}
// Open a transaction
let tx = driver.transaction(db_name, transaction_type).await?;
// Send a query
let response = tx.query(query).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
TypeDB’s data model at a glance
Before we define a database schema for our newly created database, let’s have a quick look at TypeDB’s data model. 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)
Stored data objects are called instances of types, and instances may reference other instances. This simple mechanism of “referencing” underpins the distinction of entity, relation, and attribute types above.
For example, to create a relation instance we must reference multiple other entities or relations, which we call the relation’s role players. Similarly, to create attribute instance we will reference a single entity or relation instances, which we call the attribute’s owner. Makes sense, right?
TypeDB’s data model is conceptually simple, but at the same time extremely systematic and flexible. This is what enables TypeDB’s type system to unify concepts from relational, document, and graph DBMSs, and blend them with a modern typed programming paradigm. Now, let’s write our first queries!
A quick tip if you are using Console for this course. Queries on this page can be turned into runnable Console scripts by clicking the “hidden lines” button ( |
Defining your type schema
Let’s define a simple database schema: we want to store user
's and their username
's in our database and, in addition, let’s also record friendship
's between users. Types in our database schema are defined with define
queries as follows.
-
Console
-
Python
-
Rust
-
Studio
From the server-level interface open a schema transaction with the command transaction schema my_test_db
as explained above. Then paste the following query into the transaction-level console interface:
#!test[schema, db=my_test_db]
define
entity user, owns username;
attribute username, value string;
Press Enter twice to send the query. Similarly, send the following second query:
#!test[schema, db=my_test_db]
define
relation friendship, relates friend @card(2);
user plays friendship:friend;
Finally, commit the transaction by typing commit
and press Enter. You have just defined the first few types in your database schema, congratulations!
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Define entity and attribute types
schema_1 = f"""
define
entity user, owns username;
attribute username, value string;
"""
# Define a relation type
schema_2 = f"""
define
relation friendship, relates friend @card(2);
user plays friendship:friend;
"""
# Open a transaction
with driver.transaction(database, TransactionType.SCHEMA) as tx:
# Send both queries
tx.query(schema_1).resolve()
tx.query(schema_2).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Define entity and attribute types
let schema_1 = r#"
define
entity user, owns username;
attribute username, value string;
"#;
// Define entity and attribute types
let schema_2 = r#"
define
relation friendship, relates friend @card(2);
user plays friendship:friend;
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Schema).await?;
// Send both queries
let _ = tx.query(schema_1).await?;
let _ = tx.query(schema_2).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
Let us dissect the second query in a bit more detail.
-
In the first line of the query we introduce the relation type
friendship
with afriend
role (in programmer lingo: think of a role as a trait that other types can implement). In the same line, we also declare that each friendship requires exactly two friends when created (card
is short for “cardinality”, and is a first example of an annotation. Annotations make schema statements much more specific - but more on that in a moment). -
In the second line, we then define that users can play the role of friends in friendships (in programmer lingo: the type
user
implements the “friend-role” trait).
It’s worth pointing out that roles are only one of two kinds of traits that types may implement, the other being ownership: for example, in our first query above we defined that the user
type “implements the username owner trait”. These traits make our life very easy as we’ll see, especially when dealing with type hierarchies!
Let’s write a few more definition queries to get a feel of database schemas in TypeDB.
-
Console
-
Python
-
Rust
-
Studio
First, note that relation types, like entity types, are types of “first-class objects” in our type system. In particular, they too can own attributes (and may play roles in other relation types). As an example, let’s run the following query:
#!test[schema, db=my_test_db]
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
(i.e., the type friendship
implements the “start-date owner” trait). The definition is followed by a @card
annotation, which specifies that friendships own between zero or one start dates. In fact, this is the default cardinality, so you don’t actually need @card
here at all.
The next query 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).
#!test[schema, db=my_test_db]
define
attribute status, value string;
user owns status @card(0..);
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. Run the next query to define an organization
type which shares many of the traits of user
:
#!test[schema, db=my_test_db]
define
entity organization,
owns username,
owns status,
plays friendship:friend;
As before, don’t forget to commit the transaction by typing commit
and pressing Enter.
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Define a start-date attribute for the friendship relation type
schema_3 = f"""
define
entity user, owns username;
attribute username, value string;
"""
# Define a multi-valued status attribute for users
schema_4 = f"""
define
relation friendship, relates friend @card(2);
user plays friendship:friend;
"""
# Define an organization entity type with similar traits to the user type
schema_5= f"""
define
relation friendship, relates friend @card(2);
user plays friendship:friend;
"""
# Open a transaction
with driver.transaction(database, TransactionType.SCHEMA) as tx:
# Send queries
tx.query(schema_3).resolve()
tx.query(schema_4).resolve()
tx.query(schema_5).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Define a start-date attribute for the friendship relation type
schema_3 = r#"
include::./crash-course.adoc[tag=define1]
"#;
// Define a multi-valued status attribute for users
schema_4 = r#"
include::./crash-course.adoc[tag=define2]
"#;
// Define an organization entity type with similar traits to the user type
schema_5= r#"
include::./crash-course.adoc[tag=define2]
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Schema).await?;
// Send queries
let _ = tx.query(schema_3).await?;
let _ = tx.query(schema_4).await?;
let _ = tx.query(schema_5).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
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. The next example illustrates how this works.
-
Console
-
Python
-
Rust
-
Studio
Let’s define two types that specialize our earlier organization
type: namely, we’ll define company
and university
as special cases, i.e., subtypes of organization
. The keyword to define subtypings is sub
. Run the following query:
#!test[schema, db=my_test_db]
define
entity company sub organization;
entity university sub organization;
Note that we haven’t defined any traits for companies and universities; indeed, all traits of the supertype organization
are automatically inherited! That doesn’t stop from defining new traits for our subtypes, of course:
#!test[schema, db=my_test_db]
define
attribute student-count, value integer;
relation enrolment, relates university, relates student;
university owns student-count, plays enrolment:university;
user plays enrolment:student;
Run the above query, and don’t forget to commit the transaction by typing commit
and pressing Enter.
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Define subtypes of organizations
schema_6 = f"""
define
entity company sub organization;
entity university sub organization;
"""
# Define traits specific to universities
schema_7 = f"""
define
attribute student-count, value integer;
relation enrolment, relates university, relates student;
university owns student-count, plays enrolment:university;
user plays enrolment:student;
"""
# Open a transaction
with driver.transaction(database, TransactionType.SCHEMA) as tx:
# Send queries
tx.query(schema_6).resolve()
tx.query(schema_7).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Define subtypes of organizations
schema_6 = r#"
include::./crash-course.adoc[tag=define6]
"#;
// Define traits specific to universities
schema_7 = r#"
include::./crash-course.adoc[tag=define7]
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Schema).await?;
// Send queries
let _ = tx.query(schema_6).await?;
let _ = tx.query(schema_7).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
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 roles and ownerships 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 more details.
CRUD operations
Having defined the types in our schema, we are ready to write data to our database. Let’s see a few example of how we can create data. We will also learn how TypeDB’s type system holds us accountable when adding and modifying data, requiring us to conform to the database schema that we defined in the previous section.
-
Console
-
Python
-
Rust
-
Studio
To begin, let us try insert a user with the following insert
query:
#!test[write, db=my_test_db, fail_at=commit]
insert $x isa user;
Try to commit
this query - the commit *will fail*! Indeed, we are trying to insert a user
without a username here, but we declared usernames to be a key attribute earlier when we defined user owns username @key
. (In fact, even if the insert
query above worked, it would be pretty useless: we are trying to insert an entity with type user
, but we give no information about that entity and thus no way to refer to the entity. So our schema keeps us accountable in this case.)
The following is more meaningful:
#!test[write, db=my_test_db]
insert $x isa user, has username "user_0";
We can insert multiple values in a single insert
query like so:
#!test[write, db=my_test_db]
insert
$x isa user, has username "user_1";
$y isa user;
$y has username "user_2"; # can split `isa` and `has` across different statements
This query inserts two users at the same time. Time to commit your queries again to persist your changes: type commit
and press Enter.
Finally, since we set usernames to be a @key
attribute of our users, we would - rightfully - expect the following query to fail:
#!test[write, db=my_test_db, fail_at=runtime]
insert
$x isa user, has username "user_3";
$y isa user, has username "user_3";
And it does fail: this time, it already fails at runtime, i.e., even before committing the transaction. Indeed, because the key "user_3"
would be claimed by two distinct user
instances no matter what, we can immediately invalidate the transaction.
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Try to insert a user without username (will fail)
write_1 = f"""
insert $x isa user;
"""
# Insert a user with username
write_2 = f"""
insert $x isa user, has username "user_0";
"""
# Insert multiple users in the same query
write_3= f"""
insert
$x isa user, has username "user_1";
$y isa user;
$y has username "user_2"; # can split `isa` and `has` across different statements
"""
# Try to insert multiple users with the same username
write_4= f"""
insert
$x isa user, has username "user_1";
$y isa user;
$y has username "user_2"; # can split `isa` and `has` across different statements
"""
# Open a transaction
with driver.transaction(database, TransactionType.WRITE) as tx:
# Send queries
try:
tx.query(write_1).resolve()
except Exception as e:
print(f"Got error {e}")
tx.query(write_2).resolve()
tx.query(write_3).resolve()
try:
tx.query(write_4).resolve()
except Exception as e:
print(f"Got error {e}")
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Try to insert a user without username (will fail)
write_1 = r#"
insert $x isa user;
"#;
// Insert a user with username
write_2 = r#"
insert $x isa user, has username "user_0";
"#;
// Insert multiple users in the same query
write_3= r#"
insert
$x isa user, has username "user_1";
$y isa user;
$y has username "user_2"; # can split `isa` and `has` across different statements
"#;
// Try to insert multiple users with the same username
write_4= r#"
insert
$x isa user, has username "user_1";
$y isa user;
$y has username "user_2"; # can split `isa` and `has` across different statements
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Write).await?;
// Send queries
let result_1 = tx.query(write_1).await?;
println!("got {result_1}");
let _ = tx.query(write_2).await?;
let _ = tx.query(write_3).await?;
let result_4 = tx.query(write_4).await?;
println!("got {result_4}");
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
This illustrates how we can insert simple entities with attributes, and how TypeDB keeps us accountable to follow the definitions in the database schema.
So far so good. Next question: what about inserting relations? Well, let’s have a go!
-
Console
-
Python
-
Rust
-
Studio
In order to insert a friendship, we need to refer to users that will play the role of friends in the friendship (recall: each friendship takes exactly two friends as specified in the schema; feel free to experiment though, and see what error messages TypeDB will give you when trying to insert triple friendships). That’s where variables come into play! Let’s run the following query:
#!test[write, db=my_test_db]
insert
$x isa user, has username "grothendieck_25";
$y isa user, has username "hartshorne";
friendship (friend: $x, friend: $y);
This query inserts two new users with a friendship
relation between them. The last line is a shorthand, and could itself be written as $f isa friendship (friend: $x, friend: $y);
if you’d want to further use the variable $f
; but otherwise it’s convenient to simply omit it.
But what if we want to create a friendship
relation between two existing users already in the database? That requires a two-stage query pipeline - (1) retrieve the user with a match
stage; (2) insert
the new friendship
. We will discuss pipelines in more detail below; for now let’s just run one:
#!test[write, db=my_test_db]
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);
This query first matches three existing users, and inserts three new friendships between them.
Don’t forget to commit your changes whenever you want to persist changes. Feel free to insert more data, e.g., create a university and enrol some of your users in it using our enrolment
relations!
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Insert two new users and a friendship between them
write_5 = f"""
insert
$x isa user, has username "grothendieck_25";
$y isa user, has username "hartshorne";
friendship (friend: $x, friend: $y);
"""
# Insert three friendships between existing users
write_6 = f"""
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);
"""
# Open a transaction
with driver.transaction(database, TransactionType.WRITE) as tx:
# Send queries
tx.query(write_5).resolve()
tx.query(write_6).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Insert two new users and a friendship between them
write_5 = r#"
insert
$x isa user, has username "grothendieck_25";
$y isa user, has username "hartshorne";
friendship (friend: $x, friend: $y);
"#;
// Insert three friendships between existing users
write_6 = r#"
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);
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Write).await?;
// Send queries
let _ = tx.query(write_5).await?;
let _ = tx.query(write_6).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
TL;DR Inserting data is easy!
And deleting and updating data works in a similar vein, with one additional detail: deleting and updating requires us to first match
the data that we want to delete or update, whereas for inserts we saw that this was optional. This brings us to the topic of data pipelines.
Data pipelines in 100 seconds
Just like in functional programming, where you can “map” and “operate” on an iterator of data and chain these operations step-by-step, pipelines in TypeDB allow you to step-wise (or, rather, “stage-wise”) compose database operations. For example, you could:
-
First read some data from the database using a
match
stage; -
Then feed the data you are reading into an
insert
operation that creates additional data; -
Now, the insert operation (similar to other
write
operations likeupdate
anddelete
) itself returns data and this output data can be fed into yet another stage. For example, we could add a furthermatch
stage that retrieves further connected data and that we may want to operate on in a “next step”. This is illustrated in the example below.
We can continue chaining pipeline stages in this way, with the output data of one stage becoming the input data of the next stage. But let’s see some code first.
-
Console
-
Python
-
Rust
-
Studio
The following pipeline adds "VIP" status for a specific user, then traverses all friendships of that user, and marks friends as VIPs themselves if they now have more than 3 VIP friends.
#!test[write, db=my_test_db]
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 > 3;
insert $friend has status "VIP";
Notice how the variable $user
is re-used throughout the first three stages of the pipeline. Also note how match
stages are used both to retrieve new data and to filter data; both of these operations neatly fall into TypeQL’s declarative pattern language.
Running the above pipeline will return 0 answers if no friends of "user_0"
have more than 3 “VIP” status friends themselves. Nonetheless, the pipeline still did some work. Let’s verify this:
#!test[read, db=my_test_db, count=1]
match
$user isa user, has status "VIP", has username $username;
select $username;
The query first matches users with “VIP” status and their usernames, and then selects only the username. Here, select
is “stream manipulation” stage (meaning it manipulates the data stream in a pipeline without calling back to the databases). In the case of select
, this manipulation simply removes all non-selected variables from our results. (Try deleting the last line and comparing answers produced by the database.) The reduce
stage in the previous example works similarly!
As always, don’t forget to commit
the changes that you want to persist.
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# A complex data pipeline
write_7 = f"""
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 > 3;
insert $friend has status "VIP";
"""
# Verify the pipeline did some work
write_8 = f"""
match
$user isa user, has status "VIP", has username $username;
select $username;
"""
# Open a transaction
with driver.transaction(database, TransactionType.WRITE) as tx:
# Send queries
tx.query(write_7).resolve()
tx.query(write_8).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// A complex data pipeline
write_7 = r#"
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 > 3;
insert $friend has status "VIP";
"#;
// Verify the pipeline did some work
write_8 = r#"
match
$user isa user, has status "VIP", has username $username;
select $username;
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Write).await?;
// Send queries
let _ = tx.query(write_7).await?;
let _ = tx.query(write_8).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
All read and write stages ( |
For a full rundown of pipeline stages, see the CRUD manual!
“Query programming” with TypeDB
We’ve seen how to define detailed database schemas, and how to insert connected data into the database. Now let’s turn to the simplest, but most ubiquitous database operation: reading data!
Reading data in an intuitive and composable programmatic way is TypeDB’s superpower, and achieved through its query language TypeQL which combines declarative and functional programming concepts with the simplicity of natural language. Let’s start with a basic example:
-
Console
-
Python
-
Rust
-
Studio
#!test[read, db=my_test_db]
match
$alex isa user, has username "grothendieck_25";
$friendship isa friendship (friend: $alex, friend: $friend-of-alex);
reduce $alex-friend-count = count;
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Find a specific user, their friends, and count their friends
read_1 = f"""
match
$alex isa user, has username "grothendieck_25";
$friendship isa friendship (friend: $alex, friend: $friend-of-alex);
reduce $alex-friend-count = count;
"""
# Open a transaction
with driver.transaction(database, TransactionType.WRITE) as tx:
# Send queries
tx.query(read_1).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Find a specific user, their friends, and count their friends
read_1 = r#"
match
$alex isa user, has username "grothendieck_25";
$friendship isa friendship (friend: $alex, friend: $friend-of-alex);
reduce $alex-friend-count = count;
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Write).await?;
// Send queries
let _ = tx.query(read_1).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
The query in the example 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 In addition, TypeQL has special statements to capture references between instances (as relations must references role players, and attributes must reference owners), like |
The above query is mildly interesting. But let’s look at a slightly more complex version of the same query.
-
Console
-
Python
-
Rust
-
Studio
Run the following query in Console and see what answers you get.
#!test[read, db=my_test_db, count=3]
# Find users with more friends that Alex
match
$alex isa user, has username "grothendieck_25";
friendship (friend: $alex, friend: $friend-of-alex);
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;
Can you see just from reading the query what’s going on here?
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Find users with more friends that Alex
read_2 = f"""
match
$alex isa user, has username "grothendieck_25";
friendship (friend: $alex, friend: $friend-of-alex);
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;
"""
# Open a transaction
with driver.transaction(database, TransactionType.WRITE) as tx:
# Send queries
tx.query(read_2).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Find users with more friends that Alex
read_2 = r#"
match
$alex isa user, has username "grothendieck_25";
friendship (friend: $alex, friend: $friend-of-alex);
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;
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Write).await?;
// Send queries
let _ = tx.query(read_2).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
The goal of this second 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.)
-
Console
-
Python
-
Rust
-
Studio
#!test[read, db=my_test_db, count=3]
# Find users with more friends that Alex
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;
#{{
from typedb.driver import TypeDB, TransactionType, Credentials, DriverOptions
user = "admin"
pw = "password"
address = "127.0.0.1:1729"
database = "my_test_db"
driver = TypeDB.cloud_driver(address=address, credentials=Credentials(user,pw), driver_options=DriverOptions())
#}}
# Find users with more friends that Alex
read_3 = f"""
# Find users with more friends that Alex
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;
"""
# Open a transaction
with driver.transaction(database, TransactionType.WRITE) as tx:
# Send queries
tx.query(read_3).resolve()
# Commit your changes
tx.commit()
//{{
use typedb_driver::{Credentials, DriverOptions, TransactionType, TypeDBDriver};
use std::error::Error;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let address = "127.0.0.1:1729";
let user = "admin";
let pw = "password";
let db_name = "test";
// Connect to TypeDB server
let driver = TypeDBDriver::new_core(address, Credentials::new(user, pw), DriverOptions::new(false, None)?).await?;
//}}
// Find users with more friends that Alex
read_3 = r#"
# Find users with more friends that Alex
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;
"#;
// Open a transaction
let tx = driver.transaction(db_name, TransactionType::Write).await?;
// Send queries
let _ = tx.query(read_2).await?;
// Commit your changes
tx.commit().await?;
//{{
Ok(())
}
//}}
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.
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. In fact, function can contain entire read pipelines themselves! This is a key component of TypeDB’s query programming paradigm, allowing you to seamlessly fuse "query modules" together.
What next?
There is much more to explore in the world of TypeDB, including building custom query logic using disjunctions and negations, the deep exploration of data connection using functional recursion, result formatting features, native language integration features, and more. This is the beginning of your journey, and we’d like to be here for you should you have any question - so do not hesitate to reach out!
And here are some suggestion what to look at next … (: