1use 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
76pub struct SqliteStore {
84 pub(crate) pool: Pool,
85 database_filepath: PathBuf,
86}
87
88impl SqliteStore {
89 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 Ok(SqliteStore { pool, database_filepath })
113 }
114
115 pub fn database_filepath(&self) -> &Path {
117 &self.database_filepath
118 }
119
120 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 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#[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, ¬e_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, ¬es))
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, ¬e_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
641fn database_error(err: impl core::fmt::Display) -> StoreError {
645 StoreError::DatabaseError(err.to_string())
646}
647
648pub(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
654pub(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
670pub(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
682pub(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
692pub(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
698pub(crate) fn text_array(items: impl IntoIterator<Item = String>) -> Rc<Vec<Value>> {
700 Rc::new(items.into_iter().map(Value::Text).collect())
701}
702
703pub(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#[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 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 SqliteStore::new(database_filepath).await.unwrap();
756 }
757
758 fn assert_send_sync<T: Send + Sync>() {}
759
760 #[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 async fn dyn_trait_send_fut(store: Box<dyn Store>) {
811 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}