lib.rs 37 KB

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