Officially out now: The TypeDB 3.0 Roadmap

Lesson 12.4: Using interface contracts

Overriding interface implementations

In Lesson 5.5, we learned that if an attribute type is abstract, then any types that own it must also be abstract. In the current schema, book owns isbn-13 and isbn-10, both subtypes of isbn.

define
isbn sub attribute, abstract, value string;
isbn-13 sub isbn;
isbn-10 sub isbn;
book sub relation,
    abstract,
    relates publisher,
    owns isbn-13 @key,
    owns isbn-10 @unique;
paperback sub book;
hardback sub book;
ebook sub book;

Its subtypes paperback, hardback, and ebook also own isbn-13 and isbn-10 by inheritance. If we make book own isbn, we will violate schema validation rules, even though they are both abstract types.

define
isbn sub attribute, abstract, value string;
isbn-13 sub isbn;
isbn-10 sub isbn;
book sub relation,
    abstract,
    relates publisher,
    owns isbn,
    owns isbn-13 @key,
    owns isbn-10 @unique;
paperback sub book;
hardback sub book;
ebook sub book;

This is because paperback, hardback, and ebook are concrete, and they inherit ownership of the abstract isbn. We can get around this by overriding the interface implementation using the as keyword. In Lesson 9.6, we used the as keyword in relates statements to override interface definitions. This ensured that the role interfaces exposed by the relation supertype would not be exposed by its subtypes. Similarly, we can use it in owns and plays statements to ensure that the interfaces implemented by an object supertype will not be implemented by its subtypes.

define
isbn sub attribute, abstract, value string;
isbn-13 sub isbn;
isbn-10 sub isbn;
book sub relation,
    abstract,
    relates publisher,
    owns isbn;
paperback sub book,
    owns isbn-13 as isbn @key,
    owns isbn-10 as isbn @unique;
hardback sub book,
    owns isbn-13 as isbn @key,
    owns isbn-10 as isbn @unique;
ebook sub book,
    owns isbn-13 as isbn @key,
    owns isbn-10 as isbn @unique;

Now the implementation of isbn:OWNER is overridden by isbn-13:OWNER and isbn-10:OWNER, and the schema will pass validation. Note that we have overridden the implementation twice! When overriding an interface definition or implementation, we can always override it multiple times.

In order to override an interface implementation using the as keyword in an owns or plays statement, the overriding interface type must be a subtype of the overridden interface type.

This modification to the schema isn’t particularly useful, and we’ll explore a more practical use in the remainder of this lesson.

Using ownership contracts

By making use of schema validation rules and interface implementation overrides, we can use design by contract as a schema design strategy. Consider how we might extend the bookstore model so that we can sell serial publications like newspapers, magazines, and journals. Every product sold should have a unique identification number, implemented as a key attribute. Unlike books which have ISBNs, serial publications have ISSNs instead. We could implement this in the following manner, without using ownership overrides.

define
isn sub attribute, abstract, value string;
isbn sub isn, abstract;
isbn-13 sub isbn;
isbn-10 sub isbn;
issn sub isn;
publication sub relation,
    abstract,
    relates publisher;
book sub publication,
    abstract,
    owns isbn-13 @key,
    owns isbn-10 @unique;
paperback sub book;
hardback sub book;
ebook sub book;
serial sub publication,
    abstract,
    owns issn @key;
newspaper sub serial;
magazine sub serial;
journal sub serial;

This allows us to polymorphically describe all publications and their "international standard numbers" (ISNs) with the following pattern.

$publication isa publication, has isn $isn;

However, if we extend the schema by defining a new subtype of publication, we could do so without assigning it ownership of an ISN type.

define
music-score sub publication;

Now we will have publications in the database that do not have ISNs, and so are not matched by the above pattern. We can solve this by using an ownership contract. To do so, we assign publication ownership of isn. This will force all subtypes of publication to override the ownership with a subtype of isn, or the schema will not commit.

define
isn sub attribute, abstract, value string;
isbn sub isn, abstract;
isbn-13 sub isbn;
isbn-10 sub isbn;
issn sub isn;
publication sub relation,
    abstract,
    relates publisher,
    owns isn @key;
book sub publication,
    abstract,
    owns isbn-13 as isn,
    owns isbn-10 @unique;
paperback sub book;
hardback sub book;
ebook sub book;
serial sub publication,
    abstract,
    owns issn as isn;
newspaper sub serial;
magazine sub serial;
journal sub serial;

We have also made sure to apply the @key annotation to the top-level ownership implementation. This causes it to be inherited and shared, ensuring that every publication has exactly one ISN, and no two ISNs are the same, even if they are of different types. Now if we were to define music-score as we did above, the commit would fail.

Invalid Type Write: The type 'music-score' is not abstract, and thus cannot own an abstract attribute type 'isn'.

In order to define new subtypes of publication, we are now forced to give them ownership of an ISN type, ensuring the contract defined on publication is fulfilled. For music scores, we use ISMNs.

define
ismn sub isn;
music-score sub publication,
    owns ismn as isn;

TypeDB 2.x does not support the use of schema validation to enforce role contracts in the same way as for ownership contracts. This feature will be released in TypeDB 3.0. To learn more about this and other powerful new features, see the TypeDB 3.0 roadmap.

Below is a complete implementation of the bookstore schema using the type-theoretic framework of the PERA model, and including the new publication types serial and music-score and the isn interface contract.

define

publication sub relation,
    abstract,
    relates publisher,
    relates location,
    owns isn @key,
    owns title,
    owns genre,
    owns price,
    plays contribution:work,
    plays promotion-inclusion:item,
    plays order-line:item,
    plays review:reviewed,
    plays recommendation:recommended;

book sub publication,
    abstract,
    owns isbn-13 as isn,
    owns isbn-10 @unique,
    owns year,
    owns page-count;

hardback sub book,
    owns stock;

paperback sub book,
    owns stock;

ebook sub book;

serial sub publication,
    abstract,
    owns issn as isn,
    owns next-issue-number,
    owns next-issue-date;

newspaper sub serial;

magazine sub serial;

journal sub serial;

music-score sub publication,
    owns ismn as isn,
    owns year,
    owns page-count,
    owns stock;

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 publication:publisher;

courier sub company,
    plays delivery:deliverer;

user sub relation,
    relates location,
    owns id @key,
    owns name,
    owns birth-date,
    plays user-action:user,
    plays recommendation:recipient;

user-action sub relation,
    abstract,
    relates user,
    owns timestamp;

order sub user-action,
    owns id @key,
    owns status,
    plays order-line:order,
    plays delivery:delivered;

review sub user-action,
    relates reviewed,
    owns id @key,
    owns score,
    owns verified;

login sub user-action,
    owns success;

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

address sub relation,
    relates location,
    owns street,
    plays delivery:destination;

place sub relation,
    abstract,
    owns name;

city sub place,
    relates location,
    plays publication:location,
    plays user:location,
    plays address:location;

state sub place,
    relates location,
    plays city:location;

country sub place,
    relates location,
    plays city:location,
    plays state:location;

world sub entity,
    plays country:location;

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;

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

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

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

recommendation sub relation,
    relates recommended,
    relates recipient;

isn sub attribute, abstract, value string;
isbn sub isn, abstract;
isbn-13 sub isbn;
isbn-10 sub isbn;
issn sub isn;
ismn sub isn;
title sub attribute, value string;
page-count sub attribute, value long;
next-issue-number sub attribute, value long;
next-issue-date sub attribute, value datetime;
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 ($user, $product) isa review, has timestamp $review-time;
        $order ($user) isa order, has timestamp $order-time;
        ($order, $product) isa order-line;
        $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 publication-recommendation-by-genre:
    when {
        $user isa user;
        $liked-publication isa publication;
        {
            $order ($user) isa order;
            ($order, $liked-publication) isa order-line;
        } or {
            ($user, $liked-publication) isa review,
                has score >= 7;
        };
        $new-publication isa publication;
        not { {
            $order ($user) isa order;
            ($order, $new-publication) isa order-line;
        } or {
            ($user, $new-publication) isa review;
        }; };
        $liked-publication has genre $shared-genre;
        $new-publication has genre $shared-genre;
        not { {
            $shared-genre == "fiction";
        } or {
            $shared-genre == "nonfiction";
        }; };
    } then {
        (recommended: $new-publication, recipient: $user) isa recommendation;
    };

rule publication-recommendation-by-contributor:
    when {
        $user isa user;
        $liked-publication isa publication;
        {
            $order ($user) isa order;
            ($order, $liked-publication) isa order-line;
        } or {
            ($user, $liked-publication) isa review,
                has score >= 7;
        };
        $new-publication isa publication;
        not { {
            $order ($user) isa order;
            ($order, $new-publication) isa order-line;
        } or {
            ($user, $new-publication) isa review;
        }; };
        ($liked-publication, $shared-contributor) isa contribution;
        ($new-publication, $shared-contributor) isa contribution;
    } then {
        (recommended: $new-publication, recipient: $user) isa recommendation;
    };

rule order-line-total-retail-price:
    when {
        $order 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 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;
    };