Queries

Finding Objects with dbzero.find()

The dbzero.find function is your primary tool for retrieving objects from dbzero. It's designed to be efficient and offers powerful query capabilities with the use of logical operators. It returns a query object, which is a lazily-evaluated iterable. This means you can query for huge result sets without exhausting operating memory—the results are fetched as you iterate over them.

Basic Queries

You can perform simple lookups by providing a tag, a class type, or both.

Find by Tag

The most straightforward query is finding all objects associated with a specific tag.

import dbzero as db0
 
@db0.memo
class Task:
    def __init__(self, description):
        self.description = description
 
# Create a task and tag it as urgent
task = Task("Fix production bug")
db0.tags(task).add("urgent")
 
# Find returns an iterable of all objects with "urgent"
results = db0.find("urgent")
assert len(results) == 1
assert list(results)[0] is task
 
# Querying for a non-existent tag returns an empty result set
assert len(db0.find("non-existent-tag")) == 0

Find by Type

You can also retrieve all instances by their class.

@db0.memo
class Book:
    def __init__(self, title):
        self.title = title
 
@db0.memo
class Author:
    def __init__(self, name):
        self.name = name
 
# Create several instances of different classes
book1 = Book("Dune")
book2 = Book("Foundation")
author = Author("Isaac Asimov")
 
# Find all instances of a specific type
all_books = db0.find(Book)
assert set(all_books) == {book1, book2}

Combine Type and Tag

Most commonly you'll use the combination of class and tags. This narrows down the result set to only include objects of the type you expect to find.

# Create and tag objects of different types
book = Book("The Hitchhiker's Guide")
author = Author("Douglas Adams")
db0.tags(book).add("sci-fi")
db0.tags(author).add("sci-fi")
 
# Find only books with the "sci-fi" tag
sci_fi_books = db0.find(Book, "sci-fi")
assert len(sci_fi_books) == 1
assert list(sci_fi_books)[0].title == "The Hitchhiker's Guide"

Advanced Tag Logic: AND, OR, NOT

dbzero.find() supports boolean logic for complex tag queries. The data structure you use to pass the tags determines how they are combined.

  • AND: To find objects that have all specified tags, pass the tags as a tuple ("A", "B") or as separate arguments find("A", "B").
  • OR: To find objects that have any of the specified tags, pass them in a list ["A", "B"].
  • NOT: To exclude objects with a certain tag, wrap the tag with the dbzero.no() operator.
@db0.memo
class Article:
    def __init__(self, title):
        self.title = title
 
# Create articles with different tag combinations
article1 = Article("Python Best Practices")
db0.tags(article1).add(["python", "tutorial"])
 
article2 = Article("Advanced Python Techniques")
db0.tags(article2).add(["python", "advanced"])
 
article3 = Article("JavaScript Fundamentals")
db0.tags(article3).add(["javascript", "tutorial"])
 
# AND search: Find articles that are both Python AND advanced
# Pass tags as separate arguments or in a tuple
advanced_python = db0.find(Article, ("python", "advanced"))
assert set(advanced_python) == {article2}
 
# OR search: Find articles that are either JavaScript OR Python
# Pass tags in a list
programming_articles = db0.find(Article, ["javascript", "python"])
assert set(programming_articles) == {article1, article2, article3}
 
# NOT search: Find tutorials that are NOT about Python
# Use db0.no() to exclude a tag
non_python_tutorials = db0.find(Article, "tutorial", db0.no("python"))
assert set(non_python_tutorials) == {article3}

Recap: Tag Logic

  • find("tagA", "tagB") → objects with tagA AND tagB.
  • find(("tagA", "tagB")) → objects with tagA AND tagB.
  • find(["tagA", "tagB"]) → objects with tagA OR tagB.
  • find("tagA", dbzero.no("tagB")) → objects with tagA AND NOT tagB.

Querying by Relationships

Tags and queries aren't limited to just strings and types; you can use memo objects to create and resolve relations. The query engine is also aware of inheritance and object hierarchy, allowing for broad search over categories of objects.

Find by Object Instance

You can use an object instance as a tag in a query. This is useful for resolving relations between objects.

@db0.memo
class Author:
    def __init__(self, name):
        self.name = name
 
@db0.memo
class Book:
    def __init__(self, title, author):
        self.title = title
 
# Create an author and their books
author = Author("Isaac Asimov")
book1 = Book("Foundation", author)
book2 = Book("I, Robot", author)
# Tag these book with their author to create a relationship
db0.tags(book1, book2).add(db0.as_tag(author))
 
# Find all books by this author
books_by_author = db0.find(Book, db0.as_tag(author))
assert set(books_by_author) == {book1, book2}

Inheritance-Aware Search

When you search by a base class, the results will include instances of that class and all of its subclasses.

@db0.memo
class Document:
    def __init__(self, title):
        self.title = title
 
@db0.memo
class Report(Document):
    def __init__(self, title, quarter):
        super().__init__(title)
        self.quarter = quarter
 
@db0.memo
class Presentation(Document):
    def __init__(self, title, slides):
        super().__init__(title)
        self.slides = slides
 
# Create documents of different types
report = Report("Q4 Financial Report", "Q4")
presentation = Presentation("Product Launch", 25)
db0.tags(report, presentation).add("important")
 
# Find by the base class - returns all subclass instances
all_important_docs = db0.find(Document, "important")
assert set(all_important_docs) == {report, presentation}

Combining and Refining Queries

dbzero provides tools to chain and refine queries, allowing you to build complex logic by composing simpler parts.

Subqueries

You can pass a query as an argument of another dbzero.find() query. This gives a lot of flexibility, as it also works with logical operators:

@db0.memo
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary
 
# Create employees with different attributes
employees = [
    Employee("Alice", 50000), Employee("Bob", 60000), Employee("Carol", 70000),
    Employee("David", 80000), Employee("Eve", 90000), Employee("Frank", 100000),
    Employee("Grace", 110000), Employee("Henry", 120000), Employee("Ivy", 130000),
    Employee("Jack", 140000)
]
 
# Tag employees by department and seniority
db0.tags(*employees[:6]).add("engineering")
db0.tags(*employees[4:]).add("senior")
 
# Create subqueries for different employee groups
engineers = db0.find(Employee, "engineering")
seniors = db0.find(Employee, "senior")
 
# Filter by type and subquery (all employees who are engineers)
assert set(db0.find(Employee, engineers)) == set(employees[:6])
 
# Merge result sets - employees who are EITHER engineers OR seniors
assert set(db0.find([engineers, seniors])) == set(employees)
 
# Intersection - employees who are BOTH engineers AND seniors
assert set(db0.find(engineers, seniors)) == set(employees[4:6])
 
# Difference - engineers who are NOT seniors
assert set(db0.find(engineers, db0.no(seniors))) == set(employees[:4])
 
# Create a salary index for range queries
salary_index = db0.index()
for emp in employees:
    salary_index.add(emp.salary, emp)
 
# Combine subquery with index - seniors earning 80k-120k
assert set(db0.find(seniors, salary_index.select(80000, 120000))) == set(employees[4:8])

Filtering, Sorting, and Splitting

You can further process the results of dbzero.find() using other functions:

Checking if an Object Has a Specific Tag

Tagging in dbzero creates a one-directional link from a tag to tagged objects. This is our design choice that dbzero doesn't keep reverse mappings of these relations. This is motivated by our desire to keep the query engine simple and efficient.

You can however check if a specific tag is applied to a given object. The query dbzero.find(tag, object) tests if this relation exists. What it effectively does is filter the result set of a tag query with an object instance. This is much faster (O(log n) complexity) than checking the whole result set in pure Python. If the tag is present on the object, the result set will contain this single tested object.

# Create an object and assign a tag
user = User("Alex")
db0.tags(user).add("active-user")
 
# Check for the "active-user" tag. The query result evaluates to True.
assert db0.find("active-user", user)
 
# Check for a non-existent tag. The query result is empty and evaluates to False.
assert not db0.find("archived-user", user)

If your application requires that a list of tags applied to certain objects have to be retrievable, it's recommended to maintain a collection of tags as one of the object's attributes.

Working with Scopes (Prefixes)

If your application uses multiple data scopes (prefixes), you can direct your find query to a specific one.

@db0.memo
class UserSession:
    def __init__(self, username):
        self.username = username
 
# Create a session in the production environment
prod_session = UserSession("alice")
db0.tags(prod_session).add("active-session")
 
# Switch to staging environment and create a session there
db0.open('staging')
staging_session = UserSession("bob")
db0.tags(staging_session).add("active-session")
 
# Query specific prefixes regardless of the current environment
prod_sessions = db0.find(UserSession, "active-session", prefix='production')
assert set(prod_sessions) == {prod_session}
 
staging_sessions = db0.find(UserSession, "active-session", prefix='staging')
assert set(staging_sessions) == {staging_session}

Snapshot Isolation

Performing a query within a dbzero.snapshot ensures your view of the data is isolated. Modifications made outside the snapshot will not affect your query results, giving you a stable, point-in-time view of the application's state.

for i in range(10):
    obj = SomeClass()
    db0.tags(obj).add("test-tag")
db0.commit()
 
with db0.snapshot() as snap:
    # Run query over a snapshot while updating tags
    for i, snapshot_obj in enumerate(snap.find("test-tag")):
        if i % 2:
            # Since objects originating from the snapshot are immutable,
            # they have to be fetched from the head transaction
            obj = db0.fetch(db0.uuid(snapshot_obj))
            db0.tags(obj).remove("test-tag")
 
# The head transaction now sees the changes
assert len(db0.find("test-tag")) == 5

In multi-process environments, it is recommended to execute queries inside of a snapshot view to ensure data consistency throughout the whole logic.