Skip to main content

rustlavel_ledger/
schema.rs

1//! The five tables.
2
3use rustlavel_core::Result;
4use rustlavel_db::{Schema, Table};
5
6pub const ACCOUNTS: &str = "ledger_accounts";
7pub const TRANSFERS: &str = "ledger_transfers";
8pub const ENTRIES: &str = "ledger_entries";
9pub const HOLDS: &str = "ledger_holds";
10pub const LOTS: &str = "ledger_lots";
11
12pub fn define_accounts(t: &mut Table) {
13    t.id();
14    t.string("account_id").unique();
15    t.string("owner");
16    t.string("unit");
17    t.big_integer("balance").default_int(0);
18    t.big_integer("held").default_int(0);
19    t.big_integer("created_at");
20    // One account per owner per unit. The index is the rule.
21    t.unique(&["owner", "unit"]);
22}
23
24pub fn define_transfers(t: &mut Table) {
25    t.id();
26    t.string("transfer_id").unique();
27    // Unique: this index is idempotency. A second transfer under a reference
28    // already used cannot be inserted, whatever else is racing.
29    t.string("reference").unique();
30    t.string("kind");
31    t.string("from_account");
32    t.string("to_account");
33    t.big_integer("amount");
34    t.big_integer("created_at");
35    t.json("metadata").nullable();
36}
37
38pub fn define_entries(t: &mut Table) {
39    t.id();
40    t.string("transfer_id");
41    t.string("account_id");
42    t.big_integer("amount");
43    t.big_integer("balance_after");
44    t.string("kind");
45    t.string("reference");
46    t.big_integer("created_at");
47    t.index(&["account_id", "id"]);
48}
49
50pub fn define_holds(t: &mut Table) {
51    t.id();
52    t.string("hold_id").unique();
53    t.string("account_id");
54    t.big_integer("amount");
55    // This column is the lock: capture and release are
56    // `UPDATE … WHERE status = 'held'`, and one row changed means this call
57    // won.
58    t.string("status");
59    t.string("reference").unique();
60    t.big_integer("created_at");
61    t.big_integer("expires_at");
62    t.index(&["status", "expires_at"]);
63}
64
65pub fn define_lots(t: &mut Table) {
66    t.id();
67    t.string("lot_id").unique();
68    t.string("account_id");
69    t.string("transfer_id");
70    t.big_integer("initial");
71    t.big_integer("remaining");
72    // Null never expires.
73    t.big_integer("expires_at").nullable();
74    t.big_integer("created_at");
75    t.index(&["account_id", "expires_at"]);
76}
77
78pub async fn create_tables(schema: &Schema<'_>) -> Result<()> {
79    schema.create(ACCOUNTS, define_accounts).await?;
80    schema.create(TRANSFERS, define_transfers).await?;
81    schema.create(ENTRIES, define_entries).await?;
82    schema.create(HOLDS, define_holds).await?;
83    schema.create(LOTS, define_lots).await
84}
85
86pub async fn drop_tables(schema: &Schema<'_>) -> Result<()> {
87    schema.drop(LOTS).await?;
88    schema.drop(HOLDS).await?;
89    schema.drop(ENTRIES).await?;
90    schema.drop(TRANSFERS).await?;
91    schema.drop(ACCOUNTS).await
92}
93
94rustlavel_db::migration!(
95    CreateLedgerTables,
96    "2026_09_17_000100_create_ledger_tables",
97    up: |schema| { crate::schema::create_tables(schema).await },
98    down: |schema| { crate::schema::drop_tables(schema).await },
99);