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.
Naming Prefixes
Use stable, namespaced prefix names so application data is easy to separate by owner, project, environment, and asset. A recommended pattern is:
/orgname/projectname/env/assetFor example, an application might use /acme/crm/dev/customers for development customer data and /acme/crm/prod/customers for the production version of the same asset.
Short names such as "user-data" are fine for local examples, but production prefixes should be globally meaningful enough to avoid collisions across organizations, projects, and environments.
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)Restricted Prefixes
Restricted mode is available in the standard dbzero package starting with 0.4.1. It is a sandboxing option for prefixes used by AI agents, plugins, and other constrained code. Memo objects on a restricted prefix still expose their public fields and public methods, but reflection-style access to metadata such as __class__, __dict__, private attributes, and bound-method internals is denied.
Enable restricted mode for one prefix by passing restricted=True to db0.open():
db0.init("./app-data")
db0.open("app-config", "rw")
db0.open("agent-memory", "rw", restricted=True)
assert db0.get_prefix_stats("app-config")["restricted"] is False
assert db0.get_prefix_stats("agent-memory")["restricted"] is TrueYou can also make restricted mode the default for future opened prefixes:
db0.init("./app-data", restricted=True)
# Restricted by default.
db0.open("agent-memory")
# Explicitly unrestricted for this process.
db0.open("host-admin-data", "rw", restricted=False)db0.init(..., restricted=True) applies to prefixes opened later and prefixes auto-opened by dbzero in read-only mode. restricted=False is useful when initially opening a prefix that should stay unrestricted despite a restricted workspace default. Once a prefix is open with a restriction setting, later changes can only increase the restriction level for the current process.
Starting with dbzero 0.4.2, a prefix can also use a restricted context variable. This lets trusted host code and constrained code share a process while restricted mode follows a contextvars.ContextVar value:
from contextvars import ContextVar
import dbzero as db0
agent_restricted = ContextVar("agent_restricted", default=False)
db0.init("./app-data")
db0.open("agent-memory", "rw", restricted_context=agent_restricted)When agent_restricted.get() is truthy, memo reflection access for objects in agent-memory is restricted. When it is false or unset, access is unrestricted. Prefix-level restricted_context overrides a workspace-level context for that prefix.
For already-open prefixes, use db0.set_restricted() to increase the restriction level:
db0.set_restricted(prefix="agent-memory", restricted_context=agent_restricted)
db0.set_restricted(prefix="agent-memory", restricted=True)Restriction changes are monotonic. A prefix can move from unrestricted to context-controlled restriction, and from context-controlled restriction to static restricted=True. It cannot move from static restricted mode to context-controlled mode, replace an existing context, or become unrestricted again in the same process.
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
dbzerocontainers. 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 sameExpired 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.