Officially out now: The TypeDB 3.0 Roadmap

Lesson 6.5: Processing results

The structure of results

In this lesson, we’ll be looking at how we can process the results of queries to produce the data structures we need. We’ll start with the following query.

match
$book isa hardback;
fetch
$book: title, genre, page-count;
ADDRESS = "localhost:1730"
DATABASE = "bookstore"

with TypeDB.core_driver(ADDRESS) as driver:
    with driver.session(DATABASE, SessionType.DATA) as session:
        with session.transaction(TransactionType.READ) as transaction:
            results: Iterator[dict] = transaction.query.fetch("""
                match
                $book isa hardback;
                fetch
                $book: title, genre, page-count;
            """)

            for result in results:
                print(result)

If we run this code snippet, then we get the following output.

{'book': {'genre': [{'type': {'root': 'attribute', 'value_type': 'string', 'label': 'genre'}, 'value': 'comics'}, {'type': {'label': 'genre', 'value_type': 'string', 'root': 'attribute'}, 'value': 'fiction'}], 'type': {'root': 'entity', 'label': 'hardback'}, 'page-count': [{'type': {'value_type': 'long', 'label': 'page-count', 'root': 'attribute'}, 'value': 1451}], 'title': [{'value': 'The Complete Calvin and Hobbes', 'type': {'label': 'title', 'root': 'attribute', 'value_type': 'string'}}]}}
{'book': {'genre': [{'type': {'root': 'attribute', 'label': 'genre', 'value_type': 'string'}, 'value': 'history'}, {'type': {'label': 'genre', 'root': 'attribute', 'value_type': 'string'}, 'value': 'nonfiction'}], 'page-count': [{'type': {'label': 'page-count', 'value_type': 'long', 'root': 'attribute'}, 'value': 296}], 'type': {'root': 'entity', 'label': 'hardback'}, 'title': [{'type': {'value_type': 'string', 'label': 'title', 'root': 'attribute'}, 'value': 'Under the Black Flag: The Romance and the Reality of Life Among the Pirates'}]}}
{'book': {'genre': [{'type': {'value_type': 'string', 'root': 'attribute', 'label': 'genre'}, 'value': 'nonfiction'}, {'type': {'label': 'genre', 'value_type': 'string', 'root': 'attribute'}, 'value': 'technology'}], 'type': {'root': 'entity', 'label': 'hardback'}, 'title': [{'value': 'Electron Backscatter Diffraction in Materials Science', 'type': {'root': 'attribute', 'value_type': 'string', 'label': 'title'}}], 'page-count': [{'type': {'root': 'attribute', 'label': 'page-count', 'value_type': 'long'}, 'value': 425}]}}

Each output line is a result in the format of a dictionary. The keys of the dictionary are names of the variables in the fetch clause that we retrieved the attributes of. In this case, we only fetched the attributes of $book so the only key is book.

for result in results:
    print(result.keys())
dict_keys(['book'])
dict_keys(['book'])
dict_keys(['book'])

For each result, the value of result["book"] is another dictionary. The keys of these nested dictionaries are the names of the attributes that we fetched of book, in addition to a type field as we saw in Lesson 3.2.

for result in results:
    print(result["book"].keys())
dict_keys(['type', 'genre', 'page-count', 'title'])
dict_keys(['type', 'genre', 'page-count', 'title'])
dict_keys(['title', 'genre', 'page-count', 'type'])

The values of result["book"]["title"], result["book"]["genre"], and result["book"]["page-count"] are lists, each containing a dictionary representing a retrieved attribute of $book. These have value fields containing the value of the attribute, in addition to type fields. If we print out each value field, we can clearly see the results of the query.

for result in results:
    for title in result["book"]["title"]:
        print(f"""Title: {title["value"]}""")

    for genre in result["book"]["genre"]:
        print(f"""Genre: {genre["value"]}""")

    for page_count in result["book"]["page-count"]:
        print(f"""Page count: {page_count["value"]}""")

    print()
Title: The Complete Calvin and Hobbes
Genre: comics
Genre: fiction
Page count: 1451

Title: Under the Black Flag: The Romance and the Reality of Life Among the Pirates
Genre: history
Genre: nonfiction
Page count: 296

Title: Electron Backscatter Diffraction in Materials Science
Genre: nonfiction
Genre: technology
Page count: 425

We can see that each book in the result set has one title and page count (as would be expected), in addition to two genres.

Exercise

Write a function that prints the title and ISBNs of every hardback. For each ISBN printed, make sure to specify if it is an ISBN-13 or an ISBN-10. The function should have the following signature.

print_hardback_isbns(transaction: TypeDBTransaction) -> None
Hint

The type of ISBN can be accessed via the label field of the type field of the returned dictionary representing the ISBN, for example:

for result in results:
    for isbn in result["book"]["isbn"]:
        print(isbn["type"]["label"])
Sample solution
def print_hardback_isbns(transaction: TypeDBTransaction) -> None:
    results = transaction.query.fetch("""
        match
        $book isa hardback;
        fetch
        $book: title, isbn;
    """)

    for result in results:
        for title in result["book"]["title"]:
            print(f"""Title: {title["value"]}""")

        for isbn in result["book"]["isbn"]:
            print(f"""{isbn["type"]["label"].upper()}: {isbn["value"]}""")

        print()

Yielding results

By accessing the result dictionaries in this way, we can structure the results of a query as needed for further processing. The following code snippet shows a function that retrieves the IDs of orders that include a book, specified by ISBN, and the quantity ordered. The order ID and quantity are yielded as a tuple that we can further operate on elsewhere in our application.

def get_orders_of_book(transaction: TypeDBTransaction, isbn: str) -> Iterator[tuple[str, int]]:
    results = transaction.query.fetch(f"""
        match
        $book isa book, has isbn "{isbn}";
        $line (order: $order, item: $book) isa order-line;
        fetch
        $order: id;
        $line: quantity;
    """)

    for result in results:
        order_id = result["order"]["id"][0]["value"]
        quantity = result["line"]["quantity"][0]["value"]

        yield order_id, quantity

We make sure to return an iterator by using the yield keyword rather than the return keyword. This will allow us to begin processing individual results as soon as they are each returned by the server rather than having to wait for the last one to be found. The following code snippet shows how we might call this function, and we can see it prints the results in the desired format.

with TypeDB.core_driver(ADDRESS) as driver:
    with driver.session(DATABASE, SessionType.DATA) as session:
        with session.transaction(TransactionType.READ) as transaction:
            orders = get_orders_of_book(transaction, "9780446310789")

            for order in orders:
                print(order)
('o0016', 1)
('o0032', 1)
('o0036', 2)
Exercise

Write a function that yields the ISBN-13s and titles of books in a specified genre as tuples. You can assume that each book has exactly one ISBN-13 and one title. The function should have the following signature.

get_books_in_genre(transaction: TypeDBTransaction, genre: str) -> Iterator[tuple[str, str]]

You should ensure the following test case produces the result given for the sample database. Additional results may be produced if you have inserted data.

with TypeDB.core_driver(ADDRESS) as driver:
    with driver.session(DATABASE, SessionType.DATA) as session:
        with session.transaction(TransactionType.READ) as transaction:
            scifis = get_books_in_genre(transaction, "science fiction")

            for book in scifis:
                print(book)
('9780575104419', 'Dune')
('9780671461492', "The Hitchhiker's Guide to the Galaxy")
Sample solution
def get_books_in_genre(transaction: TypeDBTransaction, genre: str) -> Iterator[tuple[str, str]]:
    results = transaction.query.fetch(f"""
        match
        $book isa book, has genre "{genre}";
        fetch
        $book: isbn-13, title;
    """)

    for result in results:
        isbn_13 = result["book"]["isbn-13"][0]["value"]
        title = result["book"]["title"][0]["value"]

        yield isbn_13, title
Exercise

Write a sample app that allows an employee of the bookstore to query books via a CLI. The app should allow employees to look up books by specific ISBNs, and to search for books based on one or more of the following fields in combination:

  • Format.

  • Genres.

  • Contributor names.

  • Publisher name.

  • Publication year.

Whether looked up directly by ISBN or as part of a search, the following details should be displayed for each book returned:

  • All attributes of the book.

  • The names of any contributors to the book.

  • The name of the book’s publisher.

  • The book’s year of publication.

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
from enum import Enum
from typing import Iterator, Optional, Any
from typedb.api.connection.session import TypeDBSession
from typedb.api.connection.transaction import TypeDBTransaction
from typedb.driver import TypeDB, SessionType, TransactionType

ADDRESS = "localhost:1730"
DATABASE = "bookstore"


class BookFormat(Enum):
    PAPERBACK = "paperback"
    HARDBACK = "hardback"
    EBOOK = "ebook"


class BookNotFoundException(Exception):
    pass


def filter_books(
    transaction: TypeDBTransaction,
    format: Optional[BookFormat],
    genres: Optional[list[str]],
    contributor_names: Optional[list[str]],
    publisher_name: Optional[str],
    publication_year: Optional[int],
) -> Iterator[str]:
    if format is None:
        query = f"""match $book isa book;"""
    else:
        query = f"""match $book isa {format.value};"""

    if genres is not None:
        for genre in genres:
            query += f""" $book has genre "{genre}";"""

    if contributor_names is not None:
        for i, name in enumerate(contributor_names):
            query += f""" $contributor-{i} isa contributor, has name "{name}"; ($book, $contributor-{i}) isa contribution;"""

    if publisher_name is not None:
        query += f""" $publisher isa publisher, has name "{publisher_name}"; ($book, $publisher) isa publishing;"""

    if publication_year is not None:
        query += f""" $publication isa publication, has year {publication_year}; ($book, $publication) isa publishing;"""

    query += f""" fetch $book: isbn-13;"""

    for result in transaction.query.fetch(query):
        yield result["book"]["isbn-13"][0]["value"]


def get_book_details(transaction: TypeDBTransaction, isbn: str) -> list[tuple[str, Any]]:
    book_query = f"""
        match
        $book isa! $book-type, has isbn "{isbn}";
        fetch
        $book-type;
        $book: attribute;
    """

    try:
        result = next(transaction.query.fetch(book_query))
    except StopIteration:
        raise BookNotFoundException()

    details = list()

    details.append(("format", result["book-type"]["label"]))

    for attribute in result["book"]["attribute"]:
        details.append((attribute["type"]["label"], attribute["value"]))

    contributing_query = f"""
        match
        $book isa book, has isbn "{isbn}";
        $contributor isa contributor;
        ($book, $contributor-role: $contributor) isa! $contribution-type;
        $contribution-type relates $contributor-role;
        fetch
        $contributor: name;
        $contributor-role;
    """

    for result in transaction.query.fetch(contributing_query):
        contributor_name = result["contributor"]["name"][0]["value"]
        contributor_role = result["contributor-role"]["label"].split(":")[1]
        details.append((contributor_role, contributor_name))

    publishing_query = f"""
        match
        $book isa book, has isbn "{isbn}";
        $publisher isa publisher;
        $publication isa publication;
        ($book, $publisher, $publication) isa publishing;
        fetch
        $publisher: name;
        $publication: year;
    """

    result = next(transaction.query.fetch(publishing_query))
    publisher_name = result["publisher"]["name"][0]["value"]
    details.append(("publisher", publisher_name))
    publication_year = result["publication"]["year"][0]["value"]
    details.append(("year", publication_year))

    return details


def format_book_details(details: list[tuple[str, Any]]) -> str:
    output = ""

    for detail in details:
        field_name = detail[0].replace("-", " ").title().replace("Isbn ", "ISBN-")
        field_value = detail[1]
        output += f"""\n{field_name}: {field_value}"""

    return output


def retrieve_book(session: TypeDBSession) -> None:
    isbn = input("""Enter ISBN-13 or ISBN-10: """).strip()

    with session.transaction(TransactionType.READ) as transaction:
        try:
            details = get_book_details(transaction, isbn)
            print(format_book_details(details))
        except BookNotFoundException:
            print(f"""No book found with ISBN: "{isbn}" """)


def search_books(session: TypeDBSession) -> None:
    print("""Available filters:""")
    print(""" - Format.""")
    print(""" - Genres.""")
    print(""" - Contributor names.""")
    print(""" - Publisher name.""")
    print(""" - Publication year.""")

    if input("""Filter on format? (Y/N): """).strip().upper() == "Y":
        try:
            format_ = BookFormat(input("""Enter format: """).strip())
        except ValueError:
            print("""Not a valid book format.""")
            return
    else:
        format_ = None

    if input("""Filter on genres? (Y/N): """).strip().upper() == "Y":
        genres = list()

        while True:
            genres.append(input("""Enter genre: """).strip())

            if input("""Filter on another genre? (Y/N): """).strip().upper() != "Y":
                break
    else:
        genres = None

    if input("""Filter on contributors? (Y/N): """).strip().upper() == "Y":
        contributor_names = list()

        while True:
            contributor_names.append(input("""Enter contributor name: """).strip())

            if input("""Filter on another contributor? (Y/N): """).strip().upper() != "Y":
                break
    else:
        contributor_names = None

    if input("""Filter on publisher? (Y/N): """).strip().upper() == "Y":
        publisher_name = input("""Enter publisher name: """)
    else:
        publisher_name = None

    if input("""Filter on publication year? (Y/N): """).strip().upper() == "Y":
        publication_year = input("""Enter year: """)
    else:
        publication_year = None

    with session.transaction(TransactionType.READ) as transaction:
        isbns = filter_books(transaction, format_, genres, contributor_names, publisher_name, publication_year)
        book_found = False

        for isbn in isbns:
            try:
                details = get_book_details(transaction, isbn)
                print(format_book_details(details))
                book_found = True
            except BookNotFoundException:
                pass

        if not book_found:
            print("""\nNo results found.""")


if __name__ == "__main__":
    with TypeDB.core_driver(ADDRESS) as driver:
        with driver.session(DATABASE, SessionType.DATA) as session:
            while True:
                print("""Available commands:""")
                print(""" - R: Retrieve book details by ISBN.""")
                print(""" - S: Search for books.""")
                print(""" - Q: Quit.""")

                command = input("""Enter command: """).strip().upper()

                if command == "R":
                    retrieve_book(session)
                elif command == "S":
                    search_books(session)
                elif command == "Q":
                    break
                else:
                    print(f"""Unrecognised command: "{command}" """)

                print()