Reference for the tables kuatia-storage-sql creates. This is the composed
end-state after replaying every migration in
crates/kuatia-storage-sql/src/migrations/ (001_init through
007_balance_projection) plus the _migrations bookkeeping table created in
crates/kuatia-storage-sql/src/migrate.rs. It is a living reference: update it
when a migration lands. For why the schema looks this way, see the ADRs it
links to, not this file.
Ground rules that keep the catalog terse:
transfer_id + idx; account_head points at
the current accounts version; the account/transfer link is an explicit index
table. See ADR-0003.TEXT. Content-addressed ids and
opaque saga bytes are lower-case hex; JSON payloads are their TEXT
serialization; a Cent is its decimal string. The store never does arithmetic
or SUM/MAX on these; balances are computed in Rust.BIGINT. No AUTOINCREMENT / SERIAL; snowflake
ids come from AutoId. See ADR-0015.sqlx::Any).Design rationale lives in ADR-0016 (immutable postings + index tables), ADR-0017 (append-only hot indexes), ADR-0022 (merged live-postings hot index), ADR-0019 (cached balance projection), and ADR-0008 (conformance-tested storage).
Edges are logical only (no FOREIGN KEY constraint exists); the labels name
how the ledger relates the rows in Rust.
erDiagram
accounts {
BIGINT id PK
BIGINT subaccount PK
BIGINT version PK
INTEGER flags
BIGINT book
TEXT metadata
}
account_head {
BIGINT id PK
BIGINT subaccount PK
BIGINT version
}
postings {
TEXT transfer_id PK
SMALLINT idx PK
BIGINT owner
BIGINT subaccount
INTEGER asset
TEXT value
}
live_postings {
TEXT transfer_id PK
SMALLINT idx PK
BIGINT owner
BIGINT subaccount
INTEGER asset
TEXT value
BIGINT reservation
}
transfers {
TEXT id PK
TEXT transfer
TEXT receipt
BIGINT created_at
BIGINT book
}
transfer_accounts {
TEXT transfer_id PK
BIGINT account_id PK
BIGINT subaccount PK
}
books {
BIGINT id PK
TEXT name
TEXT data
}
balance_projection {
BIGINT id PK
BIGINT account
BIGINT subaccount
INTEGER asset
TEXT balance
BIGINT watermark
}
sagas {
BIGINT id PK
TEXT data
}
events {
BIGINT seq PK
BIGINT timestamp
TEXT kind
TEXT data
TEXT dedup_key UK
}
migrations {
TEXT name PK
}
accounts ||--|| account_head : "head -> current version"
accounts }o--|| books : "book id"
transfers ||--o{ transfer_accounts : "id = transfer_id"
accounts ||--o{ transfer_accounts : "account_id + subaccount"
transfers ||--o{ postings : "id = transfer_id"
postings ||--o| live_postings : "live index copy"
accounts ||--o{ balance_projection : "account + subaccount"
(The _migrations table is shown as migrations; Mermaid entity names cannot
start with an underscore.)
Append-only, versioned accounts with a head pointer. Owned by account.rs
(AccountStore). See ADR-0012 (subaccounts) and
ADR-0020 (account transition recovery).
accountsEvery account version is an immutable row; a new version is appended, never
updated in place. metadata is JSON.
| Column | Type | Key | Purpose |
|---|---|---|---|
id |
BIGINT |
PK | Base account id. |
subaccount |
BIGINT |
PK | Subaccount code (0 = base). |
version |
BIGINT |
PK | Monotonic version; the chain is gap-free. |
flags |
INTEGER |
AccountFlags bitfield (frozen/closed/inflight, DEBIT_MUST_NOT_EXCEED_CREDIT). |
|
book |
BIGINT |
Owning book id. | |
metadata |
TEXT |
JSON key/value metadata. |
(id, subaccount, version).account_headOne row per account pointing at its current version, so a lookup is a single
indexed join instead of scanning the version chain. Maintained by delete+insert,
never UPDATE.
| Column | Type | Key | Purpose |
|---|---|---|---|
id |
BIGINT |
PK | Base account id. |
subaccount |
BIGINT |
PK | Subaccount code. |
version |
BIGINT |
The current version in accounts. |
(id, subaccount).A posting is a signed amount of one asset owned by one (sub)account, identified
by (transfer_id, idx). The immutable postings record is the historical source
of truth; one live_postings hot table carries a full row copy of the live set
(spendable + reserved), so spendable reads never merge back to history. Lifecycle
state is derived from live_postings membership plus its reservation column:
present with reservation NULL = Active, present with a reservation = Reserved,
in postings only = Spent, absent = Missing. Owned by posting.rs
(PostingStore). See
ADR-0016 (immutable postings + index tables),
ADR-0017 (full-row hot copies),
ADR-0022 (merged hot index),
ADR-0006 (reservation protocol).
Why one hot table with full-row copies? The index originally held only ids in
two tables (ADR-0016);
ADR-0017 switched to
full-row copies, and ADR-0022 merged
the active and reserved tables into one live_postings (a nullable reservation
replaces the two-table split). The hot read is "what can this account spend in
this asset" (get_postings_by_account, query_postings, and the balance sum):
carrying the data columns lets live_postings hold idx_live_owner(owner,
subaccount, asset), so a live read is one index scan on a small table with no
join back to history and no UNION (the index is the table). The duplication
is safe because the copied columns are immutable: postings rows never change and
reserve/release only flip the reservation column, so a copy can never drift from
its value row. And it is bounded and disposable: only the live set is duplicated
(spent postings live in postings alone), and live_postings is rebuildable from
postings plus the saga write-ahead records, so a corrupt hot table is a
drop-and-rebuild, not data loss.
postingsThe immutable record. A row here that is absent from live_postings is Spent.
| Column | Type | Key | Purpose |
|---|---|---|---|
transfer_id |
TEXT |
PK | Creating transfer's id (hex). |
idx |
SMALLINT |
PK | Position within that transfer. |
owner |
BIGINT |
Owning base account id. | |
subaccount |
BIGINT |
Owning subaccount code. | |
asset |
INTEGER |
Asset id. | |
value |
TEXT |
Signed Cent as a decimal string. |
(transfer_id, idx).idx_postings_owner (owner, subaccount, asset).live_postingsThe live-set hot copy: the six data columns plus a nullable reservation. A
posting is here while it is spendable or reserved; reservation IS NULL = Active,
a set reservation = Reserved by that saga. Reserve/release flip the column
(UPDATE), consume deletes the row (→ Spent). Rebuildable from postings + the
saga records.
| Column | Type | Key | Purpose |
|---|---|---|---|
transfer_id |
TEXT |
PK | Posting id (hex). |
idx |
SMALLINT |
PK | Position within the transfer. |
owner |
BIGINT |
Owning base account id. | |
subaccount |
BIGINT |
Owning subaccount code. | |
asset |
INTEGER |
Asset id. | |
value |
TEXT |
Signed Cent as a decimal string. |
|
reservation |
BIGINT |
Nullable: NULL = Active, set = the ReservationId holding this posting (Reserved). |
(transfer_id, idx).idx_live_owner (owner, subaccount, asset).Committed envelope records and the account index that finds them. Owned by
transfer.rs (TransferStore).
transfers| Column | Type | Key | Purpose |
|---|---|---|---|
id |
TEXT |
PK | Content-addressed envelope id (hex). |
transfer |
TEXT |
The Envelope as JSON. |
|
receipt |
TEXT |
The Receipt as JSON. |
|
created_at |
BIGINT |
Unix millis when stored (default 0). |
|
book |
BIGINT |
Owning book id (default 0). |
id.idx_transfers_created_at (created_at), idx_transfers_book (book).transfer_accountsThe account -> transfer index; the caller supplies the involved set, the store does no computation.
| Column | Type | Key | Purpose |
|---|---|---|---|
transfer_id |
TEXT |
PK | The transfer id (hex). |
account_id |
BIGINT |
PK | An involved base account id. |
subaccount |
BIGINT |
PK | The involved subaccount code. |
(transfer_id, account_id, subaccount).idx_xfer_acct (account_id, subaccount).sagasWrite-ahead saga records for crash recovery. Owned by saga.rs (SagaStore).
See ADR-0002 (saga commit pipeline).
| Column | Type | Key | Purpose |
|---|---|---|---|
id |
BIGINT |
PK | Saga id (the reservation id). |
data |
TEXT |
The encoded PendingSaga record (hex). |
id.eventsThe append-only ledger event log, idempotent on the dedup key. Owned by
event.rs (EventStore). See ADR-0010 (event stream vs transfer
log).
| Column | Type | Key | Purpose |
|---|---|---|---|
seq |
BIGINT |
PK | Monotonic sequence number. |
timestamp |
BIGINT |
Unix millis of the event. | |
kind |
TEXT |
Event kind tag (JSON). | |
data |
TEXT |
The full LedgerEvent as JSON. |
|
dedup_key |
TEXT |
UNIQUE | Stable key; a replayed event returns the existing seq. |
seq. Unique: dedup_key.booksBook definitions (asset/account/flag policy) as JSON. Owned by book.rs
(BookStore). See ADR-0013 (journaling model).
| Column | Type | Key | Purpose |
|---|---|---|---|
id |
BIGINT |
PK | Book id. |
name |
TEXT |
Human-readable name. | |
data |
TEXT |
The Book (with its BookPolicy) as JSON. |
id.balance_projectionAppend-only balance cache points: each row snapshots one (account, subaccount,
asset) balance at a commit-time watermark. Rows are only inserted; a read picks
the highest-id row at or before a watermark. A derived, rebuildable accelerator,
never authoritative. Owned by projection.rs (BalanceProjectionStore). See
ADR-0019.
| Column | Type | Key | Purpose |
|---|---|---|---|
id |
BIGINT |
PK | Rust-minted monotonic id (tie-breaker for equal watermarks). |
account |
BIGINT |
Base account id. | |
subaccount |
BIGINT |
Subaccount code. | |
asset |
INTEGER |
Asset id. | |
balance |
TEXT |
The cached Cent as a decimal string. |
|
watermark |
BIGINT |
Commit-time watermark (unix millis) this snapshot covers. |
id.idx_balance_projection_closest (account, subaccount, asset, watermark, id)._migrationsThe applied-migration ledger. Created in migrate.rs (not a .sql file); a
migration whose name is present is skipped, making migrate() idempotent.
| Column | Type | Key | Purpose |
|---|---|---|---|
name |
TEXT |
PK | The migration name (e.g. 007_balance_projection). |
name.