Understanding Prefixes & Scoped types

Understanding Prefixes and Scoped Types

In dbzero, all your data lives within prefixes. Think of a prefix as a separate, isolated data partition with its own independent commit history. This allows you to logically group related data—for instance, keeping user accounts separate from application settings.

By default, every object you create is stored in the "current" prefix. However, dbzero provides a powerful feature called scoped types that automatically places objects of a certain class or enum onto a designated prefix, no matter what the current prefix is. This helps enforce data organization and boundaries within your application.


Defining Scoped Types

You can "scope" a class or enum to a specific prefix statically using a decorator. This is the most common way to ensure that certain types of data always land in the right place.

Scoped Classes

To assign a class to a specific prefix, use the prefix argument in the @dbzero.memo decorator.

# All instances of this class will be stored on the "settings-prefix"
@db0.memo(prefix="settings-prefix")
class AppSettings:
    def __init__(self, theme: str, language: str):
        self.theme = theme
        self.language = language
 
# This class is not scoped; its instances go to the current prefix
@db0.memo(prefix=None)
class UserNote:
    def __init__(self, content: str):
        self.content = content
 
# Let's create some objects
db0.open("user-data", "rw") # Set the current prefix to "user-data"
 
# This object is created on "settings-prefix" despite the current prefix
settings = AppSettings(theme="dark", language="en")
 
# This object is created on the current prefix, "user-data"
note = UserNote(content="Remember to buy milk.")
 
# Let's verify
print(db0.get_prefix_of(settings).name) # Outputs: "settings-prefix"
print(db0.get_prefix_of(note).name)     # Outputs: "user-data"
print(db0.get_current_prefix().name)   # Outputs: "user-data"

Key takeaway: Creating an instance of a scoped type does not change the application's current prefix. It's a fire-and-forget operation that ensures the object is stored correctly.

Scoped Enums

Similarly, you can scope an enum to a prefix using the @dbzero.enum decorator. This is useful when you want to use enum values to tag objects that belong to the same logical group.

@db0.enum(values=["ADMIN", "USER", "GUEST"], prefix="user-roles")
class Role:
    pass
 
# The Role enum and its values (Role.ADMIN, etc.) belong to the "user-roles" prefix
print(db0.get_prefix_of(Role).name) # Outputs: "user-roles"
print(db0.get_prefix_of(Role.ADMIN).name) # Outputs: "user-roles"
 
# Now you can tag objects with these scoped enum values
user_object = SomeUserClass()
db0.tags(user_object).add(Role.ADMIN)

Dynamic Scoping

Sometimes, you may not know the target prefix when you define a class. dbzero allows you to set the prefix dynamically from within the class's constructor.


To do this, define the class without a prefix in the decorator. Then, call dbzero.set_prefix(self, prefix_name) as the very first operation inside your __init__ method, before any other attributes are assigned or accessed. This is a mandatory requirement for the object to be correctly initialized within its designated prefix.

@db0.memo
class Project:
    def __init__(self, name: str, client_id: str):
        # The set_prefix call must be the first operation in __init__.
        # The prefix is determined dynamically, e.g., from a constructor argument.
        db0.set_prefix(self, f"client_{client_id}_projects")
        
        # Now you can initialize other attributes
        self.name = name
 
# Create an instance for client "acme".
# This will create and/or open the "client_acme_projects" prefix
# and store the object there.
project_acme = Project(name="Website Redesign", client_id="acme")
 
print(db0.get_prefix_of(project_acme).name) # Outputs: "client_acme_projects"

Automatic Reference Hardening

dbzero ensures that a scoped object and its managed data containers (like dbzero.list or dbzero.index) are stored together on the same prefix. It achieves this through a process called auto-hardening.

This process converts a "weak" reference into a "hard" reference".

  • A weak reference is a link to an object on a different prefix (e.g., a temporary index created on a default workspace).
  • A hard reference is a link to an object on the same prefix as its parent.

Auto-hardening happens automatically when you assign an empty and unreferenced dbzero container to an attribute of a scoped object. dbzero transparently moves the container from its original prefix to the scoped object's prefix, ensuring data locality and integrity.


The example below demonstrates this. An index is created on the "default-workspace" prefix. When it's assigned to an ArchivedDocument instance, which lives on the "document-archive" prefix, the index is automatically moved.

@db0.memo(prefix="document-archive")
class ArchivedDocument:
    def __init__(self, title):
        self.title = title
        # This index starts on the "document-archive" prefix, so the reference to is is already "hard".
        self.revisions = db0.index()
 
# Set current prefix to a different workspace
db0.open("default-workspace", "rw")
 
# 1. Create an index on the current prefix ("default-workspace").
#    This is a "weak" reference relative to the ArchivedDocument.
temp_index = db0.index()
print(f"Prefix of temp_index before: {db0.get_prefix_of(temp_index).name}")
 
# 2. Create the scoped object.
doc = ArchivedDocument("Q3 Report")
 
# 3. Assign the weak reference. Auto-hardening occurs here.
doc.revisions = temp_index
 
# 4. The index has been moved to the object's prefix, becoming a "hard" reference.
print(f"Prefix of doc: {db0.get_prefix_of(doc).name}")
print(f"Prefix of doc.revisions after: {db0.get_prefix_of(doc.revisions).name}")
 

Output:

Prefix of temp_index before: default-workspace
Prefix of doc: document-archive
Prefix of doc.revisions after: document-archive

💡 Hardening only applies to empty, unreferenced dbzero containers. Standard Python collections (list, dict) are not managed this way, and containers that already hold data will not be moved.


Linking Objects Across Prefixes

dbzero enforces a strict rule to prevent data entanglement: an object cannot hold a direct, strong reference to an object on a different prefix. A strong reference would imply ownership and affect the lifecycle (reference counting) of the foreign object, which is not allowed.

🚫

Attempting to store a direct reference to an object from a different prefix will raise an exception.

The Solution: dbzero.weak_proxy()

To link objects across different prefixes, you must use a weak reference. dbzero provides dbzero.weak_proxy() for this purpose.

A weak proxy acts as a stand-in for the real object but with one crucial difference: it does not increment the target object's reference count.

# Assume user is on "users-prefix" and document is on "docs-prefix"
user = User(name="John Doe", prefix="users-prefix")
doc = Document(title="Contract", prefix="docs-prefix")
 
# This would FAIL:
# doc.author = user  # Raises an exception!
 
# This is the correct way:
doc.author = db0.weak_proxy(user)
 
# The proxy is transparent; you can access attributes through it
print(doc.author.name) # Outputs: "John Doe"
 
# The reference count of the user object is not affected
print(db0.getrefcount(user)) # Stays the same

Expired References

If the original object is deleted, the weak proxy becomes expired. Accessing an expired proxy's attributes will raise a dbzero.ReferenceError. You can check if a proxy is expired before using it with dbzero.expired().

doc.author = db0.weak_proxy(user)
 
# Now, let's delete the user
del user
db0.commit()
 
# Check the status of the proxy
print(db0.expired(doc.author)) # Outputs: True
 
# This would now raise an error:
# print(doc.author.name) # db0.ReferenceError
 
# You can still retrieve the UUID of the object the proxy once pointed to
uuid_of_deleted_user = db0.uuid(doc.author)

Weak proxies are the essential tool for creating relationships and graphs between objects that live in different logical data partitions, giving you the power to build complex, interconnected applications while maintaining clear data boundaries.