Tag Fields
Tag fields are the declarative tagging technique described in
Working with Tags. Use them when a memo field should also be searchable
as a tag, such as status, owner, category, tenant, or parent.
With imperative db0.tags(...), application code decides
when to add or remove each tag. With @db0.tag_fields(...), dbzero mirrors the
current field value into the passive tag index and updates that tag when the
field changes.
import dbzero as db0
@db0.memo
@db0.tag_fields("status", "owner")
class Task:
def __init__(self, title, owner):
self.title = title
self.status = "open"
self.owner = owner
alice = User("Alice")
task = Task("Review invoice", alice)
assert task.status == "open"
assert list(db0.find(Task, "open")) == [task]
assert list(db0.find(Task, alice)) == [task]When to Use Tag Fields
Use tag fields when the tag is directly derived from one field on the object and should stay synchronized automatically:
- task state, such as
status - ownership or assignment, such as
owner - tenant, workspace, parent, or category fields
- scalar enum or memo-object references used as query dimensions
Keep using imperative tags when the relationship is not exactly one field, when an object needs many independent tags, when a tag should keep the object alive, or when you need composite tags, manual passive tags, or batch tagging.
Tag fields always create passive tags. They are queryable, but they do not keep the tagged object alive. Use regular tags, direct references, or collection ownership when the relationship should control object lifetime.
Synchronization
The declared fields stay ordinary Python attributes. dbzero synchronizes their passive tags during object initialization, field assignment, field deletion, and object deletion.
When a tag field changes, dbzero removes the old logical tag and adds the new one in the same update:
task.status = "done"
assert list(db0.find(Task, "open")) == []
assert list(db0.find(Task, "done")) == [task]Assign None when a field should contribute no tag:
task.owner = None
assert list(db0.find(Task, alice)) == []Deleting a tag field removes its current tag and then deletes the attribute:
del task.status
assert list(db0.find(Task, "done")) == []
task.status = "blocked"
assert list(db0.find(Task, "blocked")) == [task]Each field update and its matching tag update are atomic. If the new value is not a supported tag value, dbzero raises an error and keeps the previous field value and tag unchanged.
During __init__, tag-field values are applied after object construction
completes. Queries made from inside __init__ do not observe the new object's
tag fields yet.
Querying Tag Fields
Tag fields do not add a separate query API. Use normal tag predicates with
db0.find(...):
open_tasks = db0.find(Task, "open")
alice_tasks = db0.find(Task, alice)Because tag fields are passive tags, queries should include a positive anchor, usually the memo type:
# Valid: type predicate anchors the passive tag lookup.
open_tasks = db0.find(Task, "open")
# Invalid when "open" may be passive-only.
list(db0.find("open"))Supported Values
A tag field maps one field value to one tag. Supported values are:
None, which contributes no tag- scalar values accepted by the normal tag API, such as strings, enum values, or memo objects
Containers are not expanded into multiple tags. Assigning a list, set,
tuple, or other container value raises an error:
task.status = ["open", "urgent"] # Raises an error.Use db0.tags(obj).add(...) when an object needs many tags rather than one tag
derived from one field.
Manual Tags and Collisions
Tag fields use the normal tag namespace. dbzero does not track which source created a tag or how many tag fields currently hold the same logical value.
This keeps the model simple, but it means application code should avoid collisions between tag fields and manually managed tags when ownership matters:
- changing or deleting a tag field removes its old logical tag even if the same tag was also added manually
- changing one tag field may remove a tag value that another tag field still contains
- manually removing a tag can remove a relationship created by a tag field while the field value remains unchanged
Use distinct tag values, composite tags, or explicit relationship objects when independent ownership or multiplicity matters.
Decorator Composition and Inheritance
db0.tag_fields works before or after @db0.memo:
@db0.memo
@db0.tag_fields("status")
class Task:
pass
@db0.tag_fields("status")
@db0.memo
class Job:
passMultiple declarations on the same class are merged, and duplicate names are ignored:
@db0.tag_fields("owner")
@db0.memo
@db0.tag_fields("status")
class Task:
passThis is equivalent to @db0.tag_fields("status", "owner").
A subclass includes tag fields declared by its memo base classes and can add its own declarations:
@db0.tag_fields("tenant")
@db0.memo
class Record:
def __init__(self, tenant):
self.tenant = tenant
@db0.tag_fields("status")
@db0.memo
class Ticket(Record):
def __init__(self, tenant, status):
super().__init__(tenant)
self.status = statusQueries against Record can match inherited tag-field values on Ticket
instances, while queries against Ticket can also use status.
Field Identity and Renames
Tag-field declarations are stored with stable internal field identity. If you
rename a field with db0.rename_field(...), the
tag-field relationship follows the renamed field:
db0.rename_field(Task, "status", "state")
@db0.tag_fields("state")
@db0.memo
class Task:
def __init__(self, title, state):
self.title = title
self.state = stateRenaming a tag field this way does not require rebuilding all tag entries just because the Python field name changed.
Migrating Tag Fields
Adding, removing, or renaming tag-field declarations is a schema change. dbzero stores the active declaration with the memo type and compares it with the running code when the type is loaded against a prefix.
When automatic migration is enabled, dbzero applies declaration changes before activating the new declaration:
- adding a tag field scans existing objects and adds passive tags from current field values
- removing a tag field removes passive tags previously managed by that field
- renaming a tag field with
db0.rename_field(...)preserves field identity, so the tag-field behavior follows the renamed field
@db0.memo
class Task:
def __init__(self, status):
self.status = status
# Later:
@db0.tag_fields("status")
@db0.memo
class Task:
def __init__(self, status):
self.status = statusAfter migration, existing tasks are queryable through the new tag field:
open_tasks = list(db0.find(Task, "open"))For large prefixes, treat declaration changes like migrations. Disable automatic migration at startup and run it where your application applies schema changes:
db0.init("/var/lib/my-app/dbzero", no_auto_migrate=True)
db0.open("main", no_auto_migrate=True)
try:
Task("open")
except db0.MigrateError:
db0.migrate(Task)With automatic migration disabled, a declaration mismatch raises
db0.MigrateError, keeps the previous declaration active, and leaves the
current Python type available so your application can schedule
db0.migrate(Type) explicitly.
If migration fails, dbzero keeps the previous declaration active. Read-only prefixes do not apply tag-field declaration migrations; the prefix must be opened by a read-write owner and migrated there.
When multiple application versions use the same writable prefix, coordinate tag-field declaration changes. An older process can otherwise see the newer declaration as a schema difference.