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")) == 0Find 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 argumentsfind("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 withtagAANDtagB.find(("tagA", "tagB"))→ objects withtagAANDtagB.find(["tagA", "tagB"])→ objects withtagAORtagB.find("tagA", dbzero.no("tagB"))→ objects withtagAAND NOTtagB.
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:
- dbzero.filter: Apply a Python function for a fine-grained filtering.
- dbzero.index: Sort results and query objects with populated index.
- dbzero.split_by: Decorate query result with specified groups.
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")) == 5In multi-process environments, it is recommended to execute queries inside of a snapshot view to ensure data consistency throughout the whole logic.