# Lesson 7.2: Relation patterns

We initially learned how to use TypeQL’s tuple syntax to represent relations in [Lesson 3.1](../../3-reading-data/3.1-fetching-simple-data/index.md). 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)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.

```typeql
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);
```

```typeql
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.

```typeql
order-line (order: $order, item: $book);
```

```typeql
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.

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

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

```typeql
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)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.

```typeql
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.

```typeql
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.

```typeql
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](../../4-writing-data/4.3-deleting-data/index.md), which deletes all relations in which a given review is a roleplayer.

```typeql
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.

```typeql
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

```typeql
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

```typeql
match
$book isa book;
order-line (item: $book);
promotion-inclusion (item: $book);
fetch {
  "title": $book.title,
};
```

## [](#_inferred_types_and_roles)Inferred types and roles

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

```typeql
$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.

```typeql
rating ($review, $frankenstein);
```

```typeql
($book, $review);
```

```typeql
($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)Inferring roles

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

```typeql
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.

```typeql
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)Inferring relation types

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

```typeql
(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`.

```typeql
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.

[Patterns as constraints](../7.1-patterns-as-constraints/index.md) [Logical operators](../7.3-logical-operators/index.md)

[Edit on GitHub](https://github.com/typedb/typedb-docs/edit/3.x-development/academy/modules/ROOT/pages/7-understanding-query-patterns/7.2-relation-patterns.adoc) Edit this page on GitHub.