lib.rs 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  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::Pool<Any>Options::new()
  8. //! .connect("sqlite::memory:").await?;
  9. //! let store = SqlStore::new(pool);
  10. //! store.migrate().await?;
  11. //! ```
  12. use std::str::FromStr;
  13. use std::sync::atomic::{AtomicU8, Ordering};
  14. use async_trait::async_trait;
  15. use sqlx::{Any, Pool, Row};
  16. use kuatia_storage::error::StoreError;
  17. use kuatia_storage::events::{EventStore, LedgerEvent};
  18. use kuatia_storage::store::*;
  19. use kuatia_types::*;
  20. // Cached backend kind for `SqlStore::backend`.
  21. const BACKEND_UNKNOWN: u8 = 0;
  22. const BACKEND_POSTGRES: u8 = 1;
  23. const BACKEND_SQLITE: u8 = 2;
  24. /// Row-locking clause appended to a `SELECT` on backends that support it
  25. /// (PostgreSQL). SQLite has no `FOR UPDATE` and serializes writers itself, so it
  26. /// gets an empty clause.
  27. const FOR_UPDATE: &str = " FOR UPDATE";
  28. /// SQL-backed [`Store`] implementation.
  29. pub struct SqlStore {
  30. pool: Pool<Any>,
  31. autoid: kuatia_types::autoid::AutoId,
  32. /// Detected backend kind (lazily probed): one of `BACKEND_*`.
  33. backend: AtomicU8,
  34. }
  35. impl SqlStore {
  36. /// Create a new SQL store wrapping an existing connection pool.
  37. pub fn new(pool: Pool<Any>) -> Self {
  38. Self {
  39. pool,
  40. autoid: kuatia_types::autoid::AutoId::new(),
  41. backend: AtomicU8::new(BACKEND_UNKNOWN),
  42. }
  43. }
  44. /// Whether the backend is PostgreSQL. Probed once and cached: `SELECT
  45. /// sqlite_version()` succeeds only on SQLite, so a failure means Postgres.
  46. async fn is_postgres(&self) -> Result<bool, StoreError> {
  47. match self.backend.load(Ordering::Relaxed) {
  48. BACKEND_POSTGRES => return Ok(true),
  49. BACKEND_SQLITE => return Ok(false),
  50. _ => {}
  51. }
  52. let is_sqlite = sqlx::query("SELECT sqlite_version()")
  53. .fetch_optional(&self.pool)
  54. .await
  55. .is_ok();
  56. self.backend.store(
  57. if is_sqlite {
  58. BACKEND_SQLITE
  59. } else {
  60. BACKEND_POSTGRES
  61. },
  62. Ordering::Relaxed,
  63. );
  64. Ok(!is_sqlite)
  65. }
  66. /// The row-locking clause for the current backend: [`FOR_UPDATE`] on
  67. /// Postgres, empty on SQLite.
  68. async fn lock_clause(&self) -> Result<&'static str, StoreError> {
  69. Ok(if self.is_postgres().await? {
  70. FOR_UPDATE
  71. } else {
  72. ""
  73. })
  74. }
  75. /// Run database migrations. Idempotent: a `_migrations` ledger records what
  76. /// has been applied, so re-running is a no-op. Every column is a text type,
  77. /// so the store holds no opaque binary and the DDL is identical for both
  78. /// backends. Content-addressed ids and opaque saga bytes are stored as hex
  79. /// `TEXT`, and JSON payloads as their `TEXT` serialization, keeping every
  80. /// row legible for auditing.
  81. pub async fn migrate(&self) -> Result<(), StoreError> {
  82. sqlx::query("CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY)")
  83. .execute(&self.pool)
  84. .await
  85. .map_err(|e| StoreError::Internal(e.to_string()))?;
  86. let migrations: &[(&str, &str)] = &[
  87. ("001_init", include_str!("migrations/001_init.sql")),
  88. (
  89. "002_subaccounts",
  90. include_str!("migrations/002_subaccounts.sql"),
  91. ),
  92. ];
  93. for (name, sql) in migrations {
  94. let applied = sqlx::query("SELECT 1 FROM _migrations WHERE name = $1")
  95. .bind(*name)
  96. .fetch_optional(&self.pool)
  97. .await
  98. .map_err(|e| StoreError::Internal(e.to_string()))?;
  99. if applied.is_some() {
  100. continue;
  101. }
  102. for statement in sql.split(';') {
  103. let trimmed = statement.trim();
  104. if !trimmed.is_empty() {
  105. sqlx::query(trimmed)
  106. .execute(&self.pool)
  107. .await
  108. .map_err(|e| StoreError::Internal(e.to_string()))?;
  109. }
  110. }
  111. sqlx::query("INSERT INTO _migrations (name) VALUES ($1)")
  112. .bind(*name)
  113. .execute(&self.pool)
  114. .await
  115. .map_err(|e| StoreError::Internal(e.to_string()))?;
  116. }
  117. Ok(())
  118. }
  119. }
  120. // ---------------------------------------------------------------------------
  121. // Serialization helpers
  122. // ---------------------------------------------------------------------------
  123. fn serialize_policy(policy: &AccountPolicy) -> Result<String, StoreError> {
  124. serde_json::to_string(policy)
  125. .map_err(|e| StoreError::Internal(format!("policy serialization: {e}")))
  126. }
  127. fn deserialize_policy(s: &str) -> Result<AccountPolicy, StoreError> {
  128. serde_json::from_str(s).map_err(|e| StoreError::Internal(format!("bad policy: {e}")))
  129. }
  130. /// Serialize a value to a JSON string. Payload columns store JSON as `TEXT` so
  131. /// the database is directly readable for auditing; the ledger never queries
  132. /// into the JSON, so no binary or indexed representation is needed.
  133. fn serialize_json<T: serde::Serialize>(val: &T) -> Result<String, StoreError> {
  134. serde_json::to_string(val).map_err(|e| StoreError::Internal(format!("json serialization: {e}")))
  135. }
  136. fn deserialize_json<T: serde::de::DeserializeOwned>(s: &str) -> Result<T, StoreError> {
  137. serde_json::from_str(s).map_err(|e| StoreError::Internal(format!("bad json: {e}")))
  138. }
  139. /// Lower-case hex encoding. Binary identifiers (content-addressed hashes) and
  140. /// opaque saga bytes are stored as hex `TEXT` so a row is legible in any SQL
  141. /// client and matches the hex form used in logs and `Debug` output.
  142. fn to_hex(bytes: &[u8]) -> String {
  143. let mut s = String::with_capacity(bytes.len() * 2);
  144. for b in bytes {
  145. s.push_str(&format!("{b:02x}"));
  146. }
  147. s
  148. }
  149. fn from_hex(s: &str) -> Result<Vec<u8>, StoreError> {
  150. if s.len() % 2 != 0 {
  151. return Err(StoreError::Internal(format!("odd-length hex: {s:?}")));
  152. }
  153. (0..s.len())
  154. .step_by(2)
  155. .map(|i| {
  156. u8::from_str_radix(&s[i..i + 2], 16)
  157. .map_err(|e| StoreError::Internal(format!("bad hex: {e}")))
  158. })
  159. .collect()
  160. }
  161. fn envelope_id_to_hex(id: &EnvelopeId) -> String {
  162. to_hex(&id.0)
  163. }
  164. fn envelope_id_from_hex(s: &str) -> Result<EnvelopeId, StoreError> {
  165. let bytes = from_hex(s)?;
  166. let arr: [u8; 32] = bytes.as_slice().try_into().map_err(|_| {
  167. StoreError::Internal(format!("expected 32-byte id, got {} bytes", bytes.len()))
  168. })?;
  169. Ok(EnvelopeId(arr))
  170. }
  171. fn status_to_i16(s: PostingStatus) -> i16 {
  172. match s {
  173. PostingStatus::Active => 0,
  174. PostingStatus::PendingInactive => 1,
  175. PostingStatus::Inactive => 2,
  176. }
  177. }
  178. fn status_from_i16(v: i16) -> Result<PostingStatus, StoreError> {
  179. match v {
  180. 0 => Ok(PostingStatus::Active),
  181. 1 => Ok(PostingStatus::PendingInactive),
  182. 2 => Ok(PostingStatus::Inactive),
  183. _ => Err(StoreError::Internal(format!("bad posting status: {v}"))),
  184. }
  185. }
  186. fn row_to_account(row: &sqlx::any::AnyRow) -> Result<Account, StoreError> {
  187. let id: i64 = row
  188. .try_get("id")
  189. .map_err(|e| StoreError::Internal(e.to_string()))?;
  190. let subaccount: i64 = row
  191. .try_get("subaccount")
  192. .map_err(|e| StoreError::Internal(e.to_string()))?;
  193. let version: i64 = row
  194. .try_get("version")
  195. .map_err(|e| StoreError::Internal(e.to_string()))?;
  196. let policy_str: String = row
  197. .try_get("policy")
  198. .map_err(|e| StoreError::Internal(e.to_string()))?;
  199. let flags_bits: i32 = row
  200. .try_get("flags")
  201. .map_err(|e| StoreError::Internal(e.to_string()))?;
  202. let book: i64 = row
  203. .try_get("book")
  204. .map_err(|e| StoreError::Internal(e.to_string()))?;
  205. let user_data_json: String = row
  206. .try_get("user_data")
  207. .map_err(|e| StoreError::Internal(e.to_string()))?;
  208. let metadata_json: String = row
  209. .try_get("metadata")
  210. .map_err(|e| StoreError::Internal(e.to_string()))?;
  211. Ok(Account {
  212. id: AccountId::with_sub(id, subaccount),
  213. version: version as u64,
  214. policy: deserialize_policy(&policy_str)?,
  215. flags: AccountFlags::from_bits_truncate(flags_bits as u32),
  216. book: BookId::new(book),
  217. user_data: deserialize_json(&user_data_json)?,
  218. metadata: deserialize_json(&metadata_json)?,
  219. })
  220. }
  221. fn row_to_posting(row: &sqlx::any::AnyRow) -> Result<Posting, StoreError> {
  222. let transfer_id: String = row
  223. .try_get("transfer_id")
  224. .map_err(|e| StoreError::Internal(e.to_string()))?;
  225. let idx: i16 = row
  226. .try_get("idx")
  227. .map_err(|e| StoreError::Internal(e.to_string()))?;
  228. let owner: i64 = row
  229. .try_get("owner")
  230. .map_err(|e| StoreError::Internal(e.to_string()))?;
  231. let subaccount: i64 = row
  232. .try_get("subaccount")
  233. .map_err(|e| StoreError::Internal(e.to_string()))?;
  234. let asset: i32 = row
  235. .try_get("asset")
  236. .map_err(|e| StoreError::Internal(e.to_string()))?;
  237. let value: String = row
  238. .try_get("value")
  239. .map_err(|e| StoreError::Internal(e.to_string()))?;
  240. let value = Cent::from_str(&value).map_err(|e| StoreError::Internal(e.to_string()))?;
  241. let status: i16 = row
  242. .try_get("status")
  243. .map_err(|e| StoreError::Internal(e.to_string()))?;
  244. let reservation: Option<i64> = row
  245. .try_get("reservation")
  246. .map_err(|e| StoreError::Internal(e.to_string()))?;
  247. Ok(Posting {
  248. id: PostingId {
  249. transfer: envelope_id_from_hex(&transfer_id)?,
  250. index: idx as u16,
  251. },
  252. owner: AccountId::with_sub(owner, subaccount),
  253. asset: AssetId::new(asset as u32),
  254. value,
  255. status: status_from_i16(status)?,
  256. reservation: reservation.map(ReservationId::new),
  257. })
  258. }
  259. // ---------------------------------------------------------------------------
  260. // AccountStore
  261. // ---------------------------------------------------------------------------
  262. #[async_trait]
  263. impl AccountStore for SqlStore {
  264. async fn get_account(&self, id: &AccountId) -> Result<Account, StoreError> {
  265. let row = sqlx::query(
  266. "SELECT * FROM accounts WHERE id = $1 AND subaccount = $2 ORDER BY version DESC LIMIT 1",
  267. )
  268. .bind(id.id)
  269. .bind(id.sub)
  270. .fetch_optional(&self.pool)
  271. .await
  272. .map_err(|e| StoreError::Internal(e.to_string()))?
  273. .ok_or_else(|| StoreError::NotFound(format!("account {id:?}")))?;
  274. row_to_account(&row)
  275. }
  276. async fn get_accounts(&self, ids: &[AccountId]) -> Result<Vec<Account>, StoreError> {
  277. let mut result = Vec::with_capacity(ids.len());
  278. for id in ids {
  279. result.push(self.get_account(id).await?);
  280. }
  281. Ok(result)
  282. }
  283. async fn create_account(&self, account: Account) -> Result<(), StoreError> {
  284. // Pessimistic locking: inside one transaction, lock any existing row for
  285. // this account with `SELECT ... FOR UPDATE` so a concurrent creator
  286. // waits, then insert. `ON CONFLICT DO NOTHING` is the portable backstop
  287. // (SQLite has no `FOR UPDATE`, and it turns a concurrent double-insert
  288. // into a clean affected-row count instead of a unique violation).
  289. let lock = self.lock_clause().await?;
  290. let mut tx = self
  291. .pool
  292. .begin()
  293. .await
  294. .map_err(|e| StoreError::Internal(e.to_string()))?;
  295. let existing = sqlx::query(&format!(
  296. "SELECT 1 FROM accounts WHERE id = $1 AND subaccount = $2 LIMIT 1{lock}"
  297. ))
  298. .bind(account.id.id)
  299. .bind(account.id.sub)
  300. .fetch_optional(&mut *tx)
  301. .await
  302. .map_err(|e| StoreError::Internal(e.to_string()))?;
  303. if existing.is_some() {
  304. return Err(StoreError::AlreadyExists(format!(
  305. "account {:?}",
  306. account.id
  307. )));
  308. }
  309. let res = sqlx::query(
  310. "INSERT INTO accounts (id, subaccount, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id, subaccount, version) DO NOTHING"
  311. )
  312. .bind(account.id.id)
  313. .bind(account.id.sub)
  314. .bind(account.version as i64)
  315. .bind(serialize_policy(&account.policy)?)
  316. .bind(account.flags.bits() as i32)
  317. .bind(account.book.0)
  318. .bind(serialize_json(&account.user_data)?)
  319. .bind(serialize_json(&account.metadata)?)
  320. .execute(&mut *tx)
  321. .await
  322. .map_err(|e| StoreError::Internal(e.to_string()))?;
  323. if res.rows_affected() == 0 {
  324. return Err(StoreError::AlreadyExists(format!(
  325. "account {:?}",
  326. account.id
  327. )));
  328. }
  329. tx.commit()
  330. .await
  331. .map_err(|e| StoreError::Internal(e.to_string()))?;
  332. Ok(())
  333. }
  334. async fn append_account_version(&self, account: Account) -> Result<(), StoreError> {
  335. // Pessimistic locking: inside one transaction, lock the account's latest
  336. // version row with `SELECT ... FOR UPDATE` so a concurrent appender waits
  337. // here until we commit, then check the version and insert. `ON CONFLICT`
  338. // is the portable backstop (SQLite has no `FOR UPDATE`, and it covers the
  339. // append phantom-insert a row lock does not).
  340. let lock = self.lock_clause().await?;
  341. let mut tx = self
  342. .pool
  343. .begin()
  344. .await
  345. .map_err(|e| StoreError::Internal(e.to_string()))?;
  346. let current = sqlx::query(&format!(
  347. "SELECT version FROM accounts WHERE id = $1 AND subaccount = $2 ORDER BY version DESC LIMIT 1{lock}"
  348. ))
  349. .bind(account.id.id)
  350. .bind(account.id.sub)
  351. .fetch_optional(&mut *tx)
  352. .await
  353. .map_err(|e| StoreError::Internal(e.to_string()))?
  354. .ok_or_else(|| StoreError::NotFound(format!("account {:?}", account.id)))?;
  355. let current_version: i64 = current
  356. .try_get("version")
  357. .map_err(|e| StoreError::Internal(e.to_string()))?;
  358. let expected = current_version
  359. .checked_add(1)
  360. .ok_or_else(|| StoreError::Internal("account version overflow".to_string()))?;
  361. if account.version as i64 != expected {
  362. return Err(StoreError::VersionConflict {
  363. account: account.id,
  364. expected: expected as u64,
  365. actual: account.version,
  366. });
  367. }
  368. let res = sqlx::query(
  369. "INSERT INTO accounts (id, subaccount, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id, subaccount, version) DO NOTHING"
  370. )
  371. .bind(account.id.id)
  372. .bind(account.id.sub)
  373. .bind(account.version as i64)
  374. .bind(serialize_policy(&account.policy)?)
  375. .bind(account.flags.bits() as i32)
  376. .bind(account.book.0)
  377. .bind(serialize_json(&account.user_data)?)
  378. .bind(serialize_json(&account.metadata)?)
  379. .execute(&mut *tx)
  380. .await
  381. .map_err(|e| StoreError::Internal(e.to_string()))?;
  382. if res.rows_affected() == 0 {
  383. return Err(StoreError::VersionConflict {
  384. account: account.id,
  385. expected: expected as u64,
  386. actual: account.version,
  387. });
  388. }
  389. tx.commit()
  390. .await
  391. .map_err(|e| StoreError::Internal(e.to_string()))?;
  392. Ok(())
  393. }
  394. async fn get_account_history(&self, id: &AccountId) -> Result<Vec<Account>, StoreError> {
  395. let rows = sqlx::query(
  396. "SELECT * FROM accounts WHERE id = $1 AND subaccount = $2 ORDER BY version ASC",
  397. )
  398. .bind(id.id)
  399. .bind(id.sub)
  400. .fetch_all(&self.pool)
  401. .await
  402. .map_err(|e| StoreError::Internal(e.to_string()))?;
  403. if rows.is_empty() {
  404. return Err(StoreError::NotFound(format!("account {id:?}")));
  405. }
  406. rows.iter().map(row_to_account).collect()
  407. }
  408. async fn list_accounts(&self) -> Result<Vec<Account>, StoreError> {
  409. let rows = sqlx::query("SELECT * FROM accounts ORDER BY id, subaccount, version DESC")
  410. .fetch_all(&self.pool)
  411. .await
  412. .map_err(|e| StoreError::Internal(e.to_string()))?;
  413. let mut accounts: Vec<Account> =
  414. rows.iter().map(row_to_account).collect::<Result<_, _>>()?;
  415. accounts.dedup_by_key(|a| a.id);
  416. Ok(accounts)
  417. }
  418. }
  419. // ---------------------------------------------------------------------------
  420. // PostingStore
  421. // ---------------------------------------------------------------------------
  422. #[async_trait]
  423. impl PostingStore for SqlStore {
  424. async fn get_postings(&self, ids: &[PostingId]) -> Result<Vec<Posting>, StoreError> {
  425. let mut result = Vec::with_capacity(ids.len());
  426. for id in ids {
  427. let row = sqlx::query("SELECT * FROM postings WHERE transfer_id = $1 AND idx = $2")
  428. .bind(envelope_id_to_hex(&id.transfer))
  429. .bind(id.index as i16)
  430. .fetch_optional(&self.pool)
  431. .await
  432. .map_err(|e| StoreError::Internal(e.to_string()))?
  433. .ok_or_else(|| StoreError::NotFound(format!("posting {id:?}")))?;
  434. result.push(row_to_posting(&row)?);
  435. }
  436. Ok(result)
  437. }
  438. async fn get_postings_by_account(
  439. &self,
  440. id: i64,
  441. sub: Option<i64>,
  442. asset: Option<&AssetId>,
  443. status: Option<PostingStatus>,
  444. ) -> Result<Vec<Posting>, StoreError> {
  445. // Build the predicate dynamically: `sub == None` spans every subaccount
  446. // of `id`, `Some(s)` restricts to one. The subaccount is compared only
  447. // for equality, never as a magnitude.
  448. let mut sql = String::from("SELECT * FROM postings WHERE owner = $1");
  449. let mut placeholder = 2u32;
  450. if sub.is_some() {
  451. sql.push_str(&format!(" AND subaccount = ${placeholder}"));
  452. placeholder += 1;
  453. }
  454. if asset.is_some() {
  455. sql.push_str(&format!(" AND asset = ${placeholder}"));
  456. placeholder += 1;
  457. }
  458. if status.is_some() {
  459. sql.push_str(&format!(" AND status = ${placeholder}"));
  460. }
  461. let mut q = sqlx::query(&sql).bind(id);
  462. if let Some(s) = sub {
  463. q = q.bind(s);
  464. }
  465. if let Some(a) = asset {
  466. q = q.bind(a.0 as i32);
  467. }
  468. if let Some(s) = status {
  469. q = q.bind(status_to_i16(s));
  470. }
  471. let rows = q
  472. .fetch_all(&self.pool)
  473. .await
  474. .map_err(|e| StoreError::Internal(e.to_string()))?;
  475. rows.iter().map(row_to_posting).collect()
  476. }
  477. async fn query_postings(&self, query: &PostingQuery) -> Result<Page<Posting>, StoreError> {
  478. let (where_clause, count_clause) = {
  479. let mut w = String::from("WHERE owner = $1");
  480. let mut idx = 2u32;
  481. if query.sub.is_some() {
  482. w.push_str(&format!(" AND subaccount = ${idx}"));
  483. idx += 1;
  484. }
  485. if query.asset.is_some() {
  486. w.push_str(&format!(" AND asset = ${idx}"));
  487. idx += 1;
  488. }
  489. if query.status.is_some() {
  490. w.push_str(&format!(" AND status = ${idx}"));
  491. }
  492. let c = format!("SELECT COUNT(*) as cnt FROM postings {w}");
  493. let limit = query.limit.unwrap_or(u32::MAX);
  494. let offset = query.offset.unwrap_or(0);
  495. w.push_str(&format!(" LIMIT {limit} OFFSET {offset}"));
  496. (format!("SELECT * FROM postings {w}"), c)
  497. };
  498. // Build count query
  499. let mut count_q = sqlx::query(&count_clause).bind(query.account);
  500. if let Some(s) = query.sub {
  501. count_q = count_q.bind(s);
  502. }
  503. if let Some(ref a) = query.asset {
  504. count_q = count_q.bind(a.0 as i32);
  505. }
  506. if let Some(s) = query.status {
  507. count_q = count_q.bind(status_to_i16(s));
  508. }
  509. let count_row = count_q
  510. .fetch_one(&self.pool)
  511. .await
  512. .map_err(|e| StoreError::Internal(e.to_string()))?;
  513. let total: i64 = count_row
  514. .try_get("cnt")
  515. .map_err(|e| StoreError::Internal(e.to_string()))?;
  516. // Build data query
  517. let mut data_q = sqlx::query(&where_clause).bind(query.account);
  518. if let Some(s) = query.sub {
  519. data_q = data_q.bind(s);
  520. }
  521. if let Some(ref a) = query.asset {
  522. data_q = data_q.bind(a.0 as i32);
  523. }
  524. if let Some(s) = query.status {
  525. data_q = data_q.bind(status_to_i16(s));
  526. }
  527. let rows = data_q
  528. .fetch_all(&self.pool)
  529. .await
  530. .map_err(|e| StoreError::Internal(e.to_string()))?;
  531. let items: Vec<Posting> = rows.iter().map(row_to_posting).collect::<Result<_, _>>()?;
  532. Ok(Page {
  533. items,
  534. total: total as u64,
  535. })
  536. }
  537. async fn reserve_postings(
  538. &self,
  539. ids: &[PostingId],
  540. reservation: ReservationId,
  541. ) -> Result<u64, StoreError> {
  542. // Dumb instruction: each id flips Active → PendingInactive (the status
  543. // precondition is in the WHERE so it is atomic). Return the count of rows
  544. // changed; the caller decides what a short count means.
  545. let mut tx = self
  546. .pool
  547. .begin()
  548. .await
  549. .map_err(|e| StoreError::Internal(e.to_string()))?;
  550. let mut reserved: u64 = 0;
  551. for id in ids {
  552. let res = sqlx::query(
  553. "UPDATE postings SET status = $1, reservation = $2 WHERE transfer_id = $3 AND idx = $4 AND status = $5",
  554. )
  555. .bind(status_to_i16(PostingStatus::PendingInactive))
  556. .bind(reservation.0)
  557. .bind(envelope_id_to_hex(&id.transfer))
  558. .bind(id.index as i16)
  559. .bind(status_to_i16(PostingStatus::Active))
  560. .execute(&mut *tx)
  561. .await
  562. .map_err(|e| StoreError::Internal(e.to_string()))?;
  563. reserved += res.rows_affected();
  564. }
  565. tx.commit()
  566. .await
  567. .map_err(|e| StoreError::Internal(e.to_string()))?;
  568. Ok(reserved)
  569. }
  570. async fn release_postings(
  571. &self,
  572. ids: &[PostingId],
  573. reservation: ReservationId,
  574. ) -> Result<u64, StoreError> {
  575. // Dumb instruction: each id reserved by `reservation` flips
  576. // PendingInactive → Active. Return the count released; an already-Active
  577. // or differently-owned posting simply does not count.
  578. let mut tx = self
  579. .pool
  580. .begin()
  581. .await
  582. .map_err(|e| StoreError::Internal(e.to_string()))?;
  583. let mut released: u64 = 0;
  584. for id in ids {
  585. let res = sqlx::query("UPDATE postings SET status = $1, reservation = NULL WHERE transfer_id = $2 AND idx = $3 AND status = $4 AND reservation = $5")
  586. .bind(status_to_i16(PostingStatus::Active))
  587. .bind(envelope_id_to_hex(&id.transfer))
  588. .bind(id.index as i16)
  589. .bind(status_to_i16(PostingStatus::PendingInactive))
  590. .bind(reservation.0)
  591. .execute(&mut *tx)
  592. .await
  593. .map_err(|e| StoreError::Internal(e.to_string()))?;
  594. released += res.rows_affected();
  595. }
  596. tx.commit()
  597. .await
  598. .map_err(|e| StoreError::Internal(e.to_string()))?;
  599. Ok(released)
  600. }
  601. async fn deactivate_postings(
  602. &self,
  603. ids: &[PostingId],
  604. reservation: Option<ReservationId>,
  605. ) -> Result<u64, StoreError> {
  606. let mut tx = self
  607. .pool
  608. .begin()
  609. .await
  610. .map_err(|e| StoreError::Internal(e.to_string()))?;
  611. let mut changed: u64 = 0;
  612. for id in ids {
  613. // The precondition is the instruction; the count is the result. The
  614. // caller decides what a short count means.
  615. let res = match reservation {
  616. None => {
  617. sqlx::query("UPDATE postings SET status = $1, reservation = NULL WHERE transfer_id = $2 AND idx = $3 AND status = $4")
  618. .bind(status_to_i16(PostingStatus::Inactive))
  619. .bind(envelope_id_to_hex(&id.transfer))
  620. .bind(id.index as i16)
  621. .bind(status_to_i16(PostingStatus::Active))
  622. .execute(&mut *tx)
  623. .await
  624. }
  625. Some(rid) => {
  626. sqlx::query("UPDATE postings SET status = $1, reservation = NULL WHERE transfer_id = $2 AND idx = $3 AND status = $4 AND reservation = $5")
  627. .bind(status_to_i16(PostingStatus::Inactive))
  628. .bind(envelope_id_to_hex(&id.transfer))
  629. .bind(id.index as i16)
  630. .bind(status_to_i16(PostingStatus::PendingInactive))
  631. .bind(rid.0)
  632. .execute(&mut *tx)
  633. .await
  634. }
  635. }
  636. .map_err(|e| StoreError::Internal(e.to_string()))?;
  637. changed += res.rows_affected();
  638. }
  639. tx.commit()
  640. .await
  641. .map_err(|e| StoreError::Internal(e.to_string()))?;
  642. Ok(changed)
  643. }
  644. async fn insert_postings(&self, postings: &[Posting]) -> Result<u64, StoreError> {
  645. let mut tx = self
  646. .pool
  647. .begin()
  648. .await
  649. .map_err(|e| StoreError::Internal(e.to_string()))?;
  650. let mut inserted: u64 = 0;
  651. for posting in postings {
  652. let res = sqlx::query(
  653. "INSERT INTO postings (transfer_id, idx, owner, subaccount, asset, value, status) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (transfer_id, idx) DO NOTHING"
  654. )
  655. .bind(envelope_id_to_hex(&posting.id.transfer))
  656. .bind(posting.id.index as i16)
  657. .bind(posting.owner.id)
  658. .bind(posting.owner.sub)
  659. .bind(posting.asset.0 as i32)
  660. .bind(posting.value.to_string())
  661. .bind(status_to_i16(posting.status))
  662. .execute(&mut *tx)
  663. .await
  664. .map_err(|e| StoreError::Internal(e.to_string()))?;
  665. inserted += res.rows_affected();
  666. }
  667. tx.commit()
  668. .await
  669. .map_err(|e| StoreError::Internal(e.to_string()))?;
  670. Ok(inserted)
  671. }
  672. }
  673. // ---------------------------------------------------------------------------
  674. // TransferStore
  675. // ---------------------------------------------------------------------------
  676. #[async_trait]
  677. impl TransferStore for SqlStore {
  678. async fn get_transfer(&self, id: &EnvelopeId) -> Result<Option<EnvelopeRecord>, StoreError> {
  679. let row = sqlx::query("SELECT transfer, receipt, created_at FROM transfers WHERE id = $1")
  680. .bind(envelope_id_to_hex(id))
  681. .fetch_optional(&self.pool)
  682. .await
  683. .map_err(|e| StoreError::Internal(e.to_string()))?;
  684. match row {
  685. None => Ok(None),
  686. Some(row) => {
  687. let transfer_json: String = row
  688. .try_get("transfer")
  689. .map_err(|e| StoreError::Internal(e.to_string()))?;
  690. let receipt_json: String = row
  691. .try_get("receipt")
  692. .map_err(|e| StoreError::Internal(e.to_string()))?;
  693. let created_at: i64 = row
  694. .try_get("created_at")
  695. .map_err(|e| StoreError::Internal(e.to_string()))?;
  696. Ok(Some(EnvelopeRecord {
  697. envelope: deserialize_json(&transfer_json)?,
  698. receipt: deserialize_json(&receipt_json)?,
  699. created_at,
  700. }))
  701. }
  702. }
  703. }
  704. async fn store_transfer(
  705. &self,
  706. record: EnvelopeRecord,
  707. involved: &[AccountId],
  708. ) -> Result<u64, StoreError> {
  709. let tid = record.receipt.transfer_id;
  710. let tid_hex = envelope_id_to_hex(&tid);
  711. let transfer_json = serialize_json(&record.envelope)?;
  712. let receipt_json = serialize_json(&record.receipt)?;
  713. let mut tx = self
  714. .pool
  715. .begin()
  716. .await
  717. .map_err(|e| StoreError::Internal(e.to_string()))?;
  718. let res = sqlx::query("INSERT INTO transfers (id, transfer, receipt, created_at, book) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO NOTHING")
  719. .bind(&tid_hex)
  720. .bind(&transfer_json)
  721. .bind(&receipt_json)
  722. .bind(record.created_at)
  723. .bind(record.envelope.book().0)
  724. .execute(&mut *tx)
  725. .await
  726. .map_err(|e| StoreError::Internal(e.to_string()))?;
  727. let inserted = res.rows_affected();
  728. // Index every involved account (caller supplies the set; storage does no
  729. // computation). Idempotent so a replay is harmless.
  730. for account in involved {
  731. sqlx::query("INSERT INTO transfer_accounts (transfer_id, account_id, subaccount) VALUES ($1, $2, $3) ON CONFLICT (transfer_id, account_id, subaccount) DO NOTHING")
  732. .bind(&tid_hex)
  733. .bind(account.id)
  734. .bind(account.sub)
  735. .execute(&mut *tx)
  736. .await
  737. .map_err(|e| StoreError::Internal(e.to_string()))?;
  738. }
  739. tx.commit()
  740. .await
  741. .map_err(|e| StoreError::Internal(e.to_string()))?;
  742. Ok(inserted)
  743. }
  744. async fn get_transfers_for_account(
  745. &self,
  746. id: i64,
  747. sub: Option<i64>,
  748. ) -> Result<Vec<EnvelopeRecord>, StoreError> {
  749. // `sub == None` spans every subaccount of `id`; `Some(s)` restricts to
  750. // one. The subaccount is matched only for equality.
  751. let mut sql = String::from(
  752. "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",
  753. );
  754. if sub.is_some() {
  755. sql.push_str(" AND ta.subaccount = $2");
  756. }
  757. sql.push_str(" ORDER BY t.created_at");
  758. let mut q = sqlx::query(&sql).bind(id);
  759. if let Some(s) = sub {
  760. q = q.bind(s);
  761. }
  762. let rows = q
  763. .fetch_all(&self.pool)
  764. .await
  765. .map_err(|e| StoreError::Internal(e.to_string()))?;
  766. let mut result = Vec::with_capacity(rows.len());
  767. for row in &rows {
  768. let transfer_json: String = row
  769. .try_get("transfer")
  770. .map_err(|e| StoreError::Internal(e.to_string()))?;
  771. let receipt_json: String = row
  772. .try_get("receipt")
  773. .map_err(|e| StoreError::Internal(e.to_string()))?;
  774. let created_at: i64 = row
  775. .try_get("created_at")
  776. .map_err(|e| StoreError::Internal(e.to_string()))?;
  777. result.push(EnvelopeRecord {
  778. envelope: deserialize_json(&transfer_json)?,
  779. receipt: deserialize_json(&receipt_json)?,
  780. created_at,
  781. });
  782. }
  783. Ok(result)
  784. }
  785. async fn query_transfers(
  786. &self,
  787. query: &TransferQuery,
  788. ) -> Result<Page<EnvelopeRecord>, StoreError> {
  789. // Load base records, using the account join when available.
  790. let base_records = if let Some(account) = query.account {
  791. self.get_transfers_for_account(account, query.sub).await?
  792. } else {
  793. let rows = sqlx::query(
  794. "SELECT transfer, receipt, created_at FROM transfers ORDER BY created_at",
  795. )
  796. .fetch_all(&self.pool)
  797. .await
  798. .map_err(|e| StoreError::Internal(e.to_string()))?;
  799. let mut records = Vec::with_capacity(rows.len());
  800. for row in &rows {
  801. let transfer_json: String = row
  802. .try_get("transfer")
  803. .map_err(|e| StoreError::Internal(e.to_string()))?;
  804. let receipt_json: String = row
  805. .try_get("receipt")
  806. .map_err(|e| StoreError::Internal(e.to_string()))?;
  807. let created_at: i64 = row
  808. .try_get("created_at")
  809. .map_err(|e| StoreError::Internal(e.to_string()))?;
  810. records.push(EnvelopeRecord {
  811. envelope: deserialize_json(&transfer_json)?,
  812. receipt: deserialize_json(&receipt_json)?,
  813. created_at,
  814. });
  815. }
  816. records
  817. };
  818. // Filter in memory for remaining conditions.
  819. let filtered: Vec<EnvelopeRecord> = base_records
  820. .into_iter()
  821. .filter(|r| {
  822. if let Some(from) = query.from_ts
  823. && r.created_at < from
  824. {
  825. return false;
  826. }
  827. if let Some(to) = query.to_ts
  828. && r.created_at >= to
  829. {
  830. return false;
  831. }
  832. if let Some(book) = query.book
  833. && r.envelope.book() != book
  834. {
  835. return false;
  836. }
  837. true
  838. })
  839. .collect();
  840. let total = filtered.len() as u64;
  841. let offset = query.offset.unwrap_or(0) as usize;
  842. let limit = query.limit.unwrap_or(u32::MAX) as usize;
  843. let items = filtered.into_iter().skip(offset).take(limit).collect();
  844. Ok(Page { items, total })
  845. }
  846. }
  847. // ---------------------------------------------------------------------------
  848. // SagaStore
  849. // ---------------------------------------------------------------------------
  850. #[async_trait]
  851. impl SagaStore for SqlStore {
  852. async fn save_saga(&self, id: &i64, data: Vec<u8>) -> Result<(), StoreError> {
  853. sqlx::query(
  854. "INSERT INTO sagas (id, data) VALUES ($1, $2) \
  855. ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data",
  856. )
  857. .bind(*id)
  858. .bind(to_hex(&data))
  859. .execute(&self.pool)
  860. .await
  861. .map_err(|e| StoreError::Internal(e.to_string()))?;
  862. Ok(())
  863. }
  864. async fn list_pending_sagas(&self) -> Result<Vec<(i64, Vec<u8>)>, StoreError> {
  865. let rows = sqlx::query("SELECT id, data FROM sagas")
  866. .fetch_all(&self.pool)
  867. .await
  868. .map_err(|e| StoreError::Internal(e.to_string()))?;
  869. let mut result = Vec::with_capacity(rows.len());
  870. for row in &rows {
  871. let id: i64 = row
  872. .try_get("id")
  873. .map_err(|e| StoreError::Internal(e.to_string()))?;
  874. let data_hex: String = row
  875. .try_get("data")
  876. .map_err(|e| StoreError::Internal(e.to_string()))?;
  877. result.push((id, from_hex(&data_hex)?));
  878. }
  879. Ok(result)
  880. }
  881. async fn delete_saga(&self, id: &i64) -> Result<(), StoreError> {
  882. sqlx::query("DELETE FROM sagas WHERE id = $1")
  883. .bind(*id)
  884. .execute(&self.pool)
  885. .await
  886. .map_err(|e| StoreError::Internal(e.to_string()))?;
  887. Ok(())
  888. }
  889. }
  890. // ---------------------------------------------------------------------------
  891. // EventStore
  892. // ---------------------------------------------------------------------------
  893. #[async_trait]
  894. impl EventStore for SqlStore {
  895. async fn append_event(&self, event: &LedgerEvent) -> Result<u64, StoreError> {
  896. let kind_str =
  897. serde_json::to_string(&event.kind).map_err(|e| StoreError::Internal(e.to_string()))?;
  898. let data = serialize_json(event)?;
  899. let seq = self.autoid.next() as u64;
  900. // Idempotent on the dedup key: a replayed transfer event conflicts on
  901. // `dedup_key` and returns the existing seq instead of a duplicate row.
  902. match kuatia_storage::events::event_dedup_key(&event.kind) {
  903. Some(eid) => {
  904. let dedup_hex = envelope_id_to_hex(&eid);
  905. 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")
  906. .bind(seq as i64)
  907. .bind(event.timestamp)
  908. .bind(&kind_str)
  909. .bind(&data)
  910. .bind(&dedup_hex)
  911. .execute(&self.pool)
  912. .await
  913. .map_err(|e| StoreError::Internal(e.to_string()))?;
  914. if res.rows_affected() == 0 {
  915. let row = sqlx::query("SELECT seq FROM events WHERE dedup_key = $1")
  916. .bind(&dedup_hex)
  917. .fetch_one(&self.pool)
  918. .await
  919. .map_err(|e| StoreError::Internal(e.to_string()))?;
  920. let existing: i64 = row
  921. .try_get("seq")
  922. .map_err(|e| StoreError::Internal(e.to_string()))?;
  923. return Ok(existing as u64);
  924. }
  925. Ok(seq)
  926. }
  927. None => {
  928. sqlx::query(
  929. "INSERT INTO events (seq, timestamp, kind, data) VALUES ($1, $2, $3, $4)",
  930. )
  931. .bind(seq as i64)
  932. .bind(event.timestamp)
  933. .bind(&kind_str)
  934. .bind(&data)
  935. .execute(&self.pool)
  936. .await
  937. .map_err(|e| StoreError::Internal(e.to_string()))?;
  938. Ok(seq)
  939. }
  940. }
  941. }
  942. async fn get_events_since(
  943. &self,
  944. after_seq: u64,
  945. limit: u32,
  946. ) -> Result<Vec<LedgerEvent>, StoreError> {
  947. let rows = sqlx::query("SELECT seq, data FROM events WHERE seq > $1 ORDER BY seq LIMIT $2")
  948. .bind(after_seq as i64)
  949. .bind(limit as i32)
  950. .fetch_all(&self.pool)
  951. .await
  952. .map_err(|e| StoreError::Internal(e.to_string()))?;
  953. let mut events = Vec::with_capacity(rows.len());
  954. for row in &rows {
  955. let seq: i64 = row
  956. .try_get("seq")
  957. .map_err(|e| StoreError::Internal(e.to_string()))?;
  958. let data_json: String = row
  959. .try_get("data")
  960. .map_err(|e| StoreError::Internal(e.to_string()))?;
  961. let mut event: LedgerEvent = deserialize_json(&data_json)?;
  962. event.seq = seq as u64;
  963. events.push(event);
  964. }
  965. Ok(events)
  966. }
  967. }
  968. // ---------------------------------------------------------------------------
  969. // BookStore
  970. // ---------------------------------------------------------------------------
  971. #[async_trait]
  972. impl BookStore for SqlStore {
  973. async fn create_book(&self, book: Book) -> Result<(), StoreError> {
  974. // Pessimistic locking, same shape as create_account: lock any existing
  975. // book row with `SELECT ... FOR UPDATE` inside the transaction, then
  976. // insert with `ON CONFLICT DO NOTHING` as the portable backstop.
  977. let lock = self.lock_clause().await?;
  978. let data = serialize_json(&book)?;
  979. let mut tx = self
  980. .pool
  981. .begin()
  982. .await
  983. .map_err(|e| StoreError::Internal(e.to_string()))?;
  984. let existing = sqlx::query(&format!("SELECT 1 FROM books WHERE id = $1 LIMIT 1{lock}"))
  985. .bind(book.id.0)
  986. .fetch_optional(&mut *tx)
  987. .await
  988. .map_err(|e| StoreError::Internal(e.to_string()))?;
  989. if existing.is_some() {
  990. return Err(StoreError::AlreadyExists(format!("book {:?}", book.id)));
  991. }
  992. let res = sqlx::query(
  993. "INSERT INTO books (id, name, data) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
  994. )
  995. .bind(book.id.0)
  996. .bind(&book.name)
  997. .bind(&data)
  998. .execute(&mut *tx)
  999. .await
  1000. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1001. if res.rows_affected() == 0 {
  1002. return Err(StoreError::AlreadyExists(format!("book {:?}", book.id)));
  1003. }
  1004. tx.commit()
  1005. .await
  1006. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1007. Ok(())
  1008. }
  1009. async fn get_book(&self, id: &BookId) -> Result<Book, StoreError> {
  1010. let row = sqlx::query("SELECT data FROM books WHERE id = $1")
  1011. .bind(id.0)
  1012. .fetch_optional(&self.pool)
  1013. .await
  1014. .map_err(|e| StoreError::Internal(e.to_string()))?
  1015. .ok_or_else(|| StoreError::NotFound(format!("book {id:?}")))?;
  1016. let data: String = row
  1017. .try_get("data")
  1018. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1019. deserialize_json(&data)
  1020. }
  1021. async fn list_books(&self) -> Result<Vec<Book>, StoreError> {
  1022. let rows = sqlx::query("SELECT data FROM books")
  1023. .fetch_all(&self.pool)
  1024. .await
  1025. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1026. rows.iter()
  1027. .map(|row| {
  1028. let data: String = row
  1029. .try_get("data")
  1030. .map_err(|e| StoreError::Internal(e.to_string()))?;
  1031. deserialize_json(&data)
  1032. })
  1033. .collect()
  1034. }
  1035. }