TypeDB 3.0 is live! Get started for free.

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 (studio dbs) 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 (database none), 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 (studio check) 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 of user's, or the start_date's of friendship'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 (eye). These scripts can then be pasted directly into the server-level interface of Console, and run end-to-end simply by pressing Enter.

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 (database none), 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 (studio check) 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 a friend 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 (database none), 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 (studio check) 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 (database none), 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 (studio check) 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 (database none), 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 (studio check) 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 (database none), 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 (studio check) 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 like update and delete) itself returns data and this output data can be fed into yet another stage. For example, we could add a further match 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 (database none), 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 (studio check) to persist any changes made.

All read and write stages (match, select, insert, delete, update, …​) are can be pipelined; with the only exception being the fetch stage as this returns JSON documents (and not “concept rows”, TypeDB’s internal answer format). In contrast, schema queries (like define, undefine, redefine) do not return answers, and thus cannot be used in pipelines.

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 (database none), 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 (studio check) 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:

  1. 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!

  2. Second, the reduce stage counts these answer combinations, outputs a single number, and assigns it to the variable $alex-friend-count.

The pattern $var isa <type> means the variable $var represents a data object (instance) of type <type>.

In addition, TypeQL has special statements to capture references between instances (as relations must references role players, and attributes must reference owners), like $relation isa <type> (<role>: $player); or $owner has <type> $attribute.

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 (database none), 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 (studio check) 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-users 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 (database none), 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 (studio check) 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 …​ (:

An end-to-end learning experience for TypeDB and TypeQL, showing how to take advantage of TypeDB’s unique features.

Practice-oriented guides on using TypeDB, including the TypeDB Studio and TypeDB Console manuals.

Installation guides, tutorials, and API references for the official TypeDB drivers in all supported languages.

Complete language reference for TypeQL, covering all query types, pattern elements, and keywords.