lib.rs 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489
  1. //! SQL-backed Store implementation for SQLite and PostgreSQL.
  2. //!
  3. //! Uses `sqlx::Any` for database-agnostic queries. Enable features
  4. //! `sqlite` or `postgres` to select the backend.
  5. //!
  6. //! ```text
  7. //! let pool = sqlx::any::AnyPoolOptions::new()
  8. //! .connect("sqlite::memory:").await?;
  9. //! let store = SqlStore::new(pool);
  10. //! store.migrate().await?;
  11. //! ```
  12. use std::collections::{HashMap, HashSet};
  13. use std::str::FromStr;
  14. use std::sync::atomic::{AtomicU8, Ordering};
  15. use async_trait::async_trait;
  16. use sqlx::any::AnyRow;
  17. use sqlx::{Any, Pool, Row};
  18. use kuatia_storage::error::StoreError;
  19. use kuatia_storage::events::{EventStore, LedgerEvent, event_dedup_key};
  20. use kuatia_storage::query::{filter_transfers, paginate};
  21. use kuatia_storage::store::*;
  22. use kuatia_types::autoid::AutoId;
  23. use kuatia_types::*;
  24. // Cached backend kind for `SqlStore::backend`.
  25. const BACKEND_UNKNOWN: u8 = 0;
  26. const BACKEND_POSTGRES: u8 = 1;
  27. const BACKEND_SQLITE: u8 = 2;
  28. /// Row-locking clause appended to a `SELECT` on backends that support it
  29. /// (PostgreSQL). SQLite has no `FOR UPDATE` and serializes writers itself, so it
  30. /// gets an empty clause.
  31. const FOR_UPDATE: &str = " FOR UPDATE";
  32. /// SQL-backed [`Store`] implementation.
  33. pub struct SqlStore {
  34. pool: Pool<Any>,
  35. autoid: AutoId,
  36. /// Detected backend kind (lazily probed): one of `BACKEND_*`.
  37. backend: AtomicU8,
  38. }
  39. impl SqlStore {
  40. /// Create a new SQL store wrapping an existing connection pool.
  41. pub fn new(pool: Pool<Any>) -> Self {
  42. Self {
  43. pool,
  44. autoid: AutoId::new(),
  45. backend: AtomicU8::new(BACKEND_UNKNOWN),
  46. }
  47. }
  48. /// Whether the backend is PostgreSQL. Probed once and cached: `SELECT
  49. /// sqlite_version()` succeeds only on SQLite, so a failure means Postgres.
  50. async fn is_postgres(&self) -> Result<bool, StoreError> {
  51. match self.backend.load(Ordering::Relaxed) {
  52. BACKEND_POSTGRES => return Ok(true),
  53. BACKEND_SQLITE => return Ok(false),
  54. _ => {}
  55. }
  56. let is_sqlite = sqlx::query("SELECT sqlite_version()")
  57. .fetch_optional(&self.pool)
  58. .await
  59. .is_ok();
  60. self.backend.store(
  61. if is_sqlite {
  62. BACKEND_SQLITE
  63. } else {
  64. BACKEND_POSTGRES
  65. },
  66. Ordering::Relaxed,
  67. );
  68. Ok(!is_sqlite)
  69. }
  70. /// The row-locking clause for the current backend: [`FOR_UPDATE`] on
  71. /// Postgres, empty on SQLite.
  72. async fn lock_clause(&self) -> Result<&'static str, StoreError> {
  73. Ok(if self.is_postgres().await? {
  74. FOR_UPDATE
  75. } else {
  76. ""
  77. })
  78. }
  79. /// Run database migrations. Idempotent: a `_migrations` ledger records what
  80. /// has been applied, so re-running is a no-op. Every column is a text type,
  81. /// so the store holds no opaque binary and the DDL is identical for both
  82. /// backends. Content-addressed ids and opaque saga bytes are stored as hex
  83. /// `TEXT`, and JSON payloads as their `TEXT` serialization, keeping every
  84. /// row legible for auditing.
  85. pub async fn migrate(&self) -> Result<(), StoreError> {
  86. sqlx::query("CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY)")
  87. .execute(&self.pool)
  88. .await
  89. .map_err(|e| StoreError::Internal(e.to_string()))?;
  90. let migrations: &[(&str, &str)] = &[
  91. ("001_init", include_str!("migrations/001_init.sql")),
  92. (
  93. "002_subaccounts",
  94. include_str!("migrations/002_subaccounts.sql"),
  95. ),
  96. (
  97. "003_drop_user_data",
  98. include_str!("migrations/003_drop_user_data.sql"),
  99. ),
  100. (
  101. "004_index_tables",
  102. include_str!("migrations/004_index_tables.sql"),
  103. ),
  104. (
  105. "005_account_head",
  106. include_str!("migrations/005_account_head.sql"),
  107. ),
  108. (
  109. "006_drop_policy",
  110. include_str!("migrations/006_drop_policy.sql"),
  111. ),
  112. (
  113. "007_balance_projection",
  114. include_str!("migrations/007_balance_projection.sql"),
  115. ),
  116. ];
  117. for (name, sql) in migrations {
  118. let applied = sqlx::query("SELECT 1 FROM _migrations WHERE name = $1")
  119. .bind(*name)
  120. .fetch_optional(&self.pool)
  121. .await
  122. .map_err(|e| StoreError::Internal(e.to_string()))?;
  123. if applied.is_some() {
  124. continue;
  125. }
  126. // Apply every statement and record the migration in one transaction,
  127. // so a crash mid-migration rolls back cleanly and the migration is
  128. // retried as a whole. Migration 004 drops and rebuilds `postings`;
  129. // without the transaction a partial apply would leave the schema in a
  130. // state the migration cannot be re-run against. Both SQLite and
  131. // PostgreSQL support transactional DDL.
  132. let mut tx = self
  133. .pool
  134. .begin()
  135. .await
  136. .map_err(|e| StoreError::Internal(e.to_string()))?;
  137. for statement in sql.split(';') {
  138. let trimmed = statement.trim();
  139. if !trimmed.is_empty() {
  140. sqlx::query(trimmed)
  141. .execute(&mut *tx)
  142. .await
  143. .map_err(|e| StoreError::Internal(e.to_string()))?;
  144. }
  145. }
  146. sqlx::query("INSERT INTO _migrations (name) VALUES ($1)")
  147. .bind(*name)
  148. .execute(&mut *tx)
  149. .await
  150. .map_err(|e| StoreError::Internal(e.to_string()))?;
  151. tx.commit()
  152. .await
  153. .map_err(|e| StoreError::Internal(e.to_string()))?;
  154. }
  155. Ok(())
  156. }
  157. }
  158. // ---------------------------------------------------------------------------
  159. // Serialization helpers
  160. // ---------------------------------------------------------------------------
  161. /// Serialize a value to a JSON string. Payload columns store JSON as `TEXT` so
  162. /// the database is directly readable for auditing; the ledger never queries
  163. /// into the JSON, so no binary or indexed representation is needed.
  164. fn serialize_json<T: serde::Serialize>(val: &T) -> Result<String, StoreError> {
  165. serde_json::to_string(val).map_err(|e| StoreError::Internal(format!("json serialization: {e}")))
  166. }
  167. fn deserialize_json<T: serde::de::DeserializeOwned>(s: &str) -> Result<T, StoreError> {
  168. serde_json::from_str(s).map_err(|e| StoreError::Internal(format!("bad json: {e}")))
  169. }
  170. /// Lower-case hex encoding. Binary identifiers (content-addressed hashes) and
  171. /// opaque saga bytes are stored as hex `TEXT` so a row is legible in any SQL
  172. /// client and matches the hex form used in logs and `Debug` output.
  173. fn to_hex(bytes: &[u8]) -> String {
  174. const HEX: &[u8; 16] = b"0123456789abcdef";
  175. let mut s = String::with_capacity(bytes.len() * 2);
  176. for &b in bytes {
  177. s.push(HEX[(b >> 4) as usize] as char);
  178. s.push(HEX[(b & 0x0f) as usize] as char);
  179. }
  180. s
  181. }
  182. fn from_hex(s: &str) -> Result<Vec<u8>, StoreError> {
  183. if s.len() % 2 != 0 {
  184. return Err(StoreError::Internal(format!("odd-length hex: {s:?}")));
  185. }
  186. (0..s.len())
  187. .step_by(2)
  188. .map(|i| {
  189. u8::from_str_radix(&s[i..i + 2], 16)
  190. .map_err(|e| StoreError::Internal(format!("bad hex: {e}")))
  191. })
  192. .collect()
  193. }
  194. fn envelope_id_to_hex(id: &EnvelopeId) -> String {
  195. to_hex(&id.0)
  196. }
  197. fn envelope_id_from_hex(s: &str) -> Result<EnvelopeId, StoreError> {
  198. let bytes = from_hex(s)?;
  199. let arr: [u8; 32] = bytes.as_slice().try_into().map_err(|_| {
  200. StoreError::Internal(format!("expected 32-byte id, got {} bytes", bytes.len()))
  201. })?;
  202. Ok(EnvelopeId(arr))
  203. }
  204. fn row_to_account(row: &AnyRow) -> Result<Account, StoreError> {
  205. let id: i64 = row
  206. .try_get("id")
  207. .map_err(|e| StoreError::Internal(e.to_string()))?;
  208. let subaccount: i64 = row
  209. .try_get("subaccount")
  210. .map_err(|e| StoreError::Internal(e.to_string()))?;
  211. let version: i64 = row
  212. .try_get("version")
  213. .map_err(|e| StoreError::Internal(e.to_string()))?;
  214. let flags_bits: i32 = row
  215. .try_get("flags")
  216. .map_err(|e| StoreError::Internal(e.to_string()))?;
  217. let book: i64 = row
  218. .try_get("book")
  219. .map_err(|e| StoreError::Internal(e.to_string()))?;
  220. let metadata_json: String = row
  221. .try_get("metadata")
  222. .map_err(|e| StoreError::Internal(e.to_string()))?;
  223. Ok(Account {
  224. id: AccountId::with_sub(id, subaccount),
  225. version: version as u64,
  226. flags: AccountFlags::from_bits_truncate(flags_bits as u32),
  227. book: BookId::new(book),
  228. metadata: deserialize_json(&metadata_json)?,
  229. })
  230. }
  231. fn row_to_posting(row: &AnyRow) -> Result<Posting, StoreError> {
  232. let transfer_id: String = row
  233. .try_get("transfer_id")
  234. .map_err(|e| StoreError::Internal(e.to_string()))?;
  235. let idx: i16 = row
  236. .try_get("idx")
  237. .map_err(|e| StoreError::Internal(e.to_string()))?;
  238. let owner: i64 = row
  239. .try_get("owner")
  240. .map_err(|e| StoreError::Internal(e.to_string()))?;
  241. let subaccount: i64 = row
  242. .try_get("subaccount")
  243. .map_err(|e| StoreError::Internal(e.to_string()))?;
  244. let asset: i32 = row
  245. .try_get("asset")
  246. .map_err(|e| StoreError::Internal(e.to_string()))?;
  247. let value: String = row
  248. .try_get("value")
  249. .map_err(|e| StoreError::Internal(e.to_string()))?;
  250. let value = Cent::from_str(&value).map_err(|e| StoreError::Internal(e.to_string()))?;
  251. Ok(Posting {
  252. id: PostingId {
  253. transfer: envelope_id_from_hex(&transfer_id)?,
  254. index: idx as u16,
  255. },
  256. owner: AccountId::with_sub(owner, subaccount),
  257. asset: AssetId::new(asset as u32),
  258. value,
  259. })
  260. }
  261. /// The FROM source for a posting read of the given derived state. Each index
  262. /// table carries a full row copy, so the live-set reads target the index table
  263. /// directly with no merge back to the immutable `postings` record. `Live` is a
  264. /// `UNION ALL` of the two disjoint live sets (the shared 6 data columns), still
  265. /// with no join to history. Portable across SQLite and PostgreSQL.
  266. fn filter_source(filter: PostingFilter) -> &'static str {
  267. match filter {
  268. PostingFilter::Active => "active_postings",
  269. PostingFilter::Reserved => "reserved_postings",
  270. PostingFilter::All => "postings",
  271. PostingFilter::Live => {
  272. "(SELECT transfer_id, idx, owner, subaccount, asset, value FROM active_postings \
  273. UNION ALL \
  274. SELECT transfer_id, idx, owner, subaccount, asset, value FROM reserved_postings) AS live"
  275. }
  276. }
  277. }
  278. /// Maximum posting ids matched by a single statement. `id_predicate` expands to
  279. /// an `OR` of `n` equality pairs, so the binding constraint is SQLite's
  280. /// expression-tree depth limit (`SQLITE_MAX_EXPR_DEPTH`, default 1000), which a
  281. /// chain of `n` `OR`s reaches at roughly `n` deep. It caps well before the
  282. /// bind-parameter limits (SQLite 32766, PostgreSQL 65535) that `2 * n (+1)`
  283. /// parameters would hit. `500` stays comfortably under the expression-depth
  284. /// limit; callers that pass more ids are chunked, so the id-batch primitives
  285. /// have no practical ceiling on batch size.
  286. const MAX_IDS_PER_QUERY: usize = 500;
  287. /// Build a portable predicate matching a set of posting ids:
  288. /// `(transfer_id = $s AND idx = $s+1) OR (transfer_id = $s+2 AND idx = $s+3) ...`
  289. /// starting at placeholder `$start`. Row-value `IN ((a, b), ...)` is not
  290. /// portable across SQLite and PostgreSQL; an `OR` of equality pairs is. The
  291. /// caller binds each id as `(hex(transfer), idx as i16)` in order, matching the
  292. /// placeholder sequence. `ids` must be non-empty and no longer than
  293. /// [`MAX_IDS_PER_QUERY`]; larger sets are split into chunks by the caller.
  294. fn id_predicate(count: usize, start: u32) -> String {
  295. (0..count)
  296. .map(|i| {
  297. let p = start + (i as u32) * 2;
  298. format!("(transfer_id = ${} AND idx = ${})", p, p + 1)
  299. })
  300. .collect::<Vec<_>>()
  301. .join(" OR ")
  302. }
  303. // ---------------------------------------------------------------------------
  304. // AccountStore
  305. // ---------------------------------------------------------------------------
  306. #[async_trait]
  307. impl AccountStore for SqlStore {
  308. async fn get_account(&self, id: &AccountId) -> Result<Account, StoreError> {
  309. // The head points at the current version, so this is a single indexed
  310. // lookup into the immutable history — no scan of the version chain.
  311. let row = sqlx::query(
  312. "SELECT a.* FROM accounts a \
  313. JOIN account_head h \
  314. ON h.id = a.id AND h.subaccount = a.subaccount AND h.version = a.version \
  315. WHERE h.id = $1 AND h.subaccount = $2",
  316. )
  317. .bind(id.id)
  318. .bind(id.sub)
  319. .fetch_optional(&self.pool)
  320. .await
  321. .map_err(|e| StoreError::Internal(e.to_string()))?
  322. .ok_or_else(|| StoreError::NotFound(format!("account {id:?}")))?;
  323. row_to_account(&row)
  324. }
  325. async fn get_accounts(&self, ids: &[AccountId]) -> Result<Vec<Account>, StoreError> {
  326. let mut result = Vec::with_capacity(ids.len());
  327. for id in ids {
  328. result.push(self.get_account(id).await?);
  329. }
  330. Ok(result)
  331. }
  332. async fn create_account(&self, account: Account) -> Result<u64, StoreError> {
  333. // Pessimistic locking: inside one transaction, lock the account's head
  334. // row with `SELECT ... FOR UPDATE` so a concurrent creator waits. The
  335. // head is the single row per account; its `ON CONFLICT (id, subaccount)
  336. // DO NOTHING` insert is the portable backstop that decides the winner
  337. // (SQLite has no `FOR UPDATE`, and it turns a concurrent double-create
  338. // into a clean affected-row count instead of a unique violation).
  339. let lock = self.lock_clause().await?;
  340. let mut tx = self
  341. .pool
  342. .begin()
  343. .await
  344. .map_err(|e| StoreError::Internal(e.to_string()))?;
  345. let existing = sqlx::query(&format!(
  346. "SELECT 1 FROM account_head WHERE id = $1 AND subaccount = $2 LIMIT 1{lock}"
  347. ))
  348. .bind(account.id.id)
  349. .bind(account.id.sub)
  350. .fetch_optional(&mut *tx)
  351. .await
  352. .map_err(|e| StoreError::Internal(e.to_string()))?;
  353. if existing.is_some() {
  354. return Ok(0);
  355. }
  356. // Append the immutable first version, then point the head at it.
  357. sqlx::query(
  358. "INSERT INTO accounts (id, subaccount, version, flags, book, metadata) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id, subaccount, version) DO NOTHING"
  359. )
  360. .bind(account.id.id)
  361. .bind(account.id.sub)
  362. .bind(account.version as i64)
  363. .bind(account.flags.bits() as i32)
  364. .bind(account.book.0)
  365. .bind(serialize_json(&account.metadata)?)
  366. .execute(&mut *tx)
  367. .await
  368. .map_err(|e| StoreError::Internal(e.to_string()))?;
  369. let res = sqlx::query(
  370. "INSERT INTO account_head (id, subaccount, version) VALUES ($1, $2, $3) ON CONFLICT (id, subaccount) DO NOTHING",
  371. )
  372. .bind(account.id.id)
  373. .bind(account.id.sub)
  374. .bind(account.version as i64)
  375. .execute(&mut *tx)
  376. .await
  377. .map_err(|e| StoreError::Internal(e.to_string()))?;
  378. if res.rows_affected() == 0 {
  379. return Ok(0);
  380. }
  381. tx.commit()
  382. .await
  383. .map_err(|e| StoreError::Internal(e.to_string()))?;
  384. Ok(1)
  385. }
  386. async fn append_account_version(&self, account: Account) -> Result<u64, StoreError> {
  387. // Pessimistic locking: inside one transaction, lock the account's head
  388. // row with `SELECT ... FOR UPDATE` so a concurrent appender waits here
  389. // until we commit, then check the version, append the new immutable row,
  390. // and move the head. `ON CONFLICT` is the portable backstop (SQLite has
  391. // no `FOR UPDATE`, and it covers the append phantom-insert a row lock
  392. // does not). The head is maintained by delete + insert, never `UPDATE`,
  393. // so the write path issues only inserts and deletes.
  394. let lock = self.lock_clause().await?;
  395. let mut tx = self
  396. .pool
  397. .begin()
  398. .await
  399. .map_err(|e| StoreError::Internal(e.to_string()))?;
  400. // A guarded write: no such account, or a version that is not exactly one
  401. // past the head, matches nothing and reports 0. This is what keeps the
  402. // chain gap-free (a stale or skipped version never lands) and makes a
  403. // replay of an already-applied version a no-op.
  404. let current = sqlx::query(&format!(
  405. "SELECT version FROM account_head WHERE id = $1 AND subaccount = $2{lock}"
  406. ))
  407. .bind(account.id.id)
  408. .bind(account.id.sub)
  409. .fetch_optional(&mut *tx)
  410. .await
  411. .map_err(|e| StoreError::Internal(e.to_string()))?;
  412. let Some(current) = current else {
  413. return Ok(0);
  414. };
  415. let current_version: i64 = current
  416. .try_get("version")
  417. .map_err(|e| StoreError::Internal(e.to_string()))?;
  418. let expected = current_version
  419. .checked_add(1)
  420. .ok_or_else(|| StoreError::Internal("account version overflow".to_string()))?;
  421. if account.version as i64 != expected {
  422. return Ok(0);
  423. }
  424. let res = sqlx::query(
  425. "INSERT INTO accounts (id, subaccount, version, flags, book, metadata) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id, subaccount, version) DO NOTHING"
  426. )
  427. .bind(account.id.id)
  428. .bind(account.id.sub)
  429. .bind(account.version as i64)
  430. .bind(account.flags.bits() as i32)
  431. .bind(account.book.0)
  432. .bind(serialize_json(&account.metadata)?)
  433. .execute(&mut *tx)
  434. .await
  435. .map_err(|e| StoreError::Internal(e.to_string()))?;
  436. if res.rows_affected() == 0 {
  437. return Ok(0);
  438. }
  439. // Move the head to the new version (delete + insert, never update).
  440. sqlx::query("DELETE FROM account_head WHERE id = $1 AND subaccount = $2")
  441. .bind(account.id.id)
  442. .bind(account.id.sub)
  443. .execute(&mut *tx)
  444. .await
  445. .map_err(|e| StoreError::Internal(e.to_string()))?;
  446. sqlx::query("INSERT INTO account_head (id, subaccount, version) VALUES ($1, $2, $3)")
  447. .bind(account.id.id)
  448. .bind(account.id.sub)
  449. .bind(account.version as i64)
  450. .execute(&mut *tx)
  451. .await
  452. .map_err(|e| StoreError::Internal(e.to_string()))?;
  453. tx.commit()
  454. .await
  455. .map_err(|e| StoreError::Internal(e.to_string()))?;
  456. Ok(1)
  457. }
  458. async fn get_account_history(&self, id: &AccountId) -> Result<Vec<Account>, StoreError> {
  459. let rows = sqlx::query(
  460. "SELECT * FROM accounts WHERE id = $1 AND subaccount = $2 ORDER BY version ASC",
  461. )
  462. .bind(id.id)
  463. .bind(id.sub)
  464. .fetch_all(&self.pool)
  465. .await
  466. .map_err(|e| StoreError::Internal(e.to_string()))?;
  467. if rows.is_empty() {
  468. return Err(StoreError::NotFound(format!("account {id:?}")));
  469. }
  470. rows.iter().map(row_to_account).collect()
  471. }
  472. async fn list_accounts(&self) -> Result<Vec<Account>, StoreError> {
  473. // One row per account via the head; no read-all-versions + dedup.
  474. let rows = sqlx::query(
  475. "SELECT a.* FROM accounts a \
  476. JOIN account_head h \
  477. ON h.id = a.id AND h.subaccount = a.subaccount AND h.version = a.version",
  478. )
  479. .fetch_all(&self.pool)
  480. .await
  481. .map_err(|e| StoreError::Internal(e.to_string()))?;
  482. rows.iter().map(row_to_account).collect()
  483. }
  484. }
  485. // ---------------------------------------------------------------------------
  486. // PostingStore
  487. // ---------------------------------------------------------------------------
  488. #[async_trait]
  489. impl PostingStore for SqlStore {
  490. async fn get_postings(&self, ids: &[PostingId]) -> Result<Vec<Posting>, StoreError> {
  491. if ids.is_empty() {
  492. return Ok(Vec::new());
  493. }
  494. // Set-based query per chunk instead of one probe per id, reusing the
  495. // portable `id_predicate` and binding each id in order as
  496. // `(hex(transfer), idx as i16)`. Chunked so a large batch never exceeds
  497. // the backend's bind-parameter limit (see `MAX_IDS_PER_QUERY`).
  498. let mut found: HashMap<(String, i16), Posting> = HashMap::with_capacity(ids.len());
  499. for chunk in ids.chunks(MAX_IDS_PER_QUERY) {
  500. let sql = format!(
  501. "SELECT * FROM postings WHERE {}",
  502. id_predicate(chunk.len(), 1)
  503. );
  504. let mut q = sqlx::query(&sql);
  505. for id in chunk {
  506. q = q
  507. .bind(envelope_id_to_hex(&id.transfer))
  508. .bind(id.index as i16);
  509. }
  510. let rows = q
  511. .fetch_all(&self.pool)
  512. .await
  513. .map_err(|e| StoreError::Internal(e.to_string()))?;
  514. // Index the fetched postings by the same `(hex, idx)` key that was bound.
  515. for row in &rows {
  516. let posting = row_to_posting(row)?;
  517. let key = (
  518. envelope_id_to_hex(&posting.id.transfer),
  519. posting.id.index as i16,
  520. );
  521. found.insert(key, posting);
  522. }
  523. }
  524. // Return in input order, erroring on the first id absent from the batch
  525. // (matching the per-id lookup's `NotFound` semantics).
  526. let mut result = Vec::with_capacity(ids.len());
  527. for id in ids {
  528. let key = (envelope_id_to_hex(&id.transfer), id.index as i16);
  529. let posting = found
  530. .get(&key)
  531. .ok_or_else(|| StoreError::NotFound(format!("posting {id:?}")))?;
  532. result.push(posting.clone());
  533. }
  534. Ok(result)
  535. }
  536. async fn get_postings_by_account(
  537. &self,
  538. id: i64,
  539. sub: Option<i64>,
  540. asset: Option<&AssetId>,
  541. filter: PostingFilter,
  542. ) -> Result<Vec<Posting>, StoreError> {
  543. // Build the predicate dynamically: `sub == None` spans every subaccount
  544. // of `id`, `Some(s)` restricts to one. The subaccount is compared only
  545. // for equality, never as a magnitude. The derived-state filter selects
  546. // which table (index copy or immutable record) to read from directly.
  547. let mut sql = format!("SELECT * FROM {} WHERE owner = $1", filter_source(filter));
  548. let mut placeholder = 2u32;
  549. if sub.is_some() {
  550. sql.push_str(&format!(" AND subaccount = ${placeholder}"));
  551. placeholder += 1;
  552. }
  553. if asset.is_some() {
  554. sql.push_str(&format!(" AND asset = ${placeholder}"));
  555. }
  556. // Deterministic order by the posting primary key, matching
  557. // `query_postings`, so callers (and pagination built on top) see a
  558. // stable sequence.
  559. sql.push_str(" ORDER BY transfer_id, idx");
  560. let mut q = sqlx::query(&sql).bind(id);
  561. if let Some(s) = sub {
  562. q = q.bind(s);
  563. }
  564. if let Some(a) = asset {
  565. q = q.bind(a.0 as i32);
  566. }
  567. let rows = q
  568. .fetch_all(&self.pool)
  569. .await
  570. .map_err(|e| StoreError::Internal(e.to_string()))?;
  571. rows.iter().map(row_to_posting).collect()
  572. }
  573. async fn get_posting_states(&self, ids: &[PostingId]) -> Result<Vec<PostingState>, StoreError> {
  574. if ids.is_empty() {
  575. return Ok(Vec::new());
  576. }
  577. // One set-based query per state table instead of up to three probes per
  578. // id, reusing the portable `id_predicate` (an OR of equality pairs;
  579. // row-value `IN` is not portable across SQLite and PostgreSQL) and
  580. // binding every id in order as `(hex(transfer), idx as i16)`. Chunked so
  581. // a large batch never exceeds the bind-parameter limit.
  582. // Key membership by the same `(hex, idx)` values that were bound, so the
  583. // per-id lookup below matches without decoding transfer ids back.
  584. let row_key = |row: &AnyRow| -> Result<(String, i16), StoreError> {
  585. let transfer_id: String = row
  586. .try_get("transfer_id")
  587. .map_err(|e| StoreError::Internal(e.to_string()))?;
  588. let idx: i16 = row
  589. .try_get("idx")
  590. .map_err(|e| StoreError::Internal(e.to_string()))?;
  591. Ok((transfer_id, idx))
  592. };
  593. let mut active: HashSet<(String, i16)> = HashSet::new();
  594. let mut reserved: HashMap<(String, i16), i64> = HashMap::new();
  595. let mut spent: HashSet<(String, i16)> = HashSet::new();
  596. for chunk in ids.chunks(MAX_IDS_PER_QUERY) {
  597. let predicate = id_predicate(chunk.len(), 1);
  598. let active_sql =
  599. format!("SELECT transfer_id, idx FROM active_postings WHERE {predicate}");
  600. let mut active_q = sqlx::query(&active_sql);
  601. for id in chunk {
  602. active_q = active_q
  603. .bind(envelope_id_to_hex(&id.transfer))
  604. .bind(id.index as i16);
  605. }
  606. let active_rows = active_q
  607. .fetch_all(&self.pool)
  608. .await
  609. .map_err(|e| StoreError::Internal(e.to_string()))?;
  610. for row in &active_rows {
  611. active.insert(row_key(row)?);
  612. }
  613. let reserved_sql = format!(
  614. "SELECT transfer_id, idx, reservation FROM reserved_postings WHERE {predicate}"
  615. );
  616. let mut reserved_q = sqlx::query(&reserved_sql);
  617. for id in chunk {
  618. reserved_q = reserved_q
  619. .bind(envelope_id_to_hex(&id.transfer))
  620. .bind(id.index as i16);
  621. }
  622. let reserved_rows = reserved_q
  623. .fetch_all(&self.pool)
  624. .await
  625. .map_err(|e| StoreError::Internal(e.to_string()))?;
  626. for row in &reserved_rows {
  627. let rid: i64 = row
  628. .try_get("reservation")
  629. .map_err(|e| StoreError::Internal(e.to_string()))?;
  630. reserved.insert(row_key(row)?, rid);
  631. }
  632. let spent_sql = format!("SELECT transfer_id, idx FROM postings WHERE {predicate}");
  633. let mut spent_q = sqlx::query(&spent_sql);
  634. for id in chunk {
  635. spent_q = spent_q
  636. .bind(envelope_id_to_hex(&id.transfer))
  637. .bind(id.index as i16);
  638. }
  639. let spent_rows = spent_q
  640. .fetch_all(&self.pool)
  641. .await
  642. .map_err(|e| StoreError::Internal(e.to_string()))?;
  643. for row in &spent_rows {
  644. spent.insert(row_key(row)?);
  645. }
  646. }
  647. // Reconstruct each id's state in input order, preserving the active >
  648. // reserved > spent > missing precedence of the original probes.
  649. let mut out = Vec::with_capacity(ids.len());
  650. for id in ids {
  651. let key = (envelope_id_to_hex(&id.transfer), id.index as i16);
  652. out.push(if active.contains(&key) {
  653. PostingState::Active
  654. } else if let Some(rid) = reserved.get(&key) {
  655. PostingState::Reserved(ReservationId::new(*rid))
  656. } else if spent.contains(&key) {
  657. PostingState::Spent
  658. } else {
  659. PostingState::Missing
  660. });
  661. }
  662. Ok(out)
  663. }
  664. async fn query_postings(&self, query: &PostingQuery) -> Result<Page<Posting>, StoreError> {
  665. let (where_clause, count_clause) = {
  666. let source = filter_source(query.filter);
  667. let mut w = String::from("WHERE owner = $1");
  668. let mut idx = 2u32;
  669. if query.sub.is_some() {
  670. w.push_str(&format!(" AND subaccount = ${idx}"));
  671. idx += 1;
  672. }
  673. if query.asset.is_some() {
  674. w.push_str(&format!(" AND asset = ${idx}"));
  675. }
  676. let c = format!("SELECT COUNT(*) as cnt FROM {source} {w}");
  677. let limit = query.limit.unwrap_or(u32::MAX);
  678. let offset = query.offset.unwrap_or(0);
  679. // Order by the posting primary key so pagination is deterministic:
  680. // without it LIMIT/OFFSET could skip or repeat rows across pages,
  681. // especially for `Live`, whose source is a `UNION ALL` with no
  682. // inherent order.
  683. w.push_str(&format!(
  684. " ORDER BY transfer_id, idx LIMIT {limit} OFFSET {offset}"
  685. ));
  686. (format!("SELECT * FROM {source} {w}"), c)
  687. };
  688. // Build count query
  689. let mut count_q = sqlx::query(&count_clause).bind(query.account);
  690. if let Some(s) = query.sub {
  691. count_q = count_q.bind(s);
  692. }
  693. if let Some(ref a) = query.asset {
  694. count_q = count_q.bind(a.0 as i32);
  695. }
  696. let count_row = count_q
  697. .fetch_one(&self.pool)
  698. .await
  699. .map_err(|e| StoreError::Internal(e.to_string()))?;
  700. let total: i64 = count_row
  701. .try_get("cnt")
  702. .map_err(|e| StoreError::Internal(e.to_string()))?;
  703. // Build data query
  704. let mut data_q = sqlx::query(&where_clause).bind(query.account);
  705. if let Some(s) = query.sub {
  706. data_q = data_q.bind(s);
  707. }
  708. if let Some(ref a) = query.asset {
  709. data_q = data_q.bind(a.0 as i32);
  710. }
  711. let rows = data_q
  712. .fetch_all(&self.pool)
  713. .await
  714. .map_err(|e| StoreError::Internal(e.to_string()))?;
  715. let items: Vec<Posting> = rows.iter().map(row_to_posting).collect::<Result<_, _>>()?;
  716. Ok(Page {
  717. items,
  718. total: total as u64,
  719. })
  720. }
  721. async fn reserve_postings(
  722. &self,
  723. ids: &[PostingId],
  724. reservation: ReservationId,
  725. ) -> Result<u64, StoreError> {
  726. // Dumb instruction over the whole id set, in two statements: copy the
  727. // currently-active rows into the reserved index (sourced from
  728. // `active_postings`, so only active ids move), then delete those same
  729. // ids from `active_postings`. The DELETE's affected count is the number
  730. // claimed, and by active/reserved disjointness it equals the INSERT's
  731. // row count. Concurrent reserves serialize on the reserved-index primary
  732. // key, so exactly one wins each contended id.
  733. if ids.is_empty() {
  734. return Ok(0);
  735. }
  736. let mut tx = self
  737. .pool
  738. .begin()
  739. .await
  740. .map_err(|e| StoreError::Internal(e.to_string()))?;
  741. // Chunked so a large id set stays under the bind-parameter limit; all
  742. // chunks share one transaction so the whole claim is atomic.
  743. let mut claimed: u64 = 0;
  744. for chunk in ids.chunks(MAX_IDS_PER_QUERY) {
  745. // Reservation is $1; each id pair follows starting at $2.
  746. let insert_sql = format!(
  747. "INSERT INTO reserved_postings (transfer_id, idx, owner, subaccount, asset, value, reservation) \
  748. SELECT transfer_id, idx, owner, subaccount, asset, value, $1 FROM active_postings WHERE {} \
  749. ON CONFLICT (transfer_id, idx) DO NOTHING",
  750. id_predicate(chunk.len(), 2)
  751. );
  752. let mut insert_q = sqlx::query(&insert_sql).bind(reservation.0);
  753. for id in chunk {
  754. insert_q = insert_q
  755. .bind(envelope_id_to_hex(&id.transfer))
  756. .bind(id.index as i16);
  757. }
  758. insert_q
  759. .execute(&mut *tx)
  760. .await
  761. .map_err(|e| StoreError::Internal(e.to_string()))?;
  762. let delete_sql = format!(
  763. "DELETE FROM active_postings WHERE {}",
  764. id_predicate(chunk.len(), 1)
  765. );
  766. let mut delete_q = sqlx::query(&delete_sql);
  767. for id in chunk {
  768. delete_q = delete_q
  769. .bind(envelope_id_to_hex(&id.transfer))
  770. .bind(id.index as i16);
  771. }
  772. let del = delete_q
  773. .execute(&mut *tx)
  774. .await
  775. .map_err(|e| StoreError::Internal(e.to_string()))?;
  776. claimed += del.rows_affected();
  777. }
  778. tx.commit()
  779. .await
  780. .map_err(|e| StoreError::Internal(e.to_string()))?;
  781. Ok(claimed)
  782. }
  783. async fn release_postings(
  784. &self,
  785. ids: &[PostingId],
  786. reservation: ReservationId,
  787. ) -> Result<u64, StoreError> {
  788. // Dumb instruction over the whole id set: copy the rows reserved by
  789. // `reservation` back into the active index, then delete them from the
  790. // reserved index. The DELETE's affected count is the number released; an
  791. // id already active or reserved by another saga does not match.
  792. if ids.is_empty() {
  793. return Ok(0);
  794. }
  795. let mut tx = self
  796. .pool
  797. .begin()
  798. .await
  799. .map_err(|e| StoreError::Internal(e.to_string()))?;
  800. // Chunked so a large id set stays under the bind-parameter limit; all
  801. // chunks share one transaction.
  802. let mut released: u64 = 0;
  803. for chunk in ids.chunks(MAX_IDS_PER_QUERY) {
  804. // Reservation is $1; each id pair follows starting at $2.
  805. let insert_sql = format!(
  806. "INSERT INTO active_postings (transfer_id, idx, owner, subaccount, asset, value) \
  807. SELECT transfer_id, idx, owner, subaccount, asset, value FROM reserved_postings \
  808. WHERE ({}) AND reservation = $1 ON CONFLICT (transfer_id, idx) DO NOTHING",
  809. id_predicate(chunk.len(), 2)
  810. );
  811. let mut insert_q = sqlx::query(&insert_sql).bind(reservation.0);
  812. for id in chunk {
  813. insert_q = insert_q
  814. .bind(envelope_id_to_hex(&id.transfer))
  815. .bind(id.index as i16);
  816. }
  817. insert_q
  818. .execute(&mut *tx)
  819. .await
  820. .map_err(|e| StoreError::Internal(e.to_string()))?;
  821. let delete_sql = format!(
  822. "DELETE FROM reserved_postings WHERE ({}) AND reservation = $1",
  823. id_predicate(chunk.len(), 2)
  824. );
  825. let mut delete_q = sqlx::query(&delete_sql).bind(reservation.0);
  826. for id in chunk {
  827. delete_q = delete_q
  828. .bind(envelope_id_to_hex(&id.transfer))
  829. .bind(id.index as i16);
  830. }
  831. let del = delete_q
  832. .execute(&mut *tx)
  833. .await
  834. .map_err(|e| StoreError::Internal(e.to_string()))?;
  835. released += del.rows_affected();
  836. }
  837. tx.commit()
  838. .await
  839. .map_err(|e| StoreError::Internal(e.to_string()))?;
  840. Ok(released)
  841. }
  842. async fn deactivate_postings(
  843. &self,
  844. ids: &[PostingId],
  845. reservation: Option<ReservationId>,
  846. ) -> Result<u64, StoreError> {
  847. // Dumb instruction over the whole id set: a DELETE removes the ids from
  848. // an index so they become spent (present only in the immutable table).
  849. // `rows_affected` is the count; the caller interprets a shortfall.
  850. // Chunked under one transaction so a large id set stays within the
  851. // bind-parameter limit while the removal stays atomic.
  852. if ids.is_empty() {
  853. return Ok(0);
  854. }
  855. let mut tx = self
  856. .pool
  857. .begin()
  858. .await
  859. .map_err(|e| StoreError::Internal(e.to_string()))?;
  860. let mut removed: u64 = 0;
  861. for chunk in ids.chunks(MAX_IDS_PER_QUERY) {
  862. let (sql, rid) = match reservation {
  863. // Raw path: remove from the active index.
  864. None => (
  865. format!(
  866. "DELETE FROM active_postings WHERE {}",
  867. id_predicate(chunk.len(), 1)
  868. ),
  869. None,
  870. ),
  871. // Saga path: remove only the rows reserved by `rid`.
  872. Some(rid) => (
  873. format!(
  874. "DELETE FROM reserved_postings WHERE ({}) AND reservation = $1",
  875. id_predicate(chunk.len(), 2)
  876. ),
  877. Some(rid),
  878. ),
  879. };
  880. let mut q = sqlx::query(&sql);
  881. if let Some(rid) = rid {
  882. q = q.bind(rid.0);
  883. }
  884. for id in chunk {
  885. q = q
  886. .bind(envelope_id_to_hex(&id.transfer))
  887. .bind(id.index as i16);
  888. }
  889. let res = q
  890. .execute(&mut *tx)
  891. .await
  892. .map_err(|e| StoreError::Internal(e.to_string()))?;
  893. removed += res.rows_affected();
  894. }
  895. tx.commit()
  896. .await
  897. .map_err(|e| StoreError::Internal(e.to_string()))?;
  898. Ok(removed)
  899. }
  900. async fn insert_postings(&self, postings: &[Posting]) -> Result<u64, StoreError> {
  901. // Dumb instruction: insert each posting into the immutable table and, only
  902. // when the row was newly inserted, add its id to the active index. Return
  903. // the count of immutable rows inserted. The newness gate stops a replayed
  904. // finalize from re-activating a since-spent posting.
  905. let mut tx = self
  906. .pool
  907. .begin()
  908. .await
  909. .map_err(|e| StoreError::Internal(e.to_string()))?;
  910. let mut inserted: u64 = 0;
  911. for posting in postings {
  912. let hex = envelope_id_to_hex(&posting.id.transfer);
  913. let res = sqlx::query(
  914. "INSERT INTO postings (transfer_id, idx, owner, subaccount, asset, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (transfer_id, idx) DO NOTHING"
  915. )
  916. .bind(hex.clone())
  917. .bind(posting.id.index as i16)
  918. .bind(posting.owner.id)
  919. .bind(posting.owner.sub)
  920. .bind(posting.asset.0 as i32)
  921. .bind(posting.value.to_string())
  922. .execute(&mut *tx)
  923. .await
  924. .map_err(|e| StoreError::Internal(e.to_string()))?;
  925. if res.rows_affected() == 1 {
  926. // Activate a full copy so spendable reads never merge.
  927. sqlx::query(
  928. "INSERT INTO active_postings (transfer_id, idx, owner, subaccount, asset, value) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (transfer_id, idx) DO NOTHING",
  929. )
  930. .bind(hex)
  931. .bind(posting.id.index as i16)
  932. .bind(posting.owner.id)
  933. .bind(posting.owner.sub)
  934. .bind(posting.asset.0 as i32)
  935. .bind(posting.value.to_string())
  936. .execute(&mut *tx)
  937. .await
  938. .map_err(|e| StoreError::Internal(e.to_string()))?;
  939. inserted += 1;
  940. }
  941. }
  942. tx.commit()
  943. .await
  944. .map_err(|e| StoreError::Internal(e.to_string()))?;
  945. Ok(inserted)
  946. }
  947. }
  948. // ---------------------------------------------------------------------------
  949. // TransferStore
  950. // ---------------------------------------------------------------------------
  951. #[async_trait]
  952. impl TransferStore for SqlStore {
  953. async fn get_transfer(&self, id: &EnvelopeId) -> Result<Option<EnvelopeRecord>, StoreError> {
  954. let row = sqlx::query("SELECT transfer, receipt, created_at FROM transfers WHERE id = $1")
  955. .bind(envelope_id_to_hex(id))
  956. .fetch_optional(&self.pool)
  957. .await
  958. .map_err(|e| StoreError::Internal(e.to_string()))?;
  959. match row {
  960. None => Ok(None),
  961. Some(row) => {
  962. let transfer_json: String = row
  963. .try_get("transfer")
  964. .map_err(|e| StoreError::Internal(e.to_string()))?;
  965. let receipt_json: String = row
  966. .try_get("receipt")
  967. .map_err(|e| StoreError::Internal(e.to_string()))?;
  968. let created_at: i64 = row
  969. .try_get("created_at")
  970. .map_err(|e| StoreError::Internal(e.to_string()))?;
  971. Ok(Some(EnvelopeRecord {
  972. envelope: deserialize_json(&transfer_json)?,
  973. receipt: deserialize_json(&receipt_json)?,
  974. created_at,
  975. }))
  976. }
  977. }
  978. }
  979. async fn store_transfer(
  980. &self,
  981. record: EnvelopeRecord,
  982. involved: &[AccountId],
  983. ) -> Result<u64, StoreError> {
  984. let tid = record.receipt.transfer_id;
  985. let tid_hex = envelope_id_to_hex(&tid);
  986. let transfer_json = serialize_json(&record.envelope)?;
  987. let receipt_json = serialize_json(&record.receipt)?;
  988. let mut tx = self
  989. .pool
  990. .begin()
  991. .await
  992. .map_err(|e| StoreError::Internal(e.to_string()))?;
  993. let res = sqlx::query("INSERT INTO transfers (id, transfer, receipt, created_at, book) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO NOTHING")
  994. .bind(&tid_hex)
  995. .bind(&transfer_json)
  996. .bind(&receipt_json)
  997. .bind(record.created_at)
  998. .bind(record.envelope.book().0)
  999. .execute(&mut *tx)
  1000. .await
  1001. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1002. let inserted = res.rows_affected();
  1003. // Index every involved account (caller supplies the set; storage does no
  1004. // computation). Idempotent so a replay is harmless.
  1005. for account in involved {
  1006. sqlx::query("INSERT INTO transfer_accounts (transfer_id, account_id, subaccount) VALUES ($1, $2, $3) ON CONFLICT (transfer_id, account_id, subaccount) DO NOTHING")
  1007. .bind(&tid_hex)
  1008. .bind(account.id)
  1009. .bind(account.sub)
  1010. .execute(&mut *tx)
  1011. .await
  1012. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1013. }
  1014. tx.commit()
  1015. .await
  1016. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1017. Ok(inserted)
  1018. }
  1019. async fn get_transfers_for_account(
  1020. &self,
  1021. id: i64,
  1022. sub: Option<i64>,
  1023. ) -> Result<Vec<EnvelopeRecord>, StoreError> {
  1024. // `sub == None` spans every subaccount of `id`; `Some(s)` restricts to
  1025. // one. The subaccount is matched only for equality.
  1026. let mut sql = String::from(
  1027. "SELECT t.id, t.transfer, t.receipt, t.created_at FROM transfers t INNER JOIN transfer_accounts ta ON t.id = ta.transfer_id WHERE ta.account_id = $1",
  1028. );
  1029. if sub.is_some() {
  1030. sql.push_str(" AND ta.subaccount = $2");
  1031. }
  1032. sql.push_str(" ORDER BY t.created_at");
  1033. let mut q = sqlx::query(&sql).bind(id);
  1034. if let Some(s) = sub {
  1035. q = q.bind(s);
  1036. }
  1037. let rows = q
  1038. .fetch_all(&self.pool)
  1039. .await
  1040. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1041. let mut result = Vec::with_capacity(rows.len());
  1042. for row in &rows {
  1043. let transfer_json: String = row
  1044. .try_get("transfer")
  1045. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1046. let receipt_json: String = row
  1047. .try_get("receipt")
  1048. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1049. let created_at: i64 = row
  1050. .try_get("created_at")
  1051. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1052. result.push(EnvelopeRecord {
  1053. envelope: deserialize_json(&transfer_json)?,
  1054. receipt: deserialize_json(&receipt_json)?,
  1055. created_at,
  1056. });
  1057. }
  1058. Ok(result)
  1059. }
  1060. async fn query_transfers(
  1061. &self,
  1062. query: &TransferQuery,
  1063. ) -> Result<Page<EnvelopeRecord>, StoreError> {
  1064. // Load base records, using the account join when available.
  1065. let base_records = if let Some(account) = query.account {
  1066. self.get_transfers_for_account(account, query.sub).await?
  1067. } else {
  1068. let rows = sqlx::query(
  1069. "SELECT transfer, receipt, created_at FROM transfers ORDER BY created_at",
  1070. )
  1071. .fetch_all(&self.pool)
  1072. .await
  1073. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1074. let mut records = Vec::with_capacity(rows.len());
  1075. for row in &rows {
  1076. let transfer_json: String = row
  1077. .try_get("transfer")
  1078. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1079. let receipt_json: String = row
  1080. .try_get("receipt")
  1081. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1082. let created_at: i64 = row
  1083. .try_get("created_at")
  1084. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1085. records.push(EnvelopeRecord {
  1086. envelope: deserialize_json(&transfer_json)?,
  1087. receipt: deserialize_json(&receipt_json)?,
  1088. created_at,
  1089. });
  1090. }
  1091. records
  1092. };
  1093. // The account/subaccount narrowing happened in the load above; the
  1094. // shared filter covers the time-window and book predicates, then the
  1095. // shared page cut applies `offset`/`limit`.
  1096. Ok(paginate(
  1097. filter_transfers(base_records, query),
  1098. query.offset,
  1099. query.limit,
  1100. ))
  1101. }
  1102. }
  1103. // ---------------------------------------------------------------------------
  1104. // SagaStore
  1105. // ---------------------------------------------------------------------------
  1106. #[async_trait]
  1107. impl SagaStore for SqlStore {
  1108. async fn save_saga(&self, id: &i64, data: Vec<u8>) -> Result<(), StoreError> {
  1109. sqlx::query(
  1110. "INSERT INTO sagas (id, data) VALUES ($1, $2) \
  1111. ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data",
  1112. )
  1113. .bind(*id)
  1114. .bind(to_hex(&data))
  1115. .execute(&self.pool)
  1116. .await
  1117. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1118. Ok(())
  1119. }
  1120. async fn list_pending_sagas(&self) -> Result<Vec<(i64, Vec<u8>)>, StoreError> {
  1121. let rows = sqlx::query("SELECT id, data FROM sagas")
  1122. .fetch_all(&self.pool)
  1123. .await
  1124. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1125. let mut result = Vec::with_capacity(rows.len());
  1126. for row in &rows {
  1127. let id: i64 = row
  1128. .try_get("id")
  1129. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1130. let data_hex: String = row
  1131. .try_get("data")
  1132. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1133. result.push((id, from_hex(&data_hex)?));
  1134. }
  1135. Ok(result)
  1136. }
  1137. async fn get_saga(&self, id: &i64) -> Result<Option<Vec<u8>>, StoreError> {
  1138. let row = sqlx::query("SELECT data FROM sagas WHERE id = $1")
  1139. .bind(*id)
  1140. .fetch_optional(&self.pool)
  1141. .await
  1142. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1143. match row {
  1144. Some(row) => {
  1145. let data_hex: String = row
  1146. .try_get("data")
  1147. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1148. Ok(Some(from_hex(&data_hex)?))
  1149. }
  1150. None => Ok(None),
  1151. }
  1152. }
  1153. async fn delete_saga(&self, id: &i64) -> Result<(), StoreError> {
  1154. sqlx::query("DELETE FROM sagas WHERE id = $1")
  1155. .bind(*id)
  1156. .execute(&self.pool)
  1157. .await
  1158. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1159. Ok(())
  1160. }
  1161. }
  1162. // ---------------------------------------------------------------------------
  1163. // EventStore
  1164. // ---------------------------------------------------------------------------
  1165. #[async_trait]
  1166. impl EventStore for SqlStore {
  1167. async fn append_event(&self, event: &LedgerEvent) -> Result<u64, StoreError> {
  1168. let kind_str =
  1169. serde_json::to_string(&event.kind).map_err(|e| StoreError::Internal(e.to_string()))?;
  1170. let data = serialize_json(event)?;
  1171. let seq = self.autoid.next() as u64;
  1172. // Idempotent on the dedup key: a replayed transfer or lifecycle-transition
  1173. // event conflicts on `dedup_key` and returns the existing seq instead of a
  1174. // duplicate row.
  1175. match event_dedup_key(&event.kind) {
  1176. Some(dedup_key) => {
  1177. let res = sqlx::query("INSERT INTO events (seq, timestamp, kind, data, dedup_key) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (dedup_key) DO NOTHING")
  1178. .bind(seq as i64)
  1179. .bind(event.timestamp)
  1180. .bind(&kind_str)
  1181. .bind(&data)
  1182. .bind(&dedup_key)
  1183. .execute(&self.pool)
  1184. .await
  1185. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1186. if res.rows_affected() == 0 {
  1187. let row = sqlx::query("SELECT seq FROM events WHERE dedup_key = $1")
  1188. .bind(&dedup_key)
  1189. .fetch_one(&self.pool)
  1190. .await
  1191. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1192. let existing: i64 = row
  1193. .try_get("seq")
  1194. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1195. return Ok(existing as u64);
  1196. }
  1197. Ok(seq)
  1198. }
  1199. None => {
  1200. sqlx::query(
  1201. "INSERT INTO events (seq, timestamp, kind, data) VALUES ($1, $2, $3, $4)",
  1202. )
  1203. .bind(seq as i64)
  1204. .bind(event.timestamp)
  1205. .bind(&kind_str)
  1206. .bind(&data)
  1207. .execute(&self.pool)
  1208. .await
  1209. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1210. Ok(seq)
  1211. }
  1212. }
  1213. }
  1214. async fn get_events_since(
  1215. &self,
  1216. after_seq: u64,
  1217. limit: u32,
  1218. ) -> Result<Vec<LedgerEvent>, StoreError> {
  1219. let rows = sqlx::query("SELECT seq, data FROM events WHERE seq > $1 ORDER BY seq LIMIT $2")
  1220. .bind(after_seq as i64)
  1221. .bind(limit as i32)
  1222. .fetch_all(&self.pool)
  1223. .await
  1224. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1225. let mut events = Vec::with_capacity(rows.len());
  1226. for row in &rows {
  1227. let seq: i64 = row
  1228. .try_get("seq")
  1229. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1230. let data_json: String = row
  1231. .try_get("data")
  1232. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1233. let mut event: LedgerEvent = deserialize_json(&data_json)?;
  1234. event.seq = seq as u64;
  1235. events.push(event);
  1236. }
  1237. Ok(events)
  1238. }
  1239. }
  1240. // ---------------------------------------------------------------------------
  1241. // BookStore
  1242. // ---------------------------------------------------------------------------
  1243. #[async_trait]
  1244. impl BookStore for SqlStore {
  1245. async fn create_book(&self, book: Book) -> Result<u64, StoreError> {
  1246. // Pessimistic locking, same shape as create_account: lock any existing
  1247. // book row with `SELECT ... FOR UPDATE` inside the transaction, then
  1248. // insert with `ON CONFLICT DO NOTHING` as the portable backstop.
  1249. let lock = self.lock_clause().await?;
  1250. let data = serialize_json(&book)?;
  1251. let mut tx = self
  1252. .pool
  1253. .begin()
  1254. .await
  1255. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1256. let existing = sqlx::query(&format!("SELECT 1 FROM books WHERE id = $1 LIMIT 1{lock}"))
  1257. .bind(book.id.0)
  1258. .fetch_optional(&mut *tx)
  1259. .await
  1260. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1261. if existing.is_some() {
  1262. return Ok(0);
  1263. }
  1264. let res = sqlx::query(
  1265. "INSERT INTO books (id, name, data) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
  1266. )
  1267. .bind(book.id.0)
  1268. .bind(&book.name)
  1269. .bind(&data)
  1270. .execute(&mut *tx)
  1271. .await
  1272. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1273. if res.rows_affected() == 0 {
  1274. return Ok(0);
  1275. }
  1276. tx.commit()
  1277. .await
  1278. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1279. Ok(1)
  1280. }
  1281. async fn get_book(&self, id: &BookId) -> Result<Book, StoreError> {
  1282. let row = sqlx::query("SELECT data FROM books WHERE id = $1")
  1283. .bind(id.0)
  1284. .fetch_optional(&self.pool)
  1285. .await
  1286. .map_err(|e| StoreError::Internal(e.to_string()))?
  1287. .ok_or_else(|| StoreError::NotFound(format!("book {id:?}")))?;
  1288. let data: String = row
  1289. .try_get("data")
  1290. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1291. deserialize_json(&data)
  1292. }
  1293. async fn list_books(&self) -> Result<Vec<Book>, StoreError> {
  1294. let rows = sqlx::query("SELECT data FROM books")
  1295. .fetch_all(&self.pool)
  1296. .await
  1297. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1298. rows.iter()
  1299. .map(|row| {
  1300. let data: String = row
  1301. .try_get("data")
  1302. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1303. deserialize_json(&data)
  1304. })
  1305. .collect()
  1306. }
  1307. }
  1308. // ---------------------------------------------------------------------------
  1309. // BalanceProjectionStore
  1310. // ---------------------------------------------------------------------------
  1311. #[async_trait]
  1312. impl BalanceProjectionStore for SqlStore {
  1313. async fn append_balance_projection(
  1314. &self,
  1315. account: &AccountId,
  1316. asset: &AssetId,
  1317. balance: Cent,
  1318. watermark: i64,
  1319. ) -> Result<(), StoreError> {
  1320. // Append-only: mint a fresh monotonic id and insert a new cache point.
  1321. let id = self.autoid.next();
  1322. sqlx::query(
  1323. "INSERT INTO balance_projection (id, account, subaccount, asset, balance, watermark) \
  1324. VALUES ($1, $2, $3, $4, $5, $6)",
  1325. )
  1326. .bind(id)
  1327. .bind(account.id)
  1328. .bind(account.sub)
  1329. .bind(asset.0 as i32)
  1330. .bind(balance.to_string())
  1331. .bind(watermark)
  1332. .execute(&self.pool)
  1333. .await
  1334. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1335. Ok(())
  1336. }
  1337. async fn get_closest_balance_projection(
  1338. &self,
  1339. account: &AccountId,
  1340. asset: &AssetId,
  1341. as_of: i64,
  1342. ) -> Result<Option<BalanceProjection>, StoreError> {
  1343. // Closest at or before `as_of`: the largest watermark not exceeding it,
  1344. // tie-broken by highest id. Row selection, not an aggregate over values.
  1345. let row = sqlx::query(
  1346. "SELECT id, balance, watermark FROM balance_projection \
  1347. WHERE account = $1 AND subaccount = $2 AND asset = $3 AND watermark <= $4 \
  1348. ORDER BY watermark DESC, id DESC LIMIT 1",
  1349. )
  1350. .bind(account.id)
  1351. .bind(account.sub)
  1352. .bind(asset.0 as i32)
  1353. .bind(as_of)
  1354. .fetch_optional(&self.pool)
  1355. .await
  1356. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1357. let Some(row) = row else {
  1358. return Ok(None);
  1359. };
  1360. let id: i64 = row
  1361. .try_get("id")
  1362. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1363. let balance: String = row
  1364. .try_get("balance")
  1365. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1366. let watermark: i64 = row
  1367. .try_get("watermark")
  1368. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1369. Ok(Some(BalanceProjection {
  1370. id,
  1371. account: *account,
  1372. asset: *asset,
  1373. balance: Cent::from_str(&balance).map_err(|e| StoreError::Internal(e.to_string()))?,
  1374. watermark,
  1375. }))
  1376. }
  1377. }