kuatia-types (Posting, PostingState,
PostingFilter), kuatia-storage (PostingStore, InMemoryStore),
kuatia-storage-sql (schema, migration 004), kuatia (ledger, saga)ADR-0006 gave every posting a mutable status column
(Active → PendingInactive → Inactive) plus a nullable reservation token,
both flipped in place with UPDATE. Consumed postings stayed in the table as
Inactive tombstones. That works, but it makes the postings table a
mutate-in-place table: the commit path needs UPDATE rights on it, a
historical posting's value or state can be rewritten by any code path (or any
compromised credential) that can issue an UPDATE, and every "what can this
account spend" read scans the full postings history filtered by a partial index
on status, a set that only grows.
The reservation protocol of ADR-0006 does not actually require the state to live on the posting. It requires an atomic single-winner claim, durable recoverable ownership, and count-returning primitives (ADR-0003). Can we keep those guarantees while making a posting an append-only record that is written once and never changed?
INSERT on postings, never UPDATE or
DELETE. No path can rewrite a historical row. Lifecycle churn is confined to
small index tables that hold ids, not monetary values.status (a partial index over cold and hot rows mixed together), keep the
working set in its own physically separate table. The index is the table.status column + partial index)Leave postings as a mutate-in-place table with status + reservation.
Pros:
Cons:
UPDATE on the value-bearing table, so
a bug or a compromised credential can rewrite historical postings.status; the hot working set is never physically separated from cold
tombstones.Delete a posting id from a single active_postings table to reserve it; keep
the reservation solely in the saga's write-ahead PendingSaga blob (ADR-0003).
Pros:
Cons:
Active + Reserved) and close (blocks on live) change behavior,
and recovery cannot read "reserved by rid" from a row.postings becomes insert-only and immutable:
(transfer_id, idx, owner, subaccount, asset, value), no status, no
reservation. Two index tables hold only ids: active_postings (membership =
spendable) and reserved_postings ((id, reservation), membership = claimed by
a saga). A posting's state is derived from membership: in active → Active, in
reserved → Reserved(rid), in neither → Spent. Every transition is an
insert/delete on an index table; the posting row never changes:
INSERT into postings, then INSERT id into
active_postings.DELETE id from active_postings; if it removed a row, INSERT
id + rid into reserved_postings. The delete-returns-one is the atomic
single-winner claim.DELETE id from reserved_postings where reservation =
rid. The posting stays in postings forever, now in neither index = spent.DELETE from reserved_postings, INSERT back into
active_postings.Pros:
postings is append-only. The commit role needs only INSERT
on it; no code path or credential can rewrite a historical posting. The
immutable table is the audit trail, with history also reconstructable from the
transfer + event logs.reserved_postings.reservation records who holds a posting, so
recovery and finalize target only their own rows exactly as in ADR-0006.Active ∪ Reserved) and close (blocks on any live)
keep their exact prior semantics, now expressed as index membership instead of
a status filter.Cons:
get_posting_states), and
three tables replace one.postings without the status/reservation columns).Chosen option: Option 3, immutable postings with active/reserved index
tables, because it keeps every guarantee of ADR-0006 (lock-free double-spend
safety, durable recoverable ownership, count-returning primitives) while making
the value-bearing table append-only. That buys least-privilege security (the
commit role needs no UPDATE on postings, so historical rows are
tamper-evident by construction) and read performance by segregation (the hot
active/reserved working set lives in its own id-only tables rather than behind a
partial index over ever-growing history).
This supersedes the storage representation of ADR-0006. The reservation protocol
of ADR-0006 stands; only its physical encoding changes, from an in-place
status transition to index-table membership.
postings is insert-only: grant INSERT, withhold UPDATE/DELETE.DELETE FROM active_postings as the concurrency gate; the saga
reads the affected-row count to know it won (ADR-0003).reserved_postings with our rid) from "spent" (absent from both indexes) and
"taken by another" (reserved by a different rid), which is what makes
phase-tracked roll-forward safe.active_postings /
reserved_postings / postings rather than one column.Inactive tombstone: a spent posting's state is "present in postings,
absent from the indexes." Consumers that read per-posting state use the
derived PostingState, not a stored column.