# The change model

Two change vocabularies: SourceChange goes in, CaughtChange comes out — plus the change sink that delivers them and the replay-equivalence invariant.

Rindle keeps a query's result set current by emitting the **difference** after
every write, never by re-running the query. This page is the precise contract for
those differences: the change types the engine produces, the shape of each delta,
and how a delta crosses the library boundary into your code.

Everything below maps to real types in the open `rindle` engine. The
[live-replica runtime](/docs/replica-and-views) re-exports the output types under
friendlier names (noted where relevant), but the shapes are the engine's.

## Two kinds of change

There are two distinct change types, and conflating them is the first mistake to
avoid. One is the **input** to the engine; the other is the **output** you
consume.

| Direction | Rust type | Where it comes from |
| --------- | --------- | ------------------- |
| Input (a row mutation) | `rindle::SourceChange` | a write to a base table |
| Output (a result delta) | `rindle::CaughtChange` | the engine, per affected query |

In the open engine you push a `SourceChange` yourself — through a write-through
[`TableSource`](/docs/quickstart) it also persists the row — and read the
`CaughtChange`s the engine emits. The live-replica runtime flips the input side:
its `preupdate` hook captures the `SourceChange` from your SQL statement so you
never build one, and it re-exports the output types as friendlier aliases:

```rust
// rindle-replica/src/lib.rs
pub use rindle::CaughtChange as ChangeEvent;
pub use rindle::CaughtNode as NodeData;
```

So `rindle_replica::ChangeEvent` **is** `rindle::CaughtChange`, and
`rindle_replica::NodeData` **is** `rindle::CaughtNode` — same types either way.

## The input side: `SourceChange`

A `SourceChange` is one row-level mutation against a base table. It has three
variants:

```rust
// rindle/src/change.rs
pub enum SourceChange {
    Add(Row),
    Remove(Row),
    Edit { row: Row, old: Row },
}
```

`Add` and `Remove` carry a single row. `Edit` is an in-place change whose
primary key is unchanged: it carries both the new `row` and the `old` row it
replaces. (`Row` is `rindle::OwnedRow`, an owned slice of cells — see
[Crates](/docs/crates) for the value types.)

You push these yourself in the open engine (the [quickstart](/docs/quickstart)
does); the live-replica runtime derives them from your SQL for you. Either way,
the output deltas below are what you consume.

## The output side: `CaughtChange`

A `CaughtChange` is one delta to a query's result set. It is *fully owned* — every
lazy relationship has been drained into a concrete tree — so it can safely cross a
callback, thread, or process boundary. There are **four** variants:

```rust
// rindle/src/changes.rs  (re-exported as rindle_replica::ChangeEvent)
pub enum CaughtChange {
    Add(CaughtNode),
    Remove(CaughtNode),
    Edit {
        old: OwnedRow,
        row: OwnedRow,
    },
    Child {
        row: OwnedRow,
        rel: RelId,
        change: Box<CaughtChange>,
    },
}
```

Note the asymmetry with `SourceChange`, which trips people up:

- `Add` / `Remove` carry a whole `CaughtNode` (row **plus** its relationship
  subtrees).
- `Edit` carries only the two rows — `old` and `row` — not nodes. A row's
  identity is unchanged on an edit, so its relationships did not move; there is
  nothing to re-materialize.
- `Child` is the variant that has no analog on the input side. It means "a row
  already in the result set had one of its nested relationships change." It
  carries the parent `row`, the relationship slot `rel` (a `RelId`, an index —
  not a string name), and a boxed nested `change` describing what happened
  inside that relationship.

There is no `Insert`, `Delete`, or `Update` variant, and no
`Update { old, new }` shape. The names are `Add`, `Remove`, `Edit`, `Child`.

### `CaughtNode`

The node carried by `Add` / `Remove` is a `CaughtNode` (re-exported as
`rindle_replica::NodeData`):

```rust
// rindle/src/changes.rs
pub struct CaughtNode {
    pub row: OwnedRow,
    pub relationships: BTreeMap<RelId, Vec<CaughtNode>>,
}
```

`row` is the row's cells. `relationships` maps each in-view relationship slot to
its children, keyed by `RelId` so the map is slot-stable; the child `Vec`
preserves the operator's sort order (do not re-sort it). A flat query with no
nested relationships simply has an empty `relationships` map.

## Reconstruction invariant

Applying the full stream of `CaughtChange`s in order reconstructs exactly the
result you would get by running the query from scratch. That equivalence — view
after the deltas equals a fresh query — is the engine's core correctness
contract. A subscriber that folds the stream into its own state therefore stays
bit-for-bit consistent with the source.

## Receiving deltas: the change sink

At the open-engine layer you attach a **change sink** over the pipeline's top and
drain it. `hydrate_change_sink` returns the cold-start snapshot (the initial result
as a batch of `Add`s); after each batch of source pushes, `take_sink_changes`
drains that transaction's deltas:

```rust
use rindle::CaughtChange;

let sink = graph.add_change_sink(top);
graph.set_sink_edge(top, sink);

let initial: Vec<CaughtChange> = graph.hydrate_change_sink(sink); // once
// ... after a batch of graph.source_push(..):
let delta: Vec<CaughtChange> = graph.take_sink_changes(sink);     // per transaction
```

A full fold-and-prove walkthrough is in
[fold the delta stream yourself](/docs/example-rust).

The [live-replica runtime](/docs/replica-and-views) wraps exactly this in a
callback. `Query::subscribe` delivers an `Update` — `Hydrated { tx_id, changes }`
once immediately (the initial set as `Add`s), then `Changed { tx_id, changes }`
after every committed write, each carrying a `Vec<ChangeEvent>` (i.e.
`Vec<CaughtChange>`) and the `TxId` it reflects. The callback runs on the writer
thread during `commit`, so keep it cheap and do not re-enter the writer from
inside it.

## Snapshot boundaries

Deltas are cut at transaction boundaries. A batch of pushes drains as one set of
deltas (`take_sink_changes`); the runtime cuts the same boundary at each durable
`COMMIT`, deriving the transaction's change against the pre-commit snapshot and
delivering it only after the commit — so a subscriber never observes a partial
transaction and never sees a tx that later failed. Each runtime `Update` is tagged
with the `TxId` it reflects.

## Next steps

- [How it works](/docs/how-it-works) — the build → lower → hydrate → push
  lifecycle these deltas flow through.
- [Fold the delta stream yourself](/docs/example-rust) — attach a change sink,
  fold the deltas into your own view, and prove it equals a fresh query.
- [The live-replica runtime](/docs/replica-and-views) — the `Query::subscribe` /
  `Update` delivery wrapper and the write-then-abort derivation model.
- [Crates](/docs/crates) — `rindle`, `rindle-replica`, and `rindle-sqlite`, and the
  value types (`OwnedValue`, `OwnedRow`, `RelId`) referenced above.

---

[View this page on Rindle](https://rindle.sh/docs/change-model)
