Lesson 3.4: Fetching schema types

Variablizing types

So far, we’ve seen that the output of Fetch queries includes a type field for every retrieved data instance and their attributes. However, there are also types involved in the query that are not returned. Consider this polymorphic query from Lesson 3.2, where we asked for books that a user had interacted with in some way on the web store.

match
$user isa user, has id "u0008";
$book isa book;
action-execution (executor: $user, action: $action);
$rel links ($book, $action);
fetch {
  "isbn": [$book.isbn],
  "title": $book.title
};
{
  "title": "Electron Backscatter Diffraction in Materials Science",
  "isbn": [
    "9780387881355",
    "0387881352"
  ]
}
{
  "isbn": [
    "9780060929794",
    "0060929790"
  ],
  "title": "One Hundred Years of Solitude"
}
{
  "isbn": [
    "9780008627843",
    "0008627843"
  ],
  "title": "The Hobbit"
}
{
  "isbn": [
    "9780500026557",
    "0500026556"
  ],
  "title": "Hokusai's Fuji"
}
{
  "isbn": [
    "9780008627843",
    "0008627843"
  ],
  "title": "The Hobbit"
}
{
  "title": "Classical Mythology",
  "isbn": [
    "9780195153446",
    "0195153448"
  ]
}

While the results list the books, they do not tell us anything about the nature of the user’s interaction with them. We can easily retrieve this information by adding a couple of additional lines to the query.

match
$user isa user, has id "u0008";
$book isa book;
action-execution (executor: $user, action: $action);
$rel links ($book, $action);
$action isa! $action-type;   # new line
fetch {
  "isbn": [ $book.isbn ],
  "title": $book.title,
  "action-type": $action-type
};

If we run this new query, we now also retrieve the type of $action!

{
  "title": "Electron Backscatter Diffraction in Materials Science",
  "isbn": [
    "9780387881355",
    "0387881352"
  ],
  "action-type": {
    "kind": "entity",
    "label": "order"
  }
}
{
  "isbn": [
    "9780060929794",
    "0060929790"
  ],
  "title": "One Hundred Years of Solitude",
  "action-type": {
    "kind": "entity",
    "label": "order"
  }
}
{
  "title": "The Hobbit",
  "action-type": {
    "kind": "entity",
    "label": "order"
  },
  "isbn": [
    "9780008627843",
    "0008627843"
  ]
}
{
  "action-type": {
    "kind": "entity",
    "label": "review"
  },
  "title": "Hokusai's Fuji",
  "isbn": [
    "9780500026557",
    "0500026556"
  ]
}
{
  "isbn": [
    "9780008627843",
    "0008627843"
  ],
  "action-type": {
    "kind": "entity",
    "label": "review"
  },
  "title": "The Hobbit"
}
{
  "action-type": {
    "kind": "entity",
    "label": "review"
  },
  "isbn": [
    "9780195153446",
    "0195153448"
  ],
  "title": "Classical Mythology"
}

We have been able to retrieve this information by introducing a new $action-type variable in an additional statement.

$action isa! $action-type;

Here we’ve used the isa! keyword which, like the isa keyword, is used to specify the type of a variable. However, while the isa keyword simultaneously matches a type and all of its subtypes, the isa! keyword matches a type exactly, without using inheritance polymorphism. We will see more examples of this throughout this lesson and later in the course.

Previous isa statements we’ve used have used a variable as the subject of the statement and a type label as the object, but in this isa! statement we’ve also used a variable, $action-type, as the object. The isa and isa! keywords must always take a data instance as the subject and a type as the object, which means that the variable $action-type represents a type rather than a data instance! Here we’ve made use of type variablization, one of TypeQL’s most powerful features.

Exercise

Modify the above query to also retrieve the type of the relation between $book and $action.

Sample solution
match
$user isa user, has id "u0008";
$book isa book;
action-execution (executor: $user, action: $action);
$rel isa! $relation-type, links ($book, $action);
$action isa! $action-type;
fetch {
  "isbn": [$book.isbn],
  "title": $book.title,
  "action-type": $action-type,
  "relation-type": $relation-type,
};

Querying type hierarchies

In addition to using type variablization to retrieve the types of data instances returned in our queries, we can also query the schema directly, without having to involve data instances at all. For example, the following query retrieves the subtypes of book.

match $book_type sub book;
fetch { "book-type" : $book_type };
{ "book-type": { "kind": "entity", "label": "book" } }
{ "book-type": { "kind": "entity", "label": "hardback" } }
{ "book-type": { "kind": "entity", "label": "paperback" } }
{ "book-type": { "kind": "entity", "label": "ebook" } }

These are indeed the subtypes of book. We also retrieve the type book itself because it is trivially its own subtype. For this query, we’ve used the sub keyword, which matches subtypes of a given supertype. Unlike the isa keyword, both the subject and the object of sub statements must be types. In this case, we’ve variablized the subject of the sub statement to query subtypes, but we could also variablize the object to query supertypes. In the following example, we query the supertypes of publisher.

match publisher sub $supertype;
fetch { "supertype" : $supertype };
{ "supertype": { "kind": "entity", "label": "company" } }
{ "supertype": { "kind": "entity", "label": "publisher" } }

If we would like to retrieve only direct subtypes or supertypes of a type, we can instead use the sub! keyword, similar to the isa! keyword!

match publisher sub! $supertype;
fetch { "supertype" : $supertype };
{ "supertype": { "kind": "entity", "label": "company" } }
Exercise

Write a query to retrieve the direct supertype of city.

Sample solution
match
city sub! $supertype;
fetch {
  "supertype": $supertype,
};

Now write a query to determine the other types that share that direct supertype.

Hint

The query should retrieve the direct subtypes of place.

Sample solution
match $place_type sub! place;
fetch { "place-type" : $place_type };

Querying ownership interfaces

In addition to querying the type hierarchies defined in the schema, we can also query the defined ownership and role interfaces. In the following query, we retrieve the list of attribute types that the entity type promotion owns.

match promotion owns $attribute;
fetch { "attribute-type": $attribute };
{ "attribute-type": { "kind": "attribute", "label": "code", "valueType": "string" } }
{ "attribute-type": { "kind": "attribute", "label": "name", "valueType": "string" } }
{ "attribute-type": { "kind": "attribute", "label": "start-timestamp", "valueType": "datetime" } }
{ "attribute-type": { "kind": "attribute", "label": "end-timestamp", "valueType": "datetime" } }

To query ownerships, we use the owns keyword. As with the sub keyword, we can invert the intent of the statement by variablizing the object instead of the subject. We do this in the next query to retrieve the list of types that own the attribute type isbn-13.

match $type owns isbn-13;
fetch { "type" : $type };
{ "type": { "kind": "entity", "label": "book" } }
{ "type": { "kind": "entity", "label": "hardback" } }
{ "type": { "kind": "entity", "label": "paperback" } }
{ "type": { "kind": "entity", "label": "ebook" } }
Exercise

Write a query to retrieve the list of types that own name.

Sample solution
#!test[schema]
#{{
#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#

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 };
#}}
#!test[read]
match
$type owns name;
fetch {
  "owner": $type,
};

Querying role interfaces

Ownerships of attribute types are queried using a single keyword owns, but roles in relations must be queried using two different keywords: relates and plays. We will see how they work in the next few examples. To begin with, we will use the relates keyword to retrieve the list of roles in the locating relation.

match locating relates $role;
fetch { "role" : $role };
{ "role": { "kind": "relation:role", "label": "locating:located" } }
{ "role": { "kind": "relation:role", "label": "locating:location" } }

When not part of a relation tuple, roles are described by role labels, comprising two terms specified by a : delimiter. The first term is the name of the relation that the role is part of, and the second is the name of the role in the scope of the relation. This is because multiple relations are permitted to use the same role names. So, for instance, the label locating:located represents the located role of the locating relation type.

To query for types that play a role using the plays keyword, we must use the same notation.

match $type plays locating:located;
fetch { "type" : $type };
{ "type": { "kind": "entity", "label": "publication" } }
{ "type": { "kind": "entity", "label": "user" } }
{ "type": { "kind": "entity", "label": "address" } }
{ "type": { "kind": "entity", "label": "place" } }
{ "type": { "kind": "entity", "label": "city" } }
{ "type": { "kind": "entity", "label": "state" } }
{ "type": { "kind": "entity", "label": "country" } }

Here we have queried the types that play the located role in locating. As with the sub and owns keywords, we can also invert the intent of plays statements1. In the next query, we do so in order to retrieve the list of roles that book plays.

match book plays $role;
fetch { "role" : $role };
{ "role": { "kind": "relation:role", "label": "contribution:work" } }
{ "role": { "kind": "relation:role", "label": "publishing:published" } }
{ "role": { "kind": "relation:role", "label": "promotion-inclusion:item" } }
{ "role": { "kind": "relation:role", "label": "order-line:item" } }
{ "role": { "kind": "relation:role", "label": "rating:rated" } }
{ "role": { "kind": "relation:role", "label": "recommendation:recommended" } }
Exercise

Write a query to retrieve the list of roles in the delivery relation.

Sample solution
match
delivery relates $role;
fetch {
  "role": $role,
};

Now modify the query to also retrieve the types that play those roles.

Sample solution
match
delivery relates $role;
$type plays $role;
fetch {
  "role": $role,
  "player": $type,
};

Footnotes

  1. ^ The astute reader will notice that we have used this "inversion" technique for sub, owns, and plays statements, but not for relates statements. TypeQL is flexible enough that we could certainly do so if we wanted, but it is not particularly useful to and there are certain practical considerations that make the syntax tricky to use correctly. While learning the basics of TypeQL, it is best to avoid relates statements of this kind.