Entity Snapshots

entity_snapshots is a PostgreSQL table in the causet database that stores the materialized current state of each entity. Snapshots are a performance optimization — they allow causet-runtime to evaluate rules against current state without replaying the full ledger on every intent.


What Snapshots Store

A snapshot is the current state of a single entity: the result of applying all ledger events for that entity in sequence.

CREATE TABLE entity_snapshots (
    entity_id   TEXT    NOT NULL,
    fork_id     TEXT    NOT NULL,
    stream      TEXT    NOT NULL,
    state       JSONB   NOT NULL,
    version     BIGINT  NOT NULL,
    updated_at  BIGINT  NOT NULL,
    PRIMARY KEY (entity_id, fork_id, stream)
);
ColumnDescription
entity_idThe entity identifier
fork_idThe fork this snapshot belongs to
streamThe state stream name (entity type, e.g. user, artist)
stateThe current entity state as JSONB
versionThe sequence_number of the last ledger event applied to this snapshot
updated_atTimestamp of the last snapshot update

How Snapshots Are Maintained

On every successful intent:

  1. Rules evaluate against the current snapshot (state JSONB).
  2. Core rules produce mutations (set, increment, array_push, etc.).
  3. After ledger events are appended, the snapshot is updated by applying the mutations.
  4. The UPSERT to entity_snapshots happens in the same R2DBC transaction as the ledger event inserts.

The snapshot update and ledger append are atomic — they either both succeed or both fail.


Snapshots Are Not the Source of Truth

The ledger is the source of truth. The snapshot is derived from it. This means:

  • If a snapshot is lost or corrupted, it can be rebuilt by replaying ledger events.
  • If a snapshot and the ledger disagree (e.g. after a rule bug), the ledger takes precedence.
  • Snapshots can be deleted and rebuilt without data loss.

State Accessed in Rules

When causet-runtime evaluates rules for an intent, the snapshot’s state JSONB is the state context available in expressions:

preflight:
  rules:
    - name: reject_already_following
      when: { expr: "state.following contains intent.artist_id" }
      then:
        - op: reject
          code: ALREADY_FOLLOWING

state.following refers to the following array field in the current entity snapshot.


Snapshot and Ledger Divergence

Divergence can occur if:

  1. A rule bug caused an incorrect mutation to be applied to the snapshot but the corresponding ledger event is correct.
  2. A deployment issue caused a snapshot update to succeed but the ledger write to fail (should not happen given the shared R2DBC transaction, but a catastrophic crash could theoretically cause this).
  3. A snapshot was manually modified for debugging.

If divergence is detected (e.g. the version on the snapshot does not match the expected sequence_number from the ledger), rebuild the snapshot:

# Proposed CLI — see Replay docs
causet snapshots rebuild --entity-id user-123 --fork-id production

Rebuilding Snapshots

To rebuild a snapshot, the runtime replays all ledger events for the entity in order and re-applies each event’s mutations:

  1. Load all ledger_events for (entity_id, fork_id) ordered by sequence_number.
  2. Start with an empty state ({}).
  3. For each event, apply the mutations declared in the IR for that event type.
  4. Upsert the resulting state into entity_snapshots.

This process is safe to run while the runtime is handling other entities. The cursor lock for (entity_id, fork_id) must be acquired before rebuilding to prevent concurrent writes during the rebuild.


Schema Evolution and Snapshots

When the DSL changes — for example, a new field is added to an entity state — the snapshot does not automatically gain that field. The field appears on the next intent that triggers a set for it.

If an existing snapshot is missing a field that rules now expect, expressions against that field return null. Design rules defensively:

# Guard against null before comparing
when: { expr: "state.tier != null && state.tier == 'premium'" }

If you need to backfill a field across all existing snapshots after a schema change, submit a backfill intent (a no-op action that sets the new field’s default value) to each affected entity.


Stale Snapshot Detection

The version column on a snapshot reflects the sequence_number of the last applied ledger event. If the latest sequence_number in ledger_events for an entity is higher than the snapshot’s version, the snapshot is stale.

This can happen during:

  • A runtime crash after ledger write but before snapshot update (extremely rare given the shared transaction).
  • Manual ledger manipulation (not recommended).

The runtime detects this on the next intent and can trigger an automatic snapshot rebuild before proceeding.