Skip to main content

miden_client_sqlite_store/
lib.rs

1//! SQLite-backed Store implementation for miden-client. This crate provides `SqliteStore` and its
2//! full implementation.
3//!
4//! [`SqliteStore`] enables the persistence of accounts, transactions, notes, block headers, and MMR
5//! nodes using an `SQLite` database.
6
7use std::boxed::Box;
8use std::collections::{BTreeMap, BTreeSet};
9use std::path::{Path, PathBuf};
10use std::rc::Rc;
11use std::string::{String, ToString};
12use std::time::Duration;
13use std::vec::Vec;
14
15use db_management::migration::SqliteMigrator;
16use db_management::pool_manager::{Pool, SqlitePoolManager};
17use deadpool::Runtime;
18use miden_client::Word;
19use miden_client::account::{
20    Account,
21    AccountCode,
22    AccountHeader,
23    AccountId,
24    AccountStorage,
25    Address,
26    StorageMapKey,
27    StorageSlotName,
28};
29use miden_client::asset::{Asset, AssetVault, AssetWitness};
30use miden_client::block::{AccountWitness, BlockHeader};
31use miden_client::crypto::{InOrderIndex, MmrPeaks};
32use miden_client::note::{BlockNumber, NoteScript, NoteTag, Nullifier};
33use miden_client::store::{
34    AccountRecord,
35    AccountStatus,
36    AccountStorageFilter,
37    BlockRelevance,
38    ClientAccountType,
39    InputNoteCursor,
40    InputNoteRecord,
41    NoteFilter,
42    OutputNoteRecord,
43    PartialBlockchainFilter,
44    SettingMutation,
45    SettingScope,
46    Store,
47    StoreError,
48    TransactionFilter,
49};
50use miden_client::sync::{NoteTagRecord, StateSyncUpdate};
51use miden_client::transaction::{TransactionRecord, TransactionStoreUpdate};
52use miden_client::utils::Serializable;
53use miden_client_proto as proto;
54use miden_protocol::Felt;
55use miden_protocol::account::StorageMapWitness;
56use miden_protocol::asset::AssetId;
57use rusqlite::Connection;
58use rusqlite::types::Value;
59use sql_error::SqlResultExt;
60
61use crate::account::rows::query_vault_assets;
62
63mod account;
64mod builder;
65mod chain_data;
66mod db_management;
67mod forest;
68mod note;
69mod settings;
70mod sql_error;
71mod sync;
72mod transaction;
73
74pub use builder::ClientBuilderSqliteExt;
75
76// SQLITE STORE
77// ================================================================================================
78
79/// `SQLite`-backed [`Store`] implementation.
80///
81/// Current table definitions are the result of applying every migration under `migrations/` in
82/// order.
83pub struct SqliteStore {
84    pub(crate) pool: Pool,
85    database_filepath: PathBuf,
86}
87
88impl SqliteStore {
89    // CONSTRUCTORS
90    // --------------------------------------------------------------------------------------------
91
92    /// Returns a new instance of [Store] instantiated with the specified configuration options.
93    pub async fn new(database_filepath: PathBuf) -> Result<Self, StoreError> {
94        if database_filepath.to_str().is_none() {
95            return Err(database_error(format!(
96                "database path is not valid UTF-8: {}",
97                database_filepath.display()
98            )));
99        }
100
101        let sqlite_pool_manager = SqlitePoolManager::new(database_filepath.clone());
102        let pool = Pool::builder(sqlite_pool_manager)
103            .wait_timeout(Some(Duration::from_secs(30)))
104            .runtime(Runtime::Tokio1)
105            .build()
106            .map_err(database_error)?;
107
108        Self::migrate(&pool, SqliteMigrator::client()).await?;
109
110        // Account SMT data is persisted in the forest tables and read on demand, so no state needs
111        // to be rebuilt here.
112        Ok(SqliteStore { pool, database_filepath })
113    }
114
115    /// Returns the path of the database file backing this store.
116    pub fn database_filepath(&self) -> &Path {
117        &self.database_filepath
118    }
119
120    /// Brings the database in `pool` up to the latest version of the schema `migration` builds.
121    ///
122    /// The upgrade is verified before it is committed, so a failure is rolled back by `SQLite` and
123    /// leaves the store exactly as it was.
124    async fn migrate(pool: &Pool, migration: &'static SqliteMigrator) -> Result<(), StoreError> {
125        let conn = pool.get().await.map_err(database_error)?;
126
127        conn.interact(move |conn| migration.apply(conn))
128            .await
129            .map_err(database_error)?
130            .map_err(database_error)
131    }
132
133    /// Interacts with the database by executing the provided function on a connection from the
134    /// pool.
135    ///
136    /// This function is a helper method which simplifies the process of making queries to the
137    /// database. It acquires a connection from the pool and executes the provided function,
138    /// returning the result.
139    async fn interact_with_connection<F, R>(&self, f: F) -> Result<R, StoreError>
140    where
141        F: FnOnce(&mut Connection) -> Result<R, StoreError> + Send + 'static,
142        R: Send + 'static,
143    {
144        self.pool
145            .get()
146            .await
147            .map_err(database_error)?
148            .interact(f)
149            .await
150            .map_err(database_error)?
151    }
152}
153
154// SQLite implementation of the Store trait
155//
156// To simplify, all implementations rely on inner SqliteStore functions that map 1:1 by name This
157// way, the actual implementations are grouped by entity types in their own sub-modules
158#[async_trait::async_trait]
159impl Store for SqliteStore {
160    fn identifier(&self) -> &str {
161        self.database_filepath
162            .to_str()
163            .expect("rejected by SqliteStore::new when not UTF-8")
164    }
165
166    fn get_current_timestamp(&self) -> Option<u64> {
167        Some(current_timestamp_u64())
168    }
169
170    async fn get_note_tags(&self) -> Result<Vec<NoteTagRecord>, StoreError> {
171        self.interact_with_connection(SqliteStore::get_note_tags).await
172    }
173
174    async fn get_unique_note_tags(&self) -> Result<BTreeSet<NoteTag>, StoreError> {
175        self.interact_with_connection(SqliteStore::get_unique_note_tags).await
176    }
177
178    async fn add_note_tag(&self, tag: NoteTagRecord) -> Result<bool, StoreError> {
179        self.interact_with_connection(move |conn| SqliteStore::add_note_tag(conn, tag))
180            .await
181    }
182
183    async fn remove_note_tag(&self, tag: NoteTagRecord) -> Result<usize, StoreError> {
184        self.interact_with_connection(move |conn| SqliteStore::remove_note_tag(conn, tag))
185            .await
186    }
187
188    async fn get_sync_height(&self) -> Result<BlockNumber, StoreError> {
189        self.interact_with_connection(SqliteStore::get_sync_height).await
190    }
191
192    async fn apply_state_sync(&self, state_sync_update: StateSyncUpdate) -> Result<(), StoreError> {
193        self.interact_with_connection(move |conn| {
194            SqliteStore::apply_state_sync(conn, state_sync_update)
195        })
196        .await
197    }
198
199    async fn get_transactions(
200        &self,
201        transaction_filter: TransactionFilter,
202    ) -> Result<Vec<TransactionRecord>, StoreError> {
203        self.interact_with_connection(move |conn| {
204            SqliteStore::get_transactions(conn, &transaction_filter)
205        })
206        .await
207    }
208
209    async fn apply_transaction(&self, tx_update: TransactionStoreUpdate) -> Result<(), StoreError> {
210        self.interact_with_connection(move |conn| SqliteStore::apply_transaction(conn, &tx_update))
211            .await
212    }
213
214    async fn apply_transaction_batch(
215        &self,
216        tx_updates: Vec<TransactionStoreUpdate>,
217    ) -> Result<(), StoreError> {
218        self.interact_with_connection(move |conn| {
219            SqliteStore::apply_transaction_batch(conn, &tx_updates)
220        })
221        .await
222    }
223
224    async fn get_input_notes(
225        &self,
226        filter: NoteFilter,
227    ) -> Result<Vec<InputNoteRecord>, StoreError> {
228        self.interact_with_connection(move |conn| SqliteStore::get_input_notes(conn, &filter))
229            .await
230    }
231
232    async fn get_output_notes(
233        &self,
234        note_filter: NoteFilter,
235    ) -> Result<Vec<OutputNoteRecord>, StoreError> {
236        self.interact_with_connection(move |conn| SqliteStore::get_output_notes(conn, &note_filter))
237            .await
238    }
239
240    async fn get_input_note_after(
241        &self,
242        filter: NoteFilter,
243        consumer: AccountId,
244        block_start: Option<BlockNumber>,
245        block_end: Option<BlockNumber>,
246        cursor: Option<InputNoteCursor>,
247    ) -> Result<Option<InputNoteRecord>, StoreError> {
248        self.interact_with_connection(move |conn| {
249            SqliteStore::get_input_note_after(
250                conn,
251                &filter,
252                consumer,
253                block_start,
254                block_end,
255                cursor,
256            )
257        })
258        .await
259    }
260
261    async fn upsert_input_notes(&self, notes: &[InputNoteRecord]) -> Result<(), StoreError> {
262        let notes = notes.to_vec();
263        self.interact_with_connection(move |conn| SqliteStore::upsert_input_notes(conn, &notes))
264            .await
265    }
266
267    async fn get_note_script(&self, script_root: Word) -> Result<NoteScript, StoreError> {
268        self.interact_with_connection(move |conn| SqliteStore::get_note_script(conn, script_root))
269            .await
270    }
271
272    async fn upsert_note_scripts(&self, note_scripts: &[NoteScript]) -> Result<(), StoreError> {
273        let note_scripts = note_scripts.to_vec();
274        self.interact_with_connection(move |conn| {
275            SqliteStore::upsert_note_scripts(conn, &note_scripts)
276        })
277        .await
278    }
279
280    async fn insert_block_header(
281        &self,
282        block_header: &BlockHeader,
283        nodes: &[(InOrderIndex, Word)],
284        has_client_notes: bool,
285    ) -> Result<(), StoreError> {
286        let block_header = block_header.clone();
287        let nodes = nodes.to_vec();
288        self.interact_with_connection(move |conn| {
289            SqliteStore::insert_block_header(conn, &block_header, &nodes, has_client_notes)
290        })
291        .await
292    }
293
294    async fn untrack_and_prune_irrelevant_blocks(
295        &self,
296        blocks_to_untrack: &[BlockNumber],
297        node_indices_to_remove: &[InOrderIndex],
298    ) -> Result<(), StoreError> {
299        let blocks_to_untrack = blocks_to_untrack.to_vec();
300        let node_indices_to_remove = node_indices_to_remove.to_vec();
301        self.interact_with_connection(move |conn| {
302            SqliteStore::untrack_and_prune_irrelevant_blocks(
303                conn,
304                &blocks_to_untrack,
305                &node_indices_to_remove,
306            )
307        })
308        .await
309    }
310
311    async fn prune_account_history(
312        &self,
313        account_id: AccountId,
314        up_to_nonce: Felt,
315    ) -> Result<usize, StoreError> {
316        self.interact_with_connection(move |conn| {
317            SqliteStore::prune_account_history(conn, account_id, up_to_nonce)
318        })
319        .await
320    }
321
322    async fn get_block_headers(
323        &self,
324        block_numbers: &BTreeSet<BlockNumber>,
325    ) -> Result<Vec<(BlockHeader, BlockRelevance)>, StoreError> {
326        let block_numbers = block_numbers.clone();
327        self.interact_with_connection(move |conn| {
328            SqliteStore::get_block_headers(conn, &block_numbers)
329        })
330        .await
331    }
332
333    async fn get_tracked_block_headers(&self) -> Result<Vec<BlockHeader>, StoreError> {
334        self.interact_with_connection(SqliteStore::get_tracked_block_headers).await
335    }
336
337    async fn get_tracked_block_header_numbers(&self) -> Result<BTreeSet<usize>, StoreError> {
338        self.interact_with_connection(SqliteStore::get_tracked_block_header_numbers)
339            .await
340    }
341
342    async fn get_partial_blockchain_nodes(
343        &self,
344        filter: PartialBlockchainFilter,
345    ) -> Result<BTreeMap<InOrderIndex, Word>, StoreError> {
346        self.interact_with_connection(move |conn| {
347            SqliteStore::get_partial_blockchain_nodes(conn, &filter)
348        })
349        .await
350    }
351
352    async fn get_current_blockchain_peaks(&self) -> Result<MmrPeaks, StoreError> {
353        self.interact_with_connection(SqliteStore::get_current_blockchain_peaks).await
354    }
355
356    async fn insert_account(
357        &self,
358        account: &Account,
359        initial_address: Address,
360        client_account_type: ClientAccountType,
361    ) -> Result<(), StoreError> {
362        let cloned_account = account.clone();
363
364        self.interact_with_connection(move |conn| {
365            SqliteStore::insert_account(
366                conn,
367                &cloned_account,
368                &initial_address,
369                client_account_type,
370            )
371        })
372        .await
373    }
374
375    async fn update_account(&self, account: &Account) -> Result<(), StoreError> {
376        let cloned_account = account.clone();
377
378        self.interact_with_connection(move |conn| {
379            SqliteStore::update_account(conn, &cloned_account)
380        })
381        .await
382    }
383
384    async fn get_account_ids(&self) -> Result<Vec<AccountId>, StoreError> {
385        self.interact_with_connection(SqliteStore::get_account_ids).await
386    }
387
388    async fn get_account_headers(&self) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
389        self.interact_with_connection(SqliteStore::get_account_headers).await
390    }
391
392    async fn get_account_header(
393        &self,
394        account_id: AccountId,
395    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
396        self.interact_with_connection(move |conn| SqliteStore::get_account_header(conn, account_id))
397            .await
398    }
399
400    async fn get_account_header_by_commitment(
401        &self,
402        account_commitment: Word,
403    ) -> Result<Option<AccountHeader>, StoreError> {
404        self.interact_with_connection(move |conn| {
405            SqliteStore::get_account_header_by_commitment(conn, account_commitment)
406        })
407        .await
408    }
409
410    async fn get_account(
411        &self,
412        account_id: AccountId,
413    ) -> Result<Option<AccountRecord>, StoreError> {
414        self.interact_with_connection(move |conn| SqliteStore::get_account(conn, account_id))
415            .await
416    }
417
418    async fn get_account_code(
419        &self,
420        account_id: AccountId,
421    ) -> Result<Option<AccountCode>, StoreError> {
422        self.interact_with_connection(move |conn| {
423            SqliteStore::get_account_code_by_id(conn, account_id)
424        })
425        .await
426    }
427
428    async fn upsert_foreign_account_code(
429        &self,
430        account_id: AccountId,
431        code: AccountCode,
432    ) -> Result<(), StoreError> {
433        self.interact_with_connection(move |conn| {
434            SqliteStore::upsert_foreign_account_code(conn, account_id, &code)
435        })
436        .await
437    }
438
439    async fn get_foreign_account_code(
440        &self,
441        account_ids: Vec<AccountId>,
442    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
443        self.interact_with_connection(move |conn| {
444            SqliteStore::get_foreign_account_code(conn, account_ids)
445        })
446        .await
447    }
448
449    async fn track_account_witness(&self, account_id: AccountId) -> Result<bool, StoreError> {
450        self.interact_with_connection(move |conn| {
451            SqliteStore::track_account_witness(conn, account_id)
452        })
453        .await
454    }
455
456    async fn untrack_account_witness(&self, account_id: AccountId) -> Result<bool, StoreError> {
457        self.interact_with_connection(move |conn| {
458            SqliteStore::untrack_account_witness(conn, account_id)
459        })
460        .await
461    }
462
463    async fn tracked_account_witnesses(&self) -> Result<Vec<AccountId>, StoreError> {
464        self.interact_with_connection(SqliteStore::tracked_account_witnesses).await
465    }
466
467    async fn get_account_witness(
468        &self,
469        account_id: AccountId,
470    ) -> Result<Option<AccountWitness>, StoreError> {
471        self.interact_with_connection(move |conn| {
472            SqliteStore::get_account_witness(conn, account_id)
473        })
474        .await
475    }
476
477    async fn update_account_witness(
478        &self,
479        account_id: AccountId,
480        witness: &AccountWitness,
481    ) -> Result<bool, StoreError> {
482        let witness = witness.clone();
483        self.interact_with_connection(move |conn| {
484            SqliteStore::update_account_witness(conn, account_id, &witness)
485        })
486        .await
487    }
488
489    async fn set_setting(
490        &self,
491        scope: SettingScope,
492        key: String,
493        value: Vec<u8>,
494    ) -> Result<(), StoreError> {
495        self.interact_with_connection(move |conn| {
496            SqliteStore::set_setting(conn, scope, &key, &value)
497        })
498        .await
499    }
500
501    async fn get_setting(
502        &self,
503        scope: SettingScope,
504        key: String,
505    ) -> Result<Option<Vec<u8>>, StoreError> {
506        self.interact_with_connection(move |conn| SqliteStore::get_setting(conn, scope, &key))
507            .await
508    }
509
510    async fn remove_setting(&self, scope: SettingScope, key: String) -> Result<bool, StoreError> {
511        self.interact_with_connection(move |conn| SqliteStore::remove_setting(conn, scope, &key))
512            .await
513    }
514
515    async fn list_setting_keys(&self, scope: SettingScope) -> Result<Vec<String>, StoreError> {
516        self.interact_with_connection(move |conn| SqliteStore::list_setting_keys(conn, scope))
517            .await
518    }
519
520    async fn apply_settings_mutations(
521        &self,
522        scope: SettingScope,
523        mutations: Vec<SettingMutation>,
524    ) -> Result<(), StoreError> {
525        self.interact_with_connection(move |conn| {
526            with_write_tx(conn, |tx| {
527                for mutation in &mutations {
528                    match mutation {
529                        SettingMutation::Set { key, value } => {
530                            SqliteStore::set_setting(tx, scope, key, value)?;
531                        },
532                        SettingMutation::Remove { key } => {
533                            SqliteStore::remove_setting(tx, scope, key)?;
534                        },
535                    }
536                }
537                Ok(())
538            })
539        })
540        .await
541    }
542
543    async fn get_unspent_input_note_nullifiers(&self) -> Result<Vec<Nullifier>, StoreError> {
544        self.interact_with_connection(SqliteStore::get_unspent_input_note_nullifiers)
545            .await
546    }
547
548    async fn get_account_vault(&self, account_id: AccountId) -> Result<AssetVault, StoreError> {
549        self.interact_with_connection(move |conn| SqliteStore::get_account_vault(conn, account_id))
550            .await
551    }
552
553    async fn get_account_assets(&self, account_id: AccountId) -> Result<Vec<Asset>, StoreError> {
554        self.interact_with_connection(move |conn| query_vault_assets(conn, account_id))
555            .await
556    }
557
558    async fn get_vault_asset_witnesses(
559        &self,
560        account_id: AccountId,
561        vault_root: Word,
562        asset_ids: BTreeSet<AssetId>,
563    ) -> Result<Vec<AssetWitness>, StoreError> {
564        self.interact_with_connection(move |conn| {
565            SqliteStore::get_vault_asset_witnesses(conn, account_id, vault_root, asset_ids)
566        })
567        .await
568    }
569
570    async fn get_account_asset(
571        &self,
572        account_id: AccountId,
573        asset_id: AssetId,
574    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
575        self.interact_with_connection(move |conn| {
576            SqliteStore::get_account_asset(conn, account_id, asset_id)
577        })
578        .await
579    }
580
581    async fn get_account_map_item(
582        &self,
583        account_id: AccountId,
584        slot_name: StorageSlotName,
585        key: StorageMapKey,
586    ) -> Result<(Word, StorageMapWitness), StoreError> {
587        self.interact_with_connection(move |conn| {
588            SqliteStore::get_account_map_item(conn, account_id, slot_name, key)
589        })
590        .await
591    }
592
593    async fn get_account_storage(
594        &self,
595        account_id: AccountId,
596        filter: AccountStorageFilter,
597    ) -> Result<AccountStorage, StoreError> {
598        self.interact_with_connection(move |conn| {
599            SqliteStore::get_account_storage(conn, account_id, &filter)
600        })
601        .await
602    }
603
604    async fn get_addresses_by_account_id(
605        &self,
606        account_id: AccountId,
607    ) -> Result<Vec<Address>, StoreError> {
608        self.interact_with_connection(move |conn| {
609            SqliteStore::get_account_addresses(conn, account_id)
610        })
611        .await
612    }
613
614    async fn insert_address(
615        &self,
616        address: Address,
617        account_id: AccountId,
618    ) -> Result<(), StoreError> {
619        self.interact_with_connection(move |conn| {
620            SqliteStore::insert_address(conn, &address, account_id)
621        })
622        .await
623    }
624
625    async fn remove_address(&self, address: Address) -> Result<bool, StoreError> {
626        self.interact_with_connection(move |conn| SqliteStore::remove_address(conn, &address))
627            .await
628    }
629
630    async fn get_minimal_partial_account(
631        &self,
632        account_id: AccountId,
633    ) -> Result<Option<AccountRecord>, StoreError> {
634        self.interact_with_connection(move |conn| {
635            SqliteStore::get_minimal_partial_account(conn, account_id)
636        })
637        .await
638    }
639}
640
641// UTILS
642// ================================================================================================
643
644fn database_error(err: impl core::fmt::Display) -> StoreError {
645    StoreError::DatabaseError(err.to_string())
646}
647
648/// Returns the current UTC timestamp as `u64` (non-leap seconds since Unix epoch).
649pub(crate) fn current_timestamp_u64() -> u64 {
650    let now = chrono::Utc::now();
651    u64::try_from(now.timestamp()).expect("timestamp is always after epoch")
652}
653
654/// Gets a `u64` value from the database.
655///
656/// `Sqlite` uses `i64` as its internal representation format, and so when retrieving we need to
657/// make sure we cast as `u64` to get the original value
658pub(crate) fn column_value_as_u64<I: rusqlite::RowIndex>(
659    row: &rusqlite::Row<'_>,
660    index: I,
661) -> rusqlite::Result<u64> {
662    let value: i64 = row.get(index)?;
663    #[allow(
664        clippy::cast_sign_loss,
665        reason = "We store u64 as i64 as sqlite only allows the latter."
666    )]
667    Ok(value as u64)
668}
669
670/// Converts a `u64` into a [Value].
671///
672/// `Sqlite` uses `i64` as its internal representation format. Note that the `as` operator performs
673/// a lossless conversion from `u64` to `i64`.
674pub(crate) fn u64_to_value(v: u64) -> Value {
675    #[allow(
676        clippy::cast_possible_wrap,
677        reason = "We store u64 as i64 as sqlite only allows the latter."
678    )]
679    Value::Integer(v as i64)
680}
681
682/// Builds the value list for a `rarray(?)` parameter from serializable items, each stored as a BLOB
683/// of its canonical byte encoding.
684///
685/// Binding the list as a single table-valued parameter keeps the SQL text constant, so the prepared
686/// statement stays cacheable regardless of the list length (and the list is not subject to
687/// `SQLite`'s bound-parameter limit).
688pub(crate) fn blob_array<T: Serializable>(items: impl IntoIterator<Item = T>) -> Rc<Vec<Value>> {
689    Rc::new(items.into_iter().map(|item| Value::Blob(item.to_bytes())).collect())
690}
691
692/// Builds the value list for a `rarray(?)` parameter from `u64` values, stored as SQL INTEGERs
693/// through the same bit-cast as [`u64_to_value`].
694pub(crate) fn int_array(items: impl IntoIterator<Item = u64>) -> Rc<Vec<Value>> {
695    Rc::new(items.into_iter().map(u64_to_value).collect())
696}
697
698/// Builds the value list for a `rarray(?)` parameter from string values, stored as SQL TEXT.
699pub(crate) fn text_array(items: impl IntoIterator<Item = String>) -> Rc<Vec<Value>> {
700    Rc::new(items.into_iter().map(Value::Text).collect())
701}
702
703/// Runs `f` inside an `IMMEDIATE` rusqlite transaction. Commits on `Ok`, rolls back on `Err`.
704///
705/// The closure must write. An `IMMEDIATE` transaction takes the write lock at `BEGIN`, so a closure
706/// that reads and then writes cannot lose the lock upgrade in between. In WAL mode that upgrade
707/// fails with `SQLITE_BUSY_SNAPSHOT`, which the busy timeout does not retry.
708pub(crate) fn with_write_tx<R>(
709    conn: &mut Connection,
710    f: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<R, StoreError>,
711) -> Result<R, StoreError> {
712    let tx = conn
713        .transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)
714        .into_store_error()?;
715    let result = f(&tx)?;
716    tx.commit().into_store_error()?;
717    Ok(result)
718}
719
720// TESTS
721// ================================================================================================
722
723#[cfg(test)]
724pub mod tests {
725    use std::boxed::Box;
726    use std::sync::LazyLock;
727
728    use miden_client::store::Store;
729    use miden_client::testing::common::create_test_store_path;
730
731    use super::db_management::migration::SqliteMigrator;
732    use super::db_management::migration::tests::damaging_migration;
733    use super::db_management::pool_manager::SqlitePoolManager;
734    use super::{Pool, SqliteStore, StoreError, column_value_as_u64, u64_to_value, with_write_tx};
735
736    /// A migration set that changes the store and is then rejected, which is the failure the
737    /// rollback has to undo.
738    static DAMAGING_MIGRATION: LazyLock<SqliteMigrator> = LazyLock::new(damaging_migration);
739
740    #[tokio::test]
741    async fn failed_migration_leaves_the_store_as_it_was() {
742        let database_filepath = create_test_store_path();
743        drop(SqliteStore::new(database_filepath.clone()).await.unwrap());
744
745        let pool = Pool::builder(SqlitePoolManager::new(database_filepath.clone()))
746            .build()
747            .unwrap();
748        let err = SqliteStore::migrate(&pool, &DAMAGING_MIGRATION).await.unwrap_err();
749
750        assert!(
751            err.to_string().contains("produced a schema this client does not expect"),
752            "the migration should have been rejected, got {err}"
753        );
754        // Reopening verifies the schema, so it only succeeds if the dropped table is still there.
755        SqliteStore::new(database_filepath).await.unwrap();
756    }
757
758    fn assert_send_sync<T: Send + Sync>() {}
759
760    /// The write path bit-casts `u64` to `i64` and the read path must bit-cast it back, including
761    /// for values whose top bit is set (which are stored as negative SQL INTEGERs).
762    #[test]
763    fn u64_column_round_trip() {
764        let conn = rusqlite::Connection::open_in_memory().unwrap();
765        for value in [0u64, 1, 1 << 63, u64::MAX] {
766            let read: u64 = conn
767                .query_row("SELECT ?1", [u64_to_value(value)], |row| column_value_as_u64(row, 0))
768                .unwrap();
769            assert_eq!(read, value);
770        }
771    }
772
773    #[test]
774    fn with_write_tx_rolls_back_on_error() {
775        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
776        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);").unwrap();
777
778        let result = with_write_tx(&mut conn, |tx| {
779            tx.execute("INSERT INTO t (id) VALUES (1)", []).unwrap();
780            Err::<(), _>(StoreError::DatabaseError("forced failure".into()))
781        });
782        assert!(result.is_err());
783
784        let count: i64 = conn.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0)).unwrap();
785        assert_eq!(count, 0, "the insert must roll back when the closure errors");
786    }
787
788    #[test]
789    fn with_write_tx_commits_on_success() {
790        let mut conn = rusqlite::Connection::open_in_memory().unwrap();
791        conn.execute_batch("CREATE TABLE t (id INTEGER PRIMARY KEY);").unwrap();
792
793        with_write_tx(&mut conn, |tx| {
794            tx.execute("INSERT INTO t (id) VALUES (1)", []).unwrap();
795            Ok(())
796        })
797        .unwrap();
798
799        let count: i64 = conn.query_row("SELECT COUNT(*) FROM t", [], |row| row.get(0)).unwrap();
800        assert_eq!(count, 1);
801    }
802
803    #[test]
804    fn is_send_sync() {
805        assert_send_sync::<SqliteStore>();
806        assert_send_sync::<Box<dyn Store>>();
807    }
808
809    // Function that returns a `Send` future from a dynamic trait that must be `Sync`.
810    async fn dyn_trait_send_fut(store: Box<dyn Store>) {
811        // This wouldn't compile if `get_tracked_block_headers` doesn't return a `Send` future.
812        let res = store.get_tracked_block_headers().await;
813        assert!(res.is_ok());
814    }
815
816    #[tokio::test]
817    async fn future_is_send() {
818        let client = SqliteStore::new(create_test_store_path()).await.unwrap();
819        let client: Box<SqliteStore> = client.into();
820        tokio::task::spawn(async move { dyn_trait_send_fut(client).await });
821    }
822
823    pub(crate) async fn create_test_store() -> SqliteStore {
824        SqliteStore::new(create_test_store_path()).await.unwrap()
825    }
826}