Metadata¶
Every mutation of any object creates metadata — a record of who changed what and when. Metadata is managed by the framework; you can read it and use it in queries but cannot modify it directly.
Accessing Metadata¶
metadata = person.get_metadata()
print(metadata.is_deleted) # False
print(metadata.created_at) # 1709712000000 (ms since epoch)
metadata = await person.aget_metadata()
print(metadata.is_deleted)
print(metadata.created_at)
Metadata fields¶
| Field | Type | Description |
|---|---|---|
object_id |
Any |
Object identifier (PK value or auto-generated) |
object_version |
str \| Versions |
This row's version identifier |
class_schema_reference |
Reference |
Identifies the model class and its schema version |
class_meta_schema_reference |
Reference \| None |
Reference to the class meta-schema (if any) |
is_deleted |
bool |
True if this version is a soft-delete tombstone |
prior_version |
str \| None |
The previous version's object_version (None for the first version) |
next_version |
str \| None |
The next version's object_version (None for the latest; computed at query time) |
created_at |
int |
Object creation timestamp (ms since epoch); same across all versions of the same object |
updated_at |
int |
This version's timestamp (ms since epoch); changes on every save |
transaction |
Reference \| None |
Reference to the transaction that produced this row, when present |
Convenience flags also exposed on the Model:
instance.is_from_lakehouse—Trueif the instance was loaded from the lakehouse pathinstance.is_latest(viaget_metadata().is_latest) —Trueif this is the latest version (next_version is None)
Filtering by Metadata¶
Use the _metadata prefix in queries:
Warning
_metadata__* filters apply only on historical / lakehouse connections. On state connections they are silently dropped (no error, no effect).
# Soft-deleted records (historical / lakehouse only)
Person.objects.using('lakehouse').filter(_metadata__is_deleted=True)
# Records created in the last 24 hours.
# created_at is int ms — convert datetime to ms-since-epoch explicitly.
from datetime import datetime, timedelta, timezone
threshold_ms = int((datetime.now(timezone.utc) - timedelta(hours=24)).timestamp() * 1000)
Person.objects.using('lakehouse').filter(
_metadata__created_at__gt=threshold_ms,
)
See Field Lookups for the full list of available metadata fields.
Address¶
Every record has an address that uniquely identifies it in the database — connection name, class name, class version, object ID, and object version.
person = Person(first_name="Jane")
person.save()
person.last_name = "Roe"
person.save()
latest, old = Person.objects.using(
'lakehouse',
).order_by('-_metadata__updated_at').execute()
# Same object_id, different object_version
old_addr = old.get_metadata().address
new_addr = latest.get_metadata().address
old_addr.object_id == new_addr.object_id # True
old_addr.object_version != new_addr.object_version # True
person = Person(first_name="Jane")
await person.asave()
person.last_name = "Roe"
await person.asave()
latest, old = await Person.objects.using(
'lakehouse',
).order_by('-_metadata__updated_at').aexecute()
old_addr = (await old.aget_metadata()).address
new_addr = (await latest.aget_metadata()).address
old_addr.object_id == new_addr.object_id # True
old_addr.object_version != new_addr.object_version # True
Version History¶
Every save creates a new version. Versions are linked via prior_version and next_version:
- First version:
prior_versionisNone - Latest version:
next_versionisNone
old_meta = old.get_metadata()
latest_meta = latest.get_metadata()
# First version → no prior, has next
old_meta.prior_version # None
old_meta.next_version == latest_meta.address.object_version # True
# Latest version → has prior, no next
latest_meta.prior_version == old_meta.address.object_version # True
latest_meta.next_version # None
old_meta = await old.aget_metadata()
latest_meta = await latest.aget_metadata()
old_meta.prior_version # None
old_meta.next_version == latest_meta.address.object_version # True
latest_meta.prior_version == old_meta.address.object_version # True
latest_meta.next_version # None
References¶
Metadata tracks relationships between objects via reference_to and referenced_by — both are lists of Reference objects.
jane = Person(first_name="Jane")
jane.save()
location = Location(name="New York")
location.save()
# No references yet
jane.get_metadata().reference_to # []
location.get_metadata().referenced_by # []
# Create a reference
jane.location = location
jane.save()
# Jane references location
jane.get_metadata().reference_to # [Reference(...Location...)]
jane.get_metadata().referenced_by # []
# Location is referenced by jane
location.get_metadata().referenced_by # [Reference(...Person...)]
location.get_metadata().reference_to # []
jane = Person(first_name="Jane")
await jane.asave()
location = Location(name="New York")
await location.asave()
meta = await jane.aget_metadata()
meta.reference_to # []
jane.location = location
await jane.asave()
meta = await jane.aget_metadata()
meta.reference_to # [Reference(...Location...)]
loc_meta = await location.aget_metadata()
loc_meta.referenced_by # [Reference(...Person...)]
Note
reference_to and referenced_by are populated from the lakehouse REFERENCE_TABLE. On state-only deployments (no lakehouse configured) these accessors return an empty list. Use a deployment with a lakehouse connection if you depend on this introspection.
Timestamps¶
Each record has created_at and updated_at timestamps (milliseconds since epoch). Different versions of the same object share created_at but have different updated_at:
person = Person(first_name="Jane")
person.save()
initial_created = person.get_metadata().created_at
initial_updated = person.get_metadata().updated_at
person.last_name = "Roe"
person.save()
person.get_metadata().created_at == initial_created # True — same
person.get_metadata().updated_at > initial_updated # True — newer
person = Person(first_name="Jane")
await person.asave()
meta = await person.aget_metadata()
initial_created = meta.created_at
initial_updated = meta.updated_at
person.last_name = "Roe"
await person.asave()
meta = await person.aget_metadata()
meta.created_at == initial_created # True
meta.updated_at > initial_updated # True
Performance — avoiding N+1 on metadata access¶
When models are loaded via the state path (Person.objects.filter(...).execute() without .using('lakehouse')), the in-memory instance has no metadata attached — each call to get_metadata() issues a separate lakehouse query to fetch it. Reading metadata for N objects costs N round-trips.
Load through the lakehouse path when you need metadata for many objects in one go:
# Sync — metadata bundled into each row, no extra round-trips
for person in Person.objects.using('lakehouse').latest().execute():
meta = person.get_metadata() # uses pre-loaded data; no extra query
print(meta.created_at, meta.is_deleted)
# Async
async for person in Person.objects.using('lakehouse').latest().aexecute():
meta = await person.aget_metadata()
.latest() filters to the latest version of each object — drop it if you want every historical version.