Officially out now: The TypeDB 3.0 Roadmap

Lesson 3.2: Fetching polymorphic data

Type inference

In the previous lesson, we retrieved the titles and page counts of paperback books with the following query.

match
$book isa paperback;
fetch
$book: title, page-count;

But what if we wanted these attributes for all books, regardless of format? In a SQL query, we would need to use a union of tables.

SELECT title, page_count
FROM paperback
UNION
SELECT title, page_count
FROM hardback
UNION
SELECT title, page_count
FROM ebook;

This approach has several disadvantages. Not only is the query long and verbose, but if we add a new book format to our database, then this query (and all others like it) would need to be updated. With TypeDB, we can leverage declarative polymorphism in our queries to retrieve data of multiple types without enumerating them. In the following query, we adjust the previous query to retrieve the titles and page counts of all books.

match
$book isa book;
fetch
$book: title, page-count;

studio run Run

We have just changed the type of $book from paperback to book. Now if we run this query, the results include paperbacks, hardbacks, and ebooks!

{
    "book": {
        "page-count": [ { "value": 310, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "The Hobbit", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "ebook", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 79, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "The Iron Giant", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "ebook", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 450, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Fundamentals of Data Engineering", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "ebook", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 656, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "The Odyssey", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "ebook", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 624, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Dune", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "ebook", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 196, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Physical Principles of Electron Microscopy: An Introduction to TEM, SEM, and AEM", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "ebook", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 296, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Under the Black Flag: The Romance and the Reality of Life Among the Pirates", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "hardback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 425, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Electron Backscatter Diffraction in Materials Science", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "hardback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 1451, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "The Complete Calvin and Hobbes", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "hardback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 352, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Great Discoveries in Medicine", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 416, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Hokusai's Fuji", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 199, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Interpretation of Electron Diffraction Patterns", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 295, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Pride and Prejudice", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 281, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "To Kill a Mockingbird", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 260, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Business Secrets of The Pharoahs", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 240, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "The Mummies of Urumchi", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 820, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "Classical Mythology", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 458, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "One Hundred Years of Solitude", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 215, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "The Hitchhiker's Guide to the Galaxy", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "page-count": [ { "value": 160, "value_type": "long", "type": { "label": "page-count", "root": "attribute" } } ],
        "title": [ { "value": "The Motorcycle Diaries: A Journey Around South America", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}

This is because TypeDB applies type inference to queries in order to automatically resolve the possible return types of variables in a query based on the type definitions in the schema. This means that, if we add new types of books to the schema, the query will also return them without needing to be updated!

To determine the actual return types of query results, we can look at the type fields in the JSON output. For the above query, each result includes such a field for the book, plus an additional one for each of the book’s attributes (title and page count). All type fields have two subfields: a label field indicating the name of the type, and a root field indicating the type’s root type: entity, relation, or attribute.

Polymorphism in TypeDB

To begin leveraging declarative polymorphism in our queries, we’ll first need to review the three fundamental types of polymorphism that TypeDB implements.

Inheritance polymorphism

Allows us to define type hierarchies in the schema and then query those hierarchies declaratively.

Interface polymorphism

Allows us to define interfaces that can be implemented independently in the schema and then query those interfaces declaratively.

Parametric polymorphism

Allows us to write declarative queries that are completely independent of the schema.

In this lesson, we’ll be focusing on how we can use each type of polymorphism in our queries. We’ll see in Lesson 5 how we can define type hierarchies and interfaces in the schema.

Inheritance polymorphism

In the last query, we used inheritance polymorphism to retrieve the titles and page counts of all types of books. The results contained instances of paperback, hardback, and ebook for the $book variable. This is because all three of these types are subtypes of book, which was the type we specified for $book. In fact, it is not possible to return a direct instance of book because it is an abstract type. We will explore abstract types further in Lesson 5.

Entity, relation, and attribute types can all be defined in type hierarchies, which allows us to retrieve their instances together by querying their supertypes, as we did with the entity type book. In the following query, we retrieve the ISBNs of all paperbacks using the isbn attribute type.

match
$book isa paperback;
fetch
$book: isbn;

studio run Run

{
    "book": {
        "isbn": [
            { "value": "9780500291221", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0500291225", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780500026557", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0500026556", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9781489962287", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "148996228X", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780553212150", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "055321215X", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780446310789", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0446310786", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [ { "value": "9798691153570", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780393045215", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0393045218", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780195153446", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0195153448", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780060929794", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0060929790", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780671461492", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0671461494", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9781859840665", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "1859840663", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "type": { "label": "paperback", "root": "entity" }
    }
}

As we can see from the results, we have returned instances of both isbn-13 and isbn-10, the two subtypes of isbn. Once again, the supertype is abstract in this case and so cannot be directly returned.

Exercise

Write a query to retrieve all ISBNs of all books in the database.

Sample solution
match
$book isa book;
fetch
$book: isbn;

studio run Run

We can also make use of inheritance polymorphism when constraining the values of attributes. In the next query, we retrieve the title and remaining stock of a book by ISBN, but we do not specify the type of ISBN we are providing.

match
$book isa book, has isbn "0500026556";
fetch
$book: title, stock;

studio run Run

{
    "book": {
        "stock": [ { "value": 11, "value_type": "long", "type": { "label": "stock", "root": "attribute" } } ],
        "title": [ { "value": "Hokusai's Fuji", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}

Interface polymorphism

When querying with inheritance polymorphism, we constrain the types of data instances by the supertypes of those types. In contrast, when querying with interface polymorphism, we specify the types of data instances by the interfaces that those types implement. There are two kinds of interfaces between types in TypeDB: ownerships of attributes and roles in relations.

We have already seen an example of interface polymorphism in Lesson 3.1.

match
$book isa paperback, has isbn-13 "9780446310789";
$line (order: $order, item: $book) isa order-line;
fetch
$order: id;
$line: quantity;

studio run Run

In this query, we specify the type of $book to be paperback and the type of $line to be order-line using the isa keyword, but nowhere in the query do we explicitly specify the type of $order! Instead, we constrain its type according to two facts:

  • The type of $order must play the role of order in order-line.

  • The type of $order must own the attribute id.

This is more than enough information for TypeDB to infer the type of $order, which we can see by running this query.

{
    "line": {
        "quantity": [ { "value": 1, "value_type": "long", "type": { "label": "quantity", "root": "attribute" } } ],
        "type": { "label": "order-line", "root": "relation" }
    },
    "order": {
        "id": [ { "value": "o0016", "value_type": "string", "type": { "label": "id", "root": "attribute" } } ],
        "type": { "label": "order", "root": "entity" }
    }
}
{
    "line": {
        "quantity": [ { "value": 1, "value_type": "long", "type": { "label": "quantity", "root": "attribute" } } ],
        "type": { "label": "order-line", "root": "relation" }
    },
    "order": {
        "id": [ { "value": "o0032", "value_type": "string", "type": { "label": "id", "root": "attribute" } } ],
        "type": { "label": "order", "root": "entity" }
    }
}
{
    "line": {
        "quantity": [ { "value": 2, "value_type": "long", "type": { "label": "quantity", "root": "attribute" } } ],
        "type": { "label": "order-line", "root": "relation" }
    },
    "order": {
        "id": [ { "value": "o0036", "value_type": "string", "type": { "label": "id", "root": "attribute" } } ],
        "type": { "label": "order", "root": "entity" }
    }
}

In this case, only a single return type is possible: order, as it is the only type in the schema that fulfils the above constraints. But if multiple types can fulfil the interface constraints, then they will all be valid return types for the variable in the query. Consider instead the following query.

match
$kansas-city isa city, has name "Kansas City";
(location: $kansas-city, located: $x) isa locating;
fetch
$x: attribute;

studio run Run

In this case, $x can be resolved to any type that both plays located in locating. Essentially, this query will return the attributes of anything located in Kansas City regardless of what it is. If we run this query, we see that we return instances of publication and address!

{
    "x": {
        "attribute": [ { "value": 2005, "value_type": "long", "type": { "label": "year", "root": "attribute" } } ],
        "type": { "label": "publication", "root": "entity" }
    }
}
{
    "x": {
        "attribute": [ { "value": "826 Vermont Avenue", "value_type": "string", "type": { "label": "street", "root": "attribute" } } ],
        "type": { "label": "address", "root": "entity" }
    }
}
Exercise

Write a query to retrieve all attributes of any system actions taken by the user with name "Kevin Morrison". The action-execution relation type is a binary relation type with roles executor and action, used to represent the references between users and the actions they have taken, such as login events or orders placed.

Hint

The following statement can be used to represent action-execution relations.

(executor: $user, action: $action) isa action-execution;
Sample solution
match
$user isa user, has name "Kevin Morrison";
(executor: $user, action: $action) isa action-execution;
fetch
$action: attribute;

studio run Run

Based on the results returned by running this query, what types of system actions exist in the schema?

Answer

The types login, order, and review, as they play the role of action in action-execution.

Parametric polymorphism

Parametric queries are unique in that they are valid over any schema. They match particular data by structure rather than by semantics. Parametric queries do not represent questions in the business domain, and are typically used to perform administrative procedures or database analytics. Let’s see some examples.

match
$entity isa entity;
fetch
$entity: attribute;

studio run Run

This query will retrieve all attributes of all entities. Meanwhile, the following query retrieves the attributes of any two entities that are roleplayers in the same relation.

match
$entity-1 isa entity;
$entity-2 isa entity;
($entity-1, $entity-2) isa relation;
fetch
$entity-1: attribute;
$entity-2: attribute;

studio run Run

In this query we have omitted the roles that the two entities play! In general, we can omit roles in a relation tuple if we do not care what they are. This is particularly useful for parametric queries, and we will explore this feature more fully in Lesson 7.2.

This particular query would return every pair of entities in a binary relation twice: each of the two entities would be returned in one result as $entity-1 and in another as $entity-2. In fact, it would also return pairs of entities in ternary or higher order relations, returning each possible pair of roleplayers twice. This would result in a large number of redundant results, and we will see how we can return the results of queries like these in more useful structures in Lesson 8.2.

While these queries are purely parametric, it is also possible to write non-parametric queries that include parametric statements, as we will see shortly. The defining feature of parametric statements is that they do not include any type names, except for the root type keywords entity, relation, and attribute, which allows them to be run against any schema. Much like using generics in application code, using parametric statements in queries can be a challenging aspect of TypeQL, but allows for extremely powerful queries that cannot be expressed in non-polymorphic query languages. We will see several more examples in future lessons.

Exercise

Write a query to retrieve the attributes of any three entities in a ternary (or higher order) relation.

Sample solution
match
$entity-1 isa entity;
$entity-2 isa entity;
$entity-3 isa entity;
($entity-1, $entity-2, $entity-3) isa relation;
fetch
$entity-1: attribute;
$entity-2: attribute;
$entity-3: attribute;

studio run Run

Combining types of polymorphism

Many useful polymorphic queries combine more than one form of polymorphism. For example, the next query involves all three types of polymorphism.

match
$user isa user, has id "u0008";
$book isa book;
(executor: $user, action: $action) isa action-execution;
($book, $action) isa relation;
fetch
$book: isbn, title;

studio run Run

{
    "book": {
        "isbn": [
            { "value": "9780008627843", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0008627843", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "title": [ { "value": "The Hobbit", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "ebook", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780060929794", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0060929790", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "title": [ { "value": "One Hundred Years of Solitude", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780387881355", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0387881352", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "title": [ { "value": "Electron Backscatter Diffraction in Materials Science", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "hardback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780195153446", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0195153448", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "title": [ { "value": "Classical Mythology", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}
{
    "book": {
        "isbn": [
            { "value": "9780500026557", "value_type": "string", "type": { "label": "isbn-13", "root": "attribute" } },
            { "value": "0500026556", "value_type": "string", "type": { "label": "isbn-10", "root": "attribute" } }
        ],
        "title": [ { "value": "Hokusai's Fuji", "value_type": "string", "type": { "label": "title", "root": "attribute" } } ],
        "type": { "label": "paperback", "root": "entity" }
    }
}

This query retrieves the ISBNs and title of any book that this particular user with ID "u0008" has interacted with via any kind of system action they performed, and is very useful for building a customer profile. If we extend our schema to introduce new ways that users can interact with books, then this query will return books interacted with in those ways too, without having to modify it! This is an example of a way in which we can declaratively encode high-level business questions as straightforward queries, and avoid having to modify queries when the data model is extended.

Exercise

Examine the query above line-by-line and identify how it utilises each type of polymorphism.

Answer
  • The variable $book can resolve to any subtype of book via inheritance polymorphism.

  • The variable $action can resolve to any player of the action role of action-execution via interface polymorphism.

  • The relation between $book and $action can resolve to any relation type via parametric polymorphism.

  • The returned ISBNs can be any subtypes of isbn via inheritance polymorphism.

In the remainder of this course, we will be applying polymorphism liberally to our queries. After all, TypeDB is the polymorphic database!