New ACM paper, free-tier cloud, and open-source license

Lesson 11.2: Operating on objects

Instantiating types

In the previous lesson, we learned how to retrieve data instances and schema types from the database as stateful objects. In this lesson, we’ll see how we can operate on them, starting by programmatically instantiating types. To do so, we can call methods of the type objects: the create method for entity and relation types, and the put method for attribute types. The following code snippet shows an example of each method.

with TypeDB.core_driver(ADDRESS) as driver:
    with driver.session(DATABASE, SessionType.DATA) as session:
        with session.transaction(TransactionType.WRITE) as transaction:
            book_type: EntityType
            contribution_type: RelationType
            page_count_type: AttributeType

            book: Entity = book_type.create(transaction).resolve()
            contribution: Relation = contribution_type.create(transaction).resolve()
            page_count: Attribute = page_count_type.put(transaction, 200).resolve()

            transaction.commit()

All methods that modify data require a transaction to be supplied as an argument. As when executing write queries, this must be a write transaction, and it must be committed in order for the changes to be persisted. The put method of attribute types also takes the attribute’s value as an argument.

In TypeDB, when we create two attributes of the same type and value, they are stored as the same attribute instance. For example, if we create two books with page count attributes that have the same value, they will actually reference the same instance of page count. This allows for very efficient operations on attributes. For this reason, only one attribute can exist in the database with a given type and value, and creation of attributes is idempotent.

Modifying attributes

We can add or remove attributes to an entity or relation using the set_has and unset_has methods, as shown in the following code snippet, which decrements the stock of a supplied book entity. Here we make the assumption that stock is not multivalued for books.

def decrement_stock(transaction: TypeDBTransaction, book: Entity) -> None:
    stock_type = transaction.concepts.get_attribute_type("stock").resolve()
    stock_old = next(book.get_has(transaction, stock_type))

    if stock_old.get_value() == 0:
        raise ValueError("Already out of stock.")
    else:
        stock_new = stock_type.put(transaction, stock_old.get_value() - 1).resolve()
        book.unset_has(transaction, stock_old).resolve()
        book.set_has(transaction, stock_new).resolve()

Both set_has and unset_has take an attribute object as an argument. In this example, we have retrieved the existing stock attribute and supplied it to unset_has, and created a new stock attribute to supply to set_has.

Deleting an attribute with its delete method will remove it from every entity and relation that owns it. To remove it from a single entity or relation, use the unset_has method of the owner instead. See the note above on attribute idempotence for more information.

Exercise

Write a function that creates a new promotion with the supplied attributes and returns it as a stateful object. The function should have the following signature.

create_promotion(
        transaction: TypeDBTransaction,
        code_value: str,
        name_value: str,
        start_timestamp_value: datetime,
        end_timestamp_value: datetime
) -> Entity

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
def create_promotion(
        transaction: TypeDBTransaction,
        code_value: str,
        name_value: str,
        start_timestamp_value: datetime,
        end_timestamp_value: datetime
) -> Entity:
    promotion_type = transaction.concepts.get_entity_type("promotion").resolve()
    code_type = transaction.concepts.get_attribute_type("code").resolve()
    name_type = transaction.concepts.get_attribute_type("name").resolve()
    start_timestamp_type = transaction.concepts.get_attribute_type("start-timestamp").resolve()
    end_timestamp_type = transaction.concepts.get_attribute_type("end-timestamp").resolve()
    promotion = promotion_type.create(transaction).resolve()
    code = code_type.put(transaction, code_value).resolve()
    name = name_type.put(transaction, name_value).resolve()
    start_timestamp = start_timestamp_type.put(transaction, start_timestamp_value).resolve()
    end_timestamp = end_timestamp_type.put(transaction, end_timestamp_value).resolve()
    promotion.set_has(transaction, code).resolve()
    promotion.set_has(transaction, name).resolve()
    promotion.set_has(transaction, start_timestamp).resolve()
    promotion.set_has(transaction, end_timestamp).resolve()
    return promotion

Modifying relations

To create a relation between entities, we must first create the relation by instantiating it from its type, as shown above. The entities can then be added as roleplayers using its add_players method. This is illustrated in the following code snippet, which adds a given book to a given promotion.

def add_to_promotion(transaction: TypeDBTransaction, book: Entity, promotion: Entity, discount_value: float) -> None:
    inclusion_type = transaction.concepts.get_relation_type("promotion-inclusion").resolve()
    item_role = inclusion_type.get_relates(transaction, "item").resolve()
    promotion_role = inclusion_type.get_relates(transaction, "promotion").resolve()
    discount_type = transaction.concepts.get_attribute_type("discount").resolve()
    inclusion = inclusion_type.create(transaction).resolve()
    inclusion.add_player(transaction, item_role, book).resolve()
    inclusion.add_player(transaction, promotion_role, promotion).resolve()
    discount = discount_type.put(transaction, discount_value).resolve()
    inclusion.set_has(transaction, discount)

If a relation does not have any roleplayers when a transaction is committed, that relation will be automatically destroyed.

To remove a relation between entities, we can delete it using its delete method. In the following code snippet, we remove a given book from a given promotion.

def remove_from_promotion(transaction: TypeDBTransaction, book: Entity, promotion: Entity) -> None:
    inclusion_type = transaction.concepts.get_relation_type("promotion-inclusion").resolve()
    item_role = inclusion_type.get_relates(transaction, "item").resolve()
    promotion_role = inclusion_type.get_relates(transaction, "promotion").resolve()
    inclusions = book.get_relations(transaction, item_role)

    for inclusion in inclusions:
        promotion_players = inclusion.get_players_by_role_type(transaction, promotion_role)

        for player in promotion_players:
            if player == promotion:
                inclusion.delete(transaction).resolve()
                break
Exercise

Write a function that creates a new review entity, connected to the supplied user and book entities, and with the supplied score attribute.

create_review(transaction: TypeDBTransaction, user: Entity, book: Entity, score_value: int) -> Entity

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
def create_review(transaction: TypeDBTransaction, user: Entity, book: Entity, score_value: int) -> Entity:
    review_type = transaction.concepts.get_entity_type("review").resolve()
    action_execution_type = transaction.concepts.get_relation_type("action-execution").resolve()
    rating_type = transaction.concepts.get_relation_type("rating").resolve()
    score_type = transaction.concepts.get_attribute_type("score").resolve()
    action_role = action_execution_type.get_relates(transaction, "action").resolve()
    executor_role = action_execution_type.get_relates(transaction, "executor").resolve()
    review_role = rating_type.get_relates(transaction, "review").resolve()
    rated_role = rating_type.get_relates(transaction, "rated").resolve()
    review = review_type.create(transaction).resolve()
    action_execution = action_execution_type.create(transaction).resolve()
    action_execution.add_player(transaction, executor_role, user).resolve()
    action_execution.add_player(transaction, action_role, review).resolve()
    rating = rating_type.create(transaction).resolve()
    rating.add_player(transaction, review_role, review).resolve()
    rating.add_player(transaction, rated_role, book).resolve()
    score = score_type.put(transaction, score_value).resolve()
    review.set_has(transaction, score).resolve()
    return review

Provide Feedback