Officially out now: The TypeDB 3.0 Roadmap >>

C driver tutorial

In this tutorial, we’ll build a sample application with the C driver capable of basic interaction with TypeDB:

  • Connect to a TypeDB server (Core or Cloud),

  • Manage databases, sessions, and transactions,

  • Send different types of queries.

Follow the steps below or see the full source code.

Environment setup

To run this sample application, you’ll need:

  1. TypeDB: either a TypeDB Cloud cluster or a self-hosted deployment. For TypeDB Community Edition and TypeDB Enterprise installation instructions, see the Self-managed deployments page.

  2. Download the TypeDB C driver. For the driver installation instructions, see the C driver page.

Includes

To be able to use the TypeDB C driver API in the Sample application, use the following include statements:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "include/typedb_driver.h"

Default values

We store default values as constants in the source code:

#define SERVER_ADDR "127.0.0.1:1729"
#define DB_NAME "sample_app_db"
#define CLOUD_USERNAME "admin"
#define CLOUD_PASSWORD "password"
#define FAILED() check_error_may_print(__FILE__, __LINE__)

typedef enum { CORE, CLOUD } edition;
edition TYPEDB_EDITION = CORE;

where DB_NAME — the name of the database to use; SERVER_ADDR — address of the TypeDB server to connect to; TYPEDB_EDITION — TypeDB Community Edition or Cloud edition selector; CLOUD_USERNAME/CLOUD_PASSWORD — credentials to connect to TypeDB Cloud or TypeDB Enterprise; FAILED() is used for error handling, see the Error handling section for details.

Program structure

The main workflow of this sample application includes establishing a connection to TypeDB, a new database setup, and querying.

int main() {
    bool result = EXIT_FAILURE;
    Connection* connection = NULL;
    DatabaseManager* databaseManager = NULL;
    connection = connectToTypeDB(TYPEDB_EDITION, SERVER_ADDR);
    if (!connection || FAILED()) {
        handle_error("Failed to connect to TypeDB.");
        goto cleanup;
    }
    databaseManager = database_manager_new(connection);
    if (!databaseManager || FAILED()) {
        handle_error("Failed to get database manager.");
        goto cleanup;
    }
    if (!dbSetup(databaseManager, DB_NAME, false)) {
        handle_error("Failed to set up the database.");
        goto cleanup;
    }
    if (!queries(databaseManager, DB_NAME)) {
        handle_error("Failed to query the database.");
        goto cleanup;
    }
    result = EXIT_SUCCESS;
cleanup:
    database_manager_drop(databaseManager);
    connection_close(connection);
    exit(result);
}

The entire main() function code is executed in the context of the network connection, represented by the connection object that is returned by the function.

TypeDB connection

The connectToTypeDB() function takes edition and addr as mandatory parameters.

Connection* connectToTypeDB(edition typedb_edition, const char* addr) {
    Connection* connection = NULL;
    if (typedb_edition == CORE) {
        connection = connection_open_core(addr);
    } else {
        Credential* credential = credential_new(
            CLOUD_USERNAME,
            CLOUD_PASSWORD,
            "path/to/tls_root_ca",
            true);
        const char* addrs[] = {addr, NULL};
        connection = connection_open_cloud(addrs, credential);
        credential_drop(credential);
    }
    if (!connection) handle_error("Failed to connect to TypeDB server.");
    return connection;
}

The edition is expected to be an Enum for selecting a TypeDB edition. Depending on the TypeDB edition selected, this function initializes either a TypeDB Community Edition, TypeDB Enterprise, or TypeDB Cloud connection.

TypeDB Cloud and TypeDB Enterprise connections require an object of the Credential type that is initialized with a username and password. For our sample application, we have the default credentials for the admin account set in the code of the connectToTypeDB function.

TypeDB Cloud and TypeDB Enterprise require the default password for the default admin account to be changed before any other request can be accepted.

Database setup

To set up a TypeDB database, we need to make sure that it exists and has the correct schema and data. First, we check whether a database with the provided name already exists on the server.

If such a database doesn’t exist, we create a new database, define its schema, and load initial data.

To prevent data loss, avoid deleting an existing database without confirmation from a user.

If a database with the specified name already exists, we check whether we need to replace it. To do so, we check the dbReset parameter, and, if it’s false, ask for an input from a user. If any of the two suggesting replacement of the database is acceptable, we replace the database by deleting the existing database and then creating a new one.

As the final step of the database setup, we test it.

bool dbSetup(DatabaseManager* dbManager, const char* dbName, bool dbReset) {
    printf("Setting up the database: %s\n", dbName);
    bool result = false;
    Options* opts = options_new();

    if (databases_contains(dbManager, dbName)) {
        if (dbReset) {
            if (!replaceDatabase(dbManager, dbName)) {
                printf("Failed to replace the database. Terminating...\n");
                exit(EXIT_FAILURE);
            }
        } else {
            char answer[10];
            printf("Found a pre-existing database. Do you want to replace it? (Y/N) ");
            scanf("%s", answer);
            if (strcmp(answer, "Y") == 0 || strcmp(answer, "y") == 0) {
                if (!replaceDatabase(dbManager, dbName)) {
                    printf("Failed to replace the database. Terminating...\n");
                    exit(EXIT_FAILURE);
                }
            } else {
                printf("Reusing an existing database.\n");
            }
        }
    } else {
        if (!createDatabase(dbManager, dbName)) {
            printf("Failed to create a new database. Terminating...\n");
            exit(EXIT_FAILURE);
        }
    }

    if (databases_contains(dbManager, dbName)) {
        Session* session = session_new(dbManager, dbName, Data, opts);
        if (session == NULL || FAILED()) {
            printf("Failed to open a session. Terminating...\n");
            exit(EXIT_FAILURE);
        }
        result = dbCheck(session);
        session_close(session);
        return result;
    } else {
        printf("Failed to find the database after creation. Terminating...\n");
        exit(EXIT_FAILURE);
    }
    return result;
}

Creating a new database

We create a new database with the specified name (sample_app_db by default) and call functions to define its schema and load initial data.

bool createDatabase(DatabaseManager* dbManager, const char* dbName) {
    Session* schemaSession = NULL;
    Session* dataSession = NULL;
    Options* opts = options_new();
    bool result = false;
    printf("Creating new database: %s\n", dbName);
    databases_create(dbManager, dbName);
    if (FAILED()) {
        handle_error("Database creation failed.");
        goto cleanup;
    }
    schemaSession = session_new(dbManager, dbName, Schema, opts);
    if (schemaSession == NULL || FAILED()) {
        goto cleanup;
    }
    dbSchemaSetup(schemaSession, "iam-schema.tql");
    session_close(schemaSession);
    dataSession = session_new(dbManager, dbName, Data, opts);
    if (dataSession == NULL || FAILED()) {
        goto cleanup;
    }
    dbDatasetSetup(dataSession, "iam-data-single-query.tql");
    session_close(dataSession);
    result = true;
cleanup:
    options_drop(opts);
    return result;
}

Replacing a database

We delete a database with the specified name (sample_app_db by default) and call a function to create a new one instead:

void delete_database_if_exists(DatabaseManager* databaseManager, const char* name) {
    if (NULL != databaseManager && databases_contains(databaseManager, name)) {
        Database* database = databases_get(databaseManager, name);
        database_delete(database);
    }
}

bool replaceDatabase(DatabaseManager* dbManager, const char* dbName) {
    printf("Deleting an existing database...");
    delete_database_if_exists(dbManager, dbName);
    if (FAILED()) {
        printf("Failed to delete the database. Terminating...\n");
        exit(EXIT_FAILURE);
    }
    printf("OK\n");

    if (!createDatabase(dbManager, dbName)) {
        printf("Failed to create a new database. Terminating...\n");
        exit(EXIT_FAILURE);
    }
    return true;
}

Defining a schema

We use a Define query to define a schema for the newly created database:

void dbSchemaSetup(Session* schemaSession, const char* schemaFile) {
    Transaction* tx = NULL;
    Options* opts = options_new();
    char defineQuery[4096] = {0};
    FILE* file = fopen(schemaFile, "r");
    if (FAILED()) goto cleanup;
    if (!file) handle_error("Failed to open schema file.");
    char line[512];
    while (fgets(line, sizeof(line), file)) strcat(defineQuery, line);
    fclose(file);

    tx = transaction_new(schemaSession, Write, opts);
    if (tx == NULL || FAILED()) {
        handle_error("Transaction failed to start.");
        goto cleanup;
    }
    void_promise_resolve(query_define(tx, defineQuery, opts));
    if (FAILED()) {
        handle_error("Query execution failed.");
        goto cleanup;
    }
    void_promise_resolve(transaction_commit(tx));
    if (FAILED()) {
        handle_error("Transaction commit failed.");
        goto cleanup;
    }
    printf("Schema setup complete.\n");
cleanup:
    options_drop(opts);
}

The schema for the sample application is stored in the iam-schema.tql file.

See the full schema
define

credential sub attribute, value string;
full-name sub attribute, value string;
id sub attribute, abstract, value string;
email sub id, value string;
name sub id, value string;
number sub id, value string;
path sub id, value string;
object-type sub attribute, value string;
ownership-type sub attribute, value string;
review-date sub attribute, value datetime;
size-kb sub attribute, value long;
validity sub attribute, value boolean;

access sub relation,
    relates action,
    relates object,
    plays change-request:change,
    plays permission:access;

change-request sub relation,
    relates change,
    relates requestee,
    relates requester;

membership sub relation,
    relates member,
    relates parent;

collection-membership sub membership,
    relates collection as parent;

group-membership sub membership,
    relates group as parent;

set-membership sub membership,
    relates set as parent;

ownership sub relation,
    relates owned,
    relates owner;

group-ownership sub ownership,
    owns ownership-type,
    relates group as owned;

object-ownership sub ownership,
    owns ownership-type,
    relates object as owned;

permission sub relation,
    owns review-date,
    owns validity,
    relates access,
    relates subject;

segregation-policy sub relation,
    owns name,
    relates action,
    plays segregation-violation:policy;

violation sub relation,
    abstract;

segregation-violation sub violation,
    relates object,
    relates policy,
    relates subject;

action sub entity,
    abstract,
    owns name,
    owns object-type,
    plays access:action,
    plays membership:member,
    plays segregation-policy:action;

operation sub action;

operation-set sub action,
    plays set-membership:set;

object sub entity,
    abstract,
    owns object-type,
    plays access:object,
    plays membership:member,
    plays object-ownership:object,
    plays segregation-violation:object;

resource sub object,
    abstract;

file sub resource,
    owns path,
    owns size-kb;

record sub resource,
    owns number;

resource-collection sub object,
    abstract,
    plays collection-membership:collection;

database sub resource-collection,
    owns name;

directory sub resource-collection,
    owns path,
    owns size-kb;

subject sub entity,
    abstract,
    owns credential,
    plays change-request:requestee,
    plays change-request:requester,
    plays membership:member,
    plays ownership:owner,
    plays permission:subject,
    plays segregation-violation:subject;

user sub subject,
    abstract;

person sub user,
    owns email,
    owns full-name;

user-group sub subject,
    abstract,
    plays group-membership:group,
    plays group-ownership:group;

business-unit sub user-group,
    owns name;

user-account sub user-group,
    owns email;

user-role sub user-group,
    owns name;

rule add-view-permission: when {
    $modify isa action, has name "modify_file";
    $view isa action, has name "view_file";
    $ac_modify (object: $obj, action: $modify) isa access;
    $ac_view (object: $obj, action: $view) isa access;
    (subject: $subj, access: $ac_modify) isa permission;
} then {
    (subject: $subj, access: $ac_view) isa permission;
};

We use a session object passed as a parameter to open a transaction. Then we send the contents of the file as a TypeQL Define query and commit the changes made by the transaction.

Loading initial data

With the schema defined, we can load initial data into our database with the Insert query:

void dbDatasetSetup(Session* dataSession, const char* dataFile) {
    bool result = false;
    Transaction* tx = NULL;
    Options* opts = options_new();
    char insertQuery[4096] = {0};
    FILE* file = fopen(dataFile, "r");
    if (!file) handle_error("Failed to open data file.");

    char line[512];
    while (fgets(line, sizeof(line), file)) strcat(insertQuery, line);
    fclose(file);

    tx = transaction_new(dataSession, Write, opts);
    if (tx == NULL || FAILED()) {
        handle_error("Transaction failed to start.");
        goto cleanup;
    }
    ConceptMapIterator* insertResult = query_insert(tx, insertQuery, opts);
    if (FAILED()) {
        handle_error("Query execution failed.");
        goto cleanup;
    }
    void_promise_resolve(transaction_commit(tx));
    if (FAILED()) {
        handle_error("Transaction commit failed.");
        goto cleanup;
    }
    printf("Dataset setup complete.\n");
    result = true;
cleanup:
    concept_map_iterator_drop(insertResult);
    // transaction_close(tx);
    options_drop(opts);
}

We read the iam-data-single-query.tql file, send its contents as a single query, and then commit the changes.

See the full Insert query
insert
$p1 isa person,
    has full-name "Masako Holley",
    has email "masako.holley@typedb.com";
$p2 isa person,
    has full-name "Pearle Goodman",
    has email "pearle.goodman@typedb.com";
$p3 isa person,
    has full-name "Kevin Morrison",
    has email "kevin.morrison@typedb.com";
$f1 isa file,
    has path "iopvu.java",
    has size-kb 55;

$modify isa operation, has name "modify_file";
$view isa operation, has name "view_file";

$a1 (object: $f1, action: $modify) isa access;
$a11 (object: $f1, action: $view) isa access;
$permission1 (subject: $p3, access: $a1) isa permission;
$f2 isa file,
    has path "zlckt.ts",
    has size-kb 143;
$a2 (object: $f2, action: $modify) isa access;
$a22 (object: $f2, action: $view) isa access;
$permission2 (subject: $p3, access: $a2) isa permission;
$f3 isa file,
    has path "psukg.java",
    has size-kb 171;
$a3 (object: $f3, action: $modify) isa access;
$a33 (object: $f3, action: $view) isa access;
$permission3 (subject: $p3, access: $a3) isa permission;
$f4 isa file,
    has path "axidw.java",
    has size-kb 212;
$a4 (object: $f4, action: $modify) isa access;
$a44 (object: $f4, action: $view) isa access;
$permission4 (subject: $p3, access: $a4) isa permission;
$f5 isa file,
    has path "lzfkn.java",
    has size-kb 70;
$a5 (object: $f5, action: $modify) isa access;
$a55 (object: $f5, action: $view) isa access;
$permission5 (subject: $p3, access: $a5) isa permission;
$f6 isa file,
    has path "budget_2022-05-01.xlsx",
    has size-kb 758;
$a6 (object: $f6, action: $modify) isa access;
$a66 (object: $f6, action: $view) isa access;
$permission6 (subject: $p3, access: $a6) isa permission;
$permission66 (subject: $p2, access: $a66) isa permission;
$f7 isa file,
    has path "zewhb.java";
$a7 (object: $f7, action: $modify) isa access;
$a77 (object: $f7, action: $view) isa access;
$permission7 (subject: $p3, access: $a7) isa permission;
$permission77 (subject: $p2, access: $a77) isa permission;
$f8 isa file,
    has path "budget_2021-08-01.xlsx",
    has size-kb 1705;
$a8 (object: $f8, action: $modify) isa access;
$a88 (object: $f8, action: $view) isa access;
$permission8 (subject: $p3, access: $a8) isa permission;
$permission88 (subject: $p2, access: $a88) isa permission;
$f9 isa file,
    has path "LICENSE";
$a9 (object: $f9, action: $modify) isa access;
$a99 (object: $f9, action: $view) isa access;
$permission9 (subject: $p3, access: $a9) isa permission;
$permission99 (subject: $p2, access: $a99) isa permission;
$f10 isa file,
    has path "README.md";
$a10 (object: $f10, action: $modify) isa access;
$a100 (object: $f10, action: $view) isa access;
$permission10 (subject: $p3, access: $a10) isa permission;
$permission100 (subject: $p2, access: $a100) isa permission;

Testing a database

With the schema defined and data loaded, we test our database to make sure it’s ready. To test the database, we send a query to count the number of users in the database:

bool dbCheck(Session* dataSession) {
    printf("Testing the database...");
    bool result = false;
    Transaction* tx = NULL;
    Options* opts = options_new();
    tx = transaction_new(dataSession, Read, opts);
    if (tx == NULL || FAILED()) {
        handle_error("Transaction failed to start.");
        goto cleanup;
    }
    const char* testQuery = "match $u isa user; get $u; count;";
    Concept* response = concept_promise_resolve(query_get_aggregate(tx, testQuery, opts));
    if (response == NULL || FAILED()) {
        handle_error("Query execution failed.");
        goto cleanup;
    }
    long answer = value_get_long(response);
    if (FAILED()) {
        handle_error("Value convertion failed.");
        goto cleanup;
    }

    if (answer == 3) {
        printf("Passed\n");
        result = true;
    } else {
        printf("Failed with the result: %ld\nExpected result: 3.\n", answer);
        exit(EXIT_FAILURE);
    }
cleanup:
    concept_drop(response);
    transaction_close(tx);
    options_drop(opts);
    return result;
}

Query examples

After database setup is complete, we proceed with querying our database with different types of queries in the queries() function:

bool queries(DatabaseManager* dbManager, const char* dbName) {
    printf("\nRequest 1 of 6: Fetch all users as JSON objects with full names and emails\n");
    int userCount = fetchAllUsers(dbManager, dbName);

    const char* newName = "Jack Keeper";
    const char* newEmail = "jk@typedb.com";
    printf("\nRequest 2 of 6: Add a new user with the full-name %s and email %s\n", newName, newEmail);
    int newUserAdded = insertNewUser(dbManager, dbName, newName, newEmail);

    const char* name = "Kevin Morrison";
    printf("\nRequest 3 of 6: Find all files that the user %s has access to view (no inference)\n", name);
    int noFilesCount = getFilesByUser(dbManager, dbName, name, false);

    printf("\nRequest 4 of 6: Find all files that the user %s has access to view (with inference)\n", name);
    int filesCount = getFilesByUser(dbManager, dbName, name, true);

    const char* oldPath = "lzfkn.java";
    const char* newPath = "lzfkn2.java";
    printf("\nRequest 5 of 6: Update the path of a file from %s to %s\n", oldPath, newPath);
    int16_t updatedFiles = updateFilePath(dbManager, dbName, oldPath, newPath);

    const char* filePath = "lzfkn2.java";
    printf("\nRequest 6 of 6: Delete the file with path %s\n", filePath);
    bool deleted = deleteFile(dbManager, dbName, filePath);

    return true;
}

The queries are as follows:

  1. Fetch query — to retrieve information in a JSON format

  2. Insert query — to insert new data into the database

  3. Get query — to retrieve data from the database as stateful objects

  4. Get query with inference — to retrieve data from the database as stateful objects using inference

  5. Update query — to replace data in the database

  6. Delete query — to delete data from the database

Every query is implemented as a function that includes some output of the query response and returns some meaningful data.

Fetch query

The main way to retrieve data from a TypeDB database is to use fetching to get values of attributes, matched by a pattern.

Let’s use a Fetch query to fetch names and emails for all users in the database:

int fetchAllUsers(DatabaseManager* dbManager, const char* dbName) {
    Options* opts = options_new();
    Transaction* tx = NULL;
    StringIterator* queryResult = NULL;
    Session* session = session_new(dbManager, dbName, Data, opts);
    if (session == NULL || FAILED()) {
        fprintf(stderr, "Failed to open session.\n");
        exit(EXIT_FAILURE);
    }
    tx = transaction_new(session, Read, opts);
    if (tx == NULL || FAILED()) {
        fprintf(stderr, "Failed to start transaction.\n");
        session_close(session);
        exit(EXIT_FAILURE);
    }

    const char* query = "match $u isa user; fetch $u: full-name, email;";
    queryResult = query_fetch(tx, query, opts);
    if (queryResult == NULL || FAILED()) {
        fprintf(stderr, "Query failed or no results.\n");
        transaction_close(tx);
        session_close(session);
        exit(EXIT_FAILURE);
    }

    char* userJSON = string_iterator_next(queryResult);
    int counter = 1;
    while (userJSON != NULL) {
        printf("User #%d: ", counter++);
        printf("%s \n",userJSON);
        userJSON = string_iterator_next(queryResult);
    }
cleanup:
    string_iterator_drop(queryResult);
    transaction_close(tx);
    session_close(session);
    return counter-1;
}

We get the response as a stream of JSONs and iterate through it to print all String values from each JSON.

Insert query

Let’s insert a new user with a full-name and email attributes to the database.

int insertNewUser(DatabaseManager* dbManager, const char* dbName, const char* name, const char* email) {
    Options* opts = options_new();
    Transaction* tx = NULL;
    Session* session = NULL;
    ConceptMapIterator* response = NULL;
    ConceptMap* conceptMap = NULL;

    session = session_new(dbManager, dbName, Data, opts);
    if (session == NULL || FAILED()) {
        fprintf(stderr, "Failed to open session.\n");
        exit(EXIT_FAILURE);
    }

    tx = transaction_new(session, Write, opts);
    if (tx == NULL || FAILED()) {
        fprintf(stderr, "Failed to start transaction.\n");
        session_close(session);
        exit(EXIT_FAILURE);
    }

    char query[512];
    snprintf(query, sizeof(query), "insert $p isa person, has full-name $fn, has email $e; $fn == '%s'; $e == '%s';", name, email);
    response = query_insert(tx, query, opts);
    if (response == NULL || FAILED()) {
        fprintf(stderr, "Failed to execute insert query.\n");
        transaction_close(tx);
        session_close(session);
        exit(EXIT_FAILURE);
    }

    int insertedCount = 0;
    while ((conceptMap = concept_map_iterator_next(response)) != NULL) {
        Concept* fnConcept = concept_map_get(conceptMap, "fn");
        Concept* eConcept = concept_map_get(conceptMap, "e");
        const char* fullName = value_get_string(attribute_get_value(fnConcept));
        const char* userEmail = value_get_string(attribute_get_value(eConcept));
        printf("Added new user. Name: %s, E-mail: %s\n", fullName, userEmail);
        concept_drop(fnConcept);
        concept_drop(eConcept);
        concept_map_drop(conceptMap);
        insertedCount++;
    }
    concept_map_iterator_drop(response);
    void_promise_resolve(transaction_commit(tx));
    session_close(session);
    return insertedCount;
}

The Insert query returns a stream of ConceptMaps: one for every insert clause execution. We iterate through the stream and print name and email for each ConceptMap returned.

Since the Insert query has no match clause, the insert clause is executed exactly once. But the Insert query always returns a list of ConceptMap objects, where every ConceptMap represents an inserted result.

Get query

Let’s retrieve all files available for a user with a getFilesByUser() function. It can be used with or without inference enabled.

int getFilesByUser(DatabaseManager* dbManager, const char* dbName, const char* name, bool inference) {
    Transaction* tx = NULL;
    Session* session = NULL;
    ConceptMapIterator* userResult = NULL;
    ConceptMapIterator* response = NULL;
    ConceptMap* cm = NULL;
    Options* opts = options_new();

    session = session_new(dbManager, dbName, Data, opts);
    if (session == NULL || FAILED()) {
        fprintf(stderr, "Failed to open session.\n");
        options_drop(opts);
        exit(EXIT_FAILURE);
    }

    options_set_infer(opts, inference);
    tx = transaction_new(session, Read, opts);
    if (tx == NULL || FAILED()) {
        fprintf(stderr, "Failed to start transaction.\n");
        options_drop(opts);
        session_close(session);
        exit(EXIT_FAILURE);
    }

    char query[512];
    snprintf(query, sizeof(query), "match $u isa user, has full-name '%s'; get;", name);
    userResult = query_get(tx, query, options_new());
    int userCount = 0;
    while (concept_map_iterator_next(userResult) != NULL) { userCount++; }
    concept_map_iterator_drop(userResult);

    if (userCount > 1) {
        fprintf(stderr, "Error: Found more than one user with that name.\n");
    } else if (userCount == 1) {
        snprintf(query, sizeof(query), "match $fn == '%s'; $u isa user, has full-name $fn; $p($u, $pa) isa permission; $o isa object, has path $fp; $pa($o, $va) isa access;$va isa action, has name 'view_file'; get $fp; sort $fp asc;", name);
        response = query_get(tx, query, options_new());
        int fileCount = 0;
        while ((cm = concept_map_iterator_next(response)) != NULL) {
            Concept* filePathConcept = concept_map_get(cm, "fp");
            const char* filePath = value_get_string(attribute_get_value(filePathConcept));
            printf("File #%d: %s\n", ++fileCount, filePath);
            concept_drop(filePathConcept);
            concept_map_drop(cm);
        }
        concept_map_iterator_drop(response);
        if (fileCount == 0) {
            printf("No files found. Try enabling inference.\n");
        }
    } else {
        fprintf(stderr, "Error: No users found with that name.\n");
    }

    transaction_close(tx);
    options_drop(opts);
    session_close(session);
    return userCount;
}

We call the function with the inference disabled (false) and expect it to return no results, as the query pattern matches only files available for view_file action, and there are no such files initially in the database.

The getFilesByUser() function checks that there is only one user matched with the name provided by an input parameter. It then executes the query to find the files, collect the results, and iterates through them to print a value of every matched path attribute.

For bigger numbers of results, it might be faster to iterate through a stream, rather than collect and store the results first.

Get query with inference

To get query results with inferred data, let’s enable the infer parameter of the TypeDB transaction options. We use the same getFilesByUser() function, but set the inference parameter to true when we call it again. The add-view-permission rule provides us with some inferred results this time.

Update query

Let’s replace a path for one of the files with a new path. We can do that by deleting ownership of the old path attribute from the file entity and assigning it with ownership of the new path attribute with the Update query:

int16_t updateFilePath(DatabaseManager* dbManager, const char* dbName, const char* oldPath, const char* newPath) {
    Transaction* tx = NULL;
    Session* session = NULL;
    Options* opts = options_new();
    ConceptMapIterator* response = NULL;
    session = session_new(dbManager, dbName, Data, opts);
    if (session == NULL || FAILED()) {
        fprintf(stderr, "Failed to open session.\n");
        exit(EXIT_FAILURE);
    }

    tx = transaction_new(session, Write, opts);
    if (tx == NULL || FAILED()) {
        fprintf(stderr, "Failed to start transaction.\n");
        session_close(session);
        exit(EXIT_FAILURE);
    }

    char query[512];
    snprintf(query, sizeof(query), "match $f isa file, has path $old_path; $old_path = '%s'; delete $f has $old_path; insert $f has path $new_path; $new_path = '%s';", oldPath, newPath);

    response = query_update(tx, query, opts);
    if (response == NULL || FAILED()) {
        fprintf(stderr, "Query failed or no results.\n");
        transaction_close(tx);
        session_close(session);
        exit(EXIT_FAILURE);
    }

    int16_t count = 0;
    while (concept_map_iterator_next(response) != NULL) {
        count++;
    }
    concept_map_iterator_drop(response);

    if (count > 0) {
        void_promise_resolve(transaction_commit(tx));
        printf("Total number of paths updated: %d.\n", count);
    } else {
        printf("No matched paths: nothing to update.\n");
    }

    session_close(session);
    return count;
}

We iterate through the response of the Update query and count the length of it to determine the number of times the delete and insert clauses are executed. We then commit the changes only if the number meets our expectation.

Delete query

Finally, let’s delete the same file we updated the path for. First, we match the file in a Get (or Fetch) query to check how many files get matched to prevent unplanned deletes. If the number (or any other relevant parameters) of matched results is as expected, we proceed with a Delete query with the same match clause.

By using the same write transaction we employ snapshot isolation to prevent any other transactions from changing the expected results. If any other transaction makes a conflicting change before we commit this transaction, then our transaction fails upon a commit.

bool deleteFile(DatabaseManager* dbManager, const char* dbName, const char* path) {
    bool result = false;
    Transaction* tx = NULL;
    Session* session = NULL;
    Options* opts = options_new();
    ConceptMapIterator* response = NULL;

    session = session_new(dbManager, dbName, Data, opts);
    if (session == NULL || FAILED()) {
        fprintf(stderr, "Failed to open session.\n");
        exit(EXIT_FAILURE);
    }

    tx = transaction_new(session, Write, opts);
    if (tx == NULL || FAILED()) {
        fprintf(stderr, "Failed to start transaction.\n");
        session_close(session);
        exit(EXIT_FAILURE);
    }

    char query[256];
    snprintf(query, sizeof(query), "match $f isa file, has path '%s'; get;", path);
    response = query_get(tx, query, opts);
    if (response == NULL || FAILED()) {
        fprintf(stderr, "Query failed or no results.\n");
        transaction_close(tx);
        session_close(session);
        exit(EXIT_FAILURE);
    }

    int16_t count = 0;
    while (concept_map_iterator_next(response) != NULL) {
        count++;
    }
    concept_map_iterator_drop(response);

    if (count == 1) { // Delete the file if exactly one was found
        snprintf(query, sizeof(query), "match $f isa file, has path '%s'; delete $f isa file;", path);
        if (query_delete(tx, query, opts) == NULL || FAILED()) {
            fprintf(stderr, "Failed to delete file.\n");
            transaction_close(tx);
            session_close(session);
            exit(EXIT_FAILURE);
        }
        void_promise_resolve(transaction_commit(tx));
        printf("The file has been deleted.\n");
        result = true;
    } else if (count > 1) fprintf(stderr, "Matched more than one file with the same path.\nNo files were deleted.\n");
    else fprintf(stderr, "No files matched in the database.\nNo files were deleted.\n");

    session_close(session);
    return result;
}

Error handling

To simplify error handling, we use additional functions: handle_error() and check_error_may_print().

void handle_error(const char* message) {
    fprintf(stderr, "%s\n", message);
    exit(EXIT_FAILURE);
}

bool check_error_may_print(const char* filename, int lineno) {
    if (check_error()) {
        Error* error = get_last_error();
        char* errcode = error_code(error);
        char* errmsg = error_message(error);
        fprintf(stderr, "Error!\nCheck called at %s:%d\n%s: %s\n", filename, lineno, errcode, errmsg);
        string_free(errmsg);
        string_free(errcode);
        error_drop(error);
        return true;
    } else return false;
}

Learn more

The full source code of this sample application.

The full API reference for the TypeDB C driver.