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 roleplayers) can be represented using a tuple with two or three elements respectively.
(role-1: $a, role-2: $b) isa binary-relation;
(role-1: $a, role-2: $b, role-3: $c) isa ternary-relation;
We have seen examples of these in the bookstore schema, for instance order-line
and delivery
relations.
(order: $order, item: $book) isa order-line;
(deliverer: $courier, delivered: $order, destination: $address) isa delivery;
This can be naturally extended to represent relations with any number of roleplayers, known as n-ary relations, by using a tuple with n elements.
(role-1: $a, role-2: $b, role-3: $c, role-4: $d, ...) isa n-ary-relation;
We can also represent relations with only a single roleplayer, known as unary relations, though doing so is only useful in very niche cases.
(role-1: $a) isa unary-relation;
Nullary relations, those with zero roleplayers, are not permitted in TypeDB.
Partial relation tuples
The tuple syntax can also be used to match relations with only a partial number of roleplayers specified. By default, the tuple representation of a relation represents a relation with at least the roleplayers described. Let’s consider the previous example of a delivery
relation again.
(deliverer: $courier, delivered: $order, destination: $address) isa delivery;
In theory, this statement would actually match relations that have roleplayers other than $courier
, $order
, and $address
, as long as the relations have those roleplayers at a minimum. We can use this property to represent relations partially, by omitting roleplayers 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;
(deliverer: $courier, delivered: $order, destination: $address) isa delivery;
fetch
$order: id;
$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;
(deliverer: $courier, delivered: $order) isa delivery;
fetch
$order: id;
$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 ($review-4) isa relation;
delete
$relation isa relation;
The partial representation can be as minimal as required. If we do not care about any of the roleplayers, we could skip out the tuple entirely! The following query retrieves the timestamps of any action-execution
relations, regardless of their roleplayers. Essentially, it lists the times of all system actions performed by anyone.
match
$execution isa action-execution;
fetch
$execution: timestamp;
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
book sub entity,
abstract,
owns isbn-13 @key,
owns isbn-10 @unique,
owns title,
owns page-count,
owns genre,
owns price,
plays contribution:work,
plays publishing:published,
plays promotion-inclusion:item,
plays order-line:item,
plays rating:rated,
plays recommendation:recommended;
hardback sub book,
owns stock;
paperback sub book,
owns stock;
ebook sub book;
contributor sub entity,
owns name,
plays contribution:contributor,
plays authoring:author,
plays editing:editor,
plays illustrating:illustrator;
company sub entity,
abstract,
owns name;
publisher sub company,
plays publishing:publisher;
courier sub company,
plays delivery:deliverer;
publication sub entity,
owns year,
plays publishing:publication,
plays locating:located;
user sub entity,
owns id @key,
owns name,
owns birth-date,
plays action-execution:executor,
plays locating:located,
plays recommendation:recipient;
order sub entity,
owns id @key,
owns status,
plays order-line:order,
plays action-execution:action,
plays delivery:delivered;
promotion sub entity,
owns code @key,
owns name,
owns start-timestamp,
owns end-timestamp,
plays promotion-inclusion:promotion;
review sub entity,
owns id @key,
owns score,
owns verified,
plays rating:review,
plays action-execution:action;
login sub entity,
owns success,
plays action-execution:action;
address sub entity,
owns street,
plays delivery:destination,
plays locating:located;
place sub entity,
abstract,
owns name,
plays locating:located,
plays locating:location;
city sub place;
state sub place;
country sub place;
contribution sub relation,
relates contributor,
relates work;
authoring sub contribution,
relates author as contributor;
editing sub contribution,
relates editor as contributor;
illustrating sub contribution,
relates illustrator as contributor;
publishing sub relation,
relates publisher,
relates published,
relates publication;
promotion-inclusion sub relation,
relates promotion,
relates item,
owns discount;
order-line sub relation,
relates order,
relates item,
owns quantity,
owns price;
rating sub relation,
relates review,
relates rated;
action-execution sub relation,
relates action,
relates executor,
owns timestamp;
delivery sub relation,
relates deliverer,
relates delivered,
relates destination;
locating sub relation,
relates located,
relates location;
recommendation sub relation,
relates recommended,
relates recipient;
isbn sub attribute, abstract, value string;
isbn-13 sub isbn;
isbn-10 sub isbn;
title sub attribute, value string;
page-count sub attribute, value long;
genre sub attribute, value string;
stock sub attribute, value long;
price sub attribute, value double;
discount sub attribute, value double;
id sub attribute, value string;
code sub attribute, value string;
name sub attribute, value string;
birth-date sub attribute, value datetime;
street sub attribute, value string;
year sub attribute, value long;
quantity sub attribute, value long;
score sub attribute, value long;
verified sub attribute, value boolean;
timestamp sub attribute, value datetime;
start-timestamp sub attribute, value datetime;
end-timestamp sub attribute, value datetime;
status sub attribute, value string, regex "^(paid|dispatched|delivered|returned|canceled)$";
success sub attribute, value boolean;
rule review-verified-by-purchase:
when {
($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;
} then {
$review has verified true;
};
rule review-unverified:
when {
$review isa review;
not { $review has verified true; };
} then {
$review has verified false;
};
rule book-recommendation-by-genre:
when {
$user isa user;
$liked-book isa book;
{
($user, $order) isa action-execution;
($order, $liked-book) isa order-line;
} or {
($user, $review) isa action-execution;
($review, $liked-book) isa rating;
$review has score >= 7;
};
$new-book isa book;
not { {
($user, $order) isa action-execution;
($order, $new-book) isa order-line;
} or {
($user, $review) isa action-execution;
($review, $new-book) isa rating;
}; };
$liked-book has genre $shared-genre;
$new-book has genre $shared-genre;
not { {
$shared-genre == "fiction";
} or {
$shared-genre == "nonfiction";
}; };
} then {
(recommended: $new-book, recipient: $user) isa recommendation;
};
rule book-recommendation-by-author:
when {
$user isa user;
$liked-book isa book;
{
($user, $order) isa action-execution;
($order, $liked-book) isa order-line;
} or {
($user, $review) isa action-execution;
($review, $liked-book) isa rating;
$review has score >= 7;
};
$new-book isa book;
not { {
($user, $order) isa action-execution;
($order, $new-book) isa order-line;
} or {
($user, $review) isa action-execution;
($review, $new-book) isa rating;
}; };
($liked-book, $shared-author) isa authoring;
($new-book, $shared-author) isa authoring;
} then {
(recommended: $new-book, recipient: $user) isa recommendation;
};
rule order-line-total-retail-price:
when {
($order) isa action-execution, has timestamp $order-time;
$line ($order, $item) isa order-line;
not {
($promotion, $item) isa promotion-inclusion;
$promotion has start-timestamp <= $order-time,
has end-timestamp >= $order-time;
};
$item has price $retail-price;
$line has quantity $quantity;
?line-total = $quantity * $retail-price;
} then {
$line has price ?line-total;
};
rule order-line-total-discounted-price:
when {
($order) isa action-execution, has timestamp $order-time;
$line ($order, $item) isa order-line;
($best-promotion, $item) isa promotion-inclusion, has discount $best-discount;
$best-promotion has start-timestamp <= $order-time,
has end-timestamp >= $order-time;
not {
($other-promotion, $item) isa promotion-inclusion, has discount > $best-discount;
$other-promotion has start-timestamp <= $order-time,
has end-timestamp >= $order-time;
};
$item has price $retail-price;
?discounted-price = round(100 * $retail-price * (1 - $best-discount)) / 100;
$line has quantity $quantity;
?line-total = $quantity * ?discounted-price;
} then {
$line has price ?line-total;
};
rule transitive-location:
when {
(location: $parent-place, located: $child-place) isa locating;
(location: $child-place, located: $x) isa locating;
} then {
(location: $parent-place, located: $x) isa locating;
};
Sample solution
match
$book isa book;
(item: $book) isa order-line;
(item: $book) isa promotion-inclusion;
fetch
$book: title;
Inferred types and roles
In the previous Delete query example, the partial relation tuple omitted the role played by $review-4
.
$relation ($review-4) isa relation;
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.
($review, $frankenstein) isa rating;
($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 roleplayers.
($a, $b) isa example-relation;
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;
($ca, $related-place) isa locating;
fetch
$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 (item: $book);
fetch
$including-book: attribute;
Namely, the query would match instances of both order-line
and promotion-inclusion
for $including-book
, and return their attributes.