Skip to content

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_lakehouseTrue if the instance was loaded from the lakehouse path
  • instance.is_latest (via get_metadata().is_latest) — True if 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_version is None
  • Latest version: next_version is None
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.