Lesson 7.2: Relation patterns

We initially learned how to use TypeQL’s tuple syntax to represent relations in Lesson 3.1. In this lesson, we will cover the many flexible ways that TypeQL allows us to format relation tuples to represent different kinds of relations.

N-ary relations

Binary and ternary relations (those with two or three role players) can be represented using a tuple with two or three elements respectively.

binary-relation (role-1: $a, role-2: $b);

# note: could also be written in the following forms, if we wanted to return the relation itself:
# $r isa binary-relation (role-1: $a, role-2: $b);
# or
# $r isa binary-relation, links (role-1: $a, role-2: $b);
ternary-relation (role-1: $a, role-2: $b, role-3: $c);

We have seen examples of these in the bookstore schema, for instance order-line and delivery relations.

order-line (order: $order, item: $book);
delivery (deliverer: $courier, delivered: $order, destination: $address);

This can be naturally extended to represent relations with any number of role players, known as n-ary relations, by using a tuple with n elements.

n-ary-relation (role-1: $a, role-2: $b, role-3: $c, role-4: $d, ...) isa;

We can also represent relations with only a single roleplayer, known as unary relations, though doing so is only useful in very niche cases.

unary-relation (role-1: $a);

Nullary relations, those with zero role players, are not permitted in TypeDB and are automatically deleted commit time.

Partial relation tuples

The tuple syntax can also be used to match relations with only a partial number of role players specified. By default, the tuple representation of a relation represents a relation with at least the role players described. Let’s consider the previous example of a delivery relation again.

delivery (deliverer: $courier, delivered: $order, destination: $address);

In theory, this statement would actually match relations that have role players other than $courier, $order, and $address, as long as the relations have those role players at a minimum. We can use this property to represent relations partially, by omitting role players that we are not interested in. Consider the following query, which retrieves a list of orders and their assigned couriers.

match
$order isa order;
$courier isa courier;
delivery (deliverer: $courier, delivered: $order, destination: $address);
fetch {
  "order-id": $order.id,
  "courier": $courier.name,
};

For this query, we do not actually need the destination roleplayer at all, so we can simply omit it instead.

match
$order isa order;
$courier isa courier;
delivery (deliverer: $courier, delivered: $order);
fetch {
  "order-id": $order.id,
  "courier": $courier.name,
};

Functionally, there is a slight difference in semantics. The first form of the query, where we include the destination, will only match instances of delivery where there is a roleplayer of destination, while the second form can match instances where there is no destination roleplayer at all. However, if we always instantiate delivery relations with exactly one of each roleplayer, then these two queries will have identical results.

Partial relations are essential to certain queries. For instance, we encountered the following query in Lesson 4.3, which deletes all relations in which a given review is a roleplayer.

match
$review-4 isa review, has id "r0004";
$relation links ($review-4);
delete
$relation;

The partial representation can be as minimal as required. If we do not care about any of the role players, we could skip out the tuple entirely! The following query retrieves the timestamps of any action-execution relations, regardless of their role players. Essentially, it lists the times of all system actions performed by anyone.

match
$execution isa action-execution;
fetch {
  "timestamp": $execution.timestamp,
};
Exercise

Write a Fetch query to retrieve the titles of all books that have been ordered at least once and have also been included in at least one promotion, without using more than a single variable.

You may find it useful to refer to the bookstore’s schema.

Schema
define

entity book @abstract,
    owns isbn @card(0..2),
    owns isbn-13 @key,
    owns isbn-10 @unique,
    owns title,
    owns page-count,
    owns genre @card(0..),
    owns price,
    plays contribution:work,
    plays publishing:published,
    plays promotion-inclusion:item,
    plays order-line:item,
    plays rating:rated,
    plays recommendation:recommended;

entity hardback sub book,
    owns stock;

entity paperback sub book,
    owns stock;

entity ebook sub book;

entity contributor,
    owns name,
    plays contribution:contributor,
    plays authoring:author,
    plays editing:editor,
    plays illustrating:illustrator;

entity company @abstract,
    owns name;

entity publisher sub company,
    plays publishing:publisher;

entity courier sub company,
    plays delivery:deliverer;

entity publication,
    owns year,
    plays publishing:publication,
    plays locating:located;

entity user,
    owns id @key,
    owns name,
    owns birth-date,
    plays action-execution:executor,
    plays locating:located,
    plays recommendation:recipient;

entity order,
    owns id @key,
    owns status,
    plays order-line:order,
    plays action-execution:action,
    plays delivery:delivered;

entity promotion,
    owns code @key,
    owns name,
    owns start-timestamp,
    owns end-timestamp,
    plays promotion-inclusion:promotion;

entity review,
    owns id @key,
    owns score,
    owns verified,
    plays rating:review,
    plays action-execution:action;

entity login,
    owns success,
    plays action-execution:action;

entity address,
    owns street,
    plays delivery:destination,
    plays locating:located;

entity place @abstract,
    owns name,
    plays locating:located,
    plays locating:location;

entity city sub place;

entity state sub place;

entity country sub place;

relation contribution,
    relates contributor,
    relates work;

relation authoring sub contribution,
    relates author as contributor;

relation editing sub contribution,
    relates editor as contributor;

relation illustrating sub contribution,
    relates illustrator as contributor;

relation publishing,
    relates publisher,
    relates published,
    relates publication;

relation promotion-inclusion,
    relates promotion,
    relates item,
    owns discount;

relation order-line,
    relates order,
    relates item,
    owns quantity,
    owns price;

relation rating,
    relates review,
    relates rated;

relation action-execution,
    relates action,
    relates executor,
    owns timestamp;

relation delivery,
    relates deliverer,
    relates delivered,
    relates destination;

relation locating,
    relates located,
    relates location;

relation recommendation,
    relates recommended,
    relates recipient;

attribute isbn @abstract, value string;
attribute isbn-13 sub isbn;
attribute isbn-10 sub isbn;
attribute title, value string;
attribute page-count, value integer;
attribute genre, value string;
attribute stock, value integer;
attribute price, value double;
attribute discount, value double;
attribute id, value string;
attribute code, value string;
attribute name, value string;
attribute birth-date, value datetime;
attribute street, value string;
attribute year, value integer;
attribute quantity, value integer;
attribute score, value integer;
attribute verified, value boolean;
attribute timestamp, value datetime;
attribute start-timestamp, value datetime;
attribute end-timestamp, value datetime;
attribute status, value string @regex("^(paid|dispatched|delivered|returned|canceled)$");
attribute success, value boolean;

# TODO: Change to check
fun is_review_verified_by_purchase($review: review) -> { order }:
  match
    ($review, $product) isa rating;
    ($order, $product) isa order-line;
    ($user, $review) isa action-execution, has timestamp $review-time;
    ($user, $order) isa action-execution, has timestamp $order-time;
    $review-time > $order-time;
  return { $order };

fun book_recommendations_for($user: user) -> {book}:
  match
    $new-book isa book;
    {
        let $new-book in book_recommendations_by_author($user);
    } or {
        let $new-book in book_recommendations_by_genre($user);
    };
  return { $new-book };

fun book_recommendations_by_genre($user: user) -> { book }:
match
    $user isa user;
    $liked-book isa book;
    {
        ($user, $order-for-liked) isa action-execution;
        ($order-for-liked, $liked-book) isa order-line;
    } or {
        ($user, $review-for-liked) isa action-execution;
        ($review-for-liked, $liked-book) isa rating;
        $review-for-liked has score >= 7;
    };
    $new-book isa book;
    not { {
        ($user, $order-for-new) isa action-execution;
        ($order-for-new, $new-book) isa order-line;
    } or {
        ($user, $review-for-new) isa action-execution;
        ($review-for-new, $new-book) isa rating;
    }; };
    $liked-book has genre $shared-genre;
    $new-book has genre $shared-genre;
    not { {
        $shared-genre == "fiction";
    } or {
        $shared-genre == "nonfiction";
    }; };
  return { $new-book };

fun book_recommendations_by_author($user: user) -> { book }:
  match
    $user isa user;
    $liked-book isa book;
    {
        ($user, $order-for-liked) isa action-execution;
        ($order-for-liked, $liked-book) isa order-line;
    } or {
        ($user, $review-for-liked) isa action-execution;
        ($review-for-liked, $liked-book) isa rating;
        $review-for-liked has score >= 7;
    };
    $new-book isa book;
    not { {
        ($user, $order-for-new) isa action-execution;
        ($order-for-new, $new-book) isa order-line;
    } or {
        ($user, $review-for-new) isa action-execution;
        ($review-for-new, $new-book) isa rating;
    }; };
    ($liked-book, $shared-author) isa authoring;
    ($new-book, $shared-author) isa authoring;
  return { $new-book };

fun order_line_best_price($line: order-line) -> { double }:
  match
    ($order) isa action-execution, has timestamp $order-time;
    $line isa order-line, links ($order, $item);
    $item has price $retail-price;
    let $time_value = $order-time;
    let $best-discount = best_discount_for_item($item, $time_value);
    let $discounted-price = round(100 * $retail-price * (1 - $best-discount)) / 100;
    $line has quantity $quantity;
    let $line-total = $quantity * $discounted-price;
  return { $line-total };

fun best_discount_for_item($item: book, $order-time: datetime) -> double:
  match
    {
        $inclusion isa promotion-inclusion,
            links ($promotion, $item),
            has discount $discount-attr;
        $promotion has start-timestamp <= $order-time,
            has end-timestamp >= $order-time;
        let $discount = $discount-attr;
    } or {
        let $discount = 0.0; # default
    };
return max($discount);

fun transitive_places($place: place) -> { place }:
  match
    {
      locating (located: $place, location: $parent);
    } or {
      locating (located: $place, location: $middle);
      let $parent in transitive_places($middle);
    };
  return { $parent };
Sample solution
match
$book isa book;
order-line (item: $book);
promotion-inclusion (item: $book);
fetch {
  "title": $book.title,
};

Inferred types and roles

In the previous Delete query example, the partial relation tuple omitted the role played by $review-4.

$relation links ($review-4);

We have also seen examples in previous lessons where we omit types and roles from relation tuples and allow TypeDB to infer them. Some such statements are shown here.

rating ($review, $frankenstein);
($book, $review);
($user, action: $review);

As we can see from these examples, there are two ways in which we can make use of type inference when representing relations, sometimes in combination. The first is to infer one or more roles in the relation, and the second is to infer its type.

Inferring roles

To allow TypeDB to infer roles, we omit the roles from the tuple representation and list only the role players.

example-relation ($a, $b);

Practically, this statement will match any example-relation in which $a and $b both play roles, regardless of what those roles are. Consider the following Fetch query in the context of the bookstore schema.

match
$ca isa state, has name "California";
$related-place isa place;
locating ($ca, $related-place);
fetch {
  "related-place": $related-place.name,
};

This query returns the names of places that are associated with California, either because they are located in California (like Sacramento) or because California is located in them (like the US). In the first case, $ca will be playing locating:location and $related-place will be playing locating:located, and in the second case the roles will be reversed. In this case, we have omitted both roles from the tuple, but it is also possible to mix explicitly stated and inferred roles within a single relation tuple as needed, as we have seen above.

Inferring relation types

To allow TypeDB to infer the type of the relation, we simply omit the isa statement following the tuple representation.

(role-1: $a, role-2: $b);

Practically, this statement will match any relation with the supplied role names. Here we must recall the difference between role names and role labels. A role name includes only the name of the role itself, for instance publisher, while a role label also includes the name of the parent relation, for instance publishing:publisher. Because we use role names rather than labels in relation tuples, omitting the relation’s type as above will allow TypeDB to match any relation which uses the same role names, even if their labels are different. The following query retrieves the attributes of any relation in which $book plays a role with name item.

match
$book isa book, has isbn "9780500026557";
$including-book links (item: $book);
fetch {
  "attributes": { $including-book.* },
};

Namely, the query would match instances of both order-line and promotion-inclusion for $including-book, and return their attributes.