1mod pool;
2mod sqlcipher_connection;
3
4use crate::StorageError;
5use crate::database::instrumentation::TestInstrumentation;
6use crate::{ConnectionError, ConnectionExt, DbConnection, NotFound};
8use arc_swap::ArcSwapOption;
9use diesel::sqlite::SqliteConnection;
10use diesel::{
11 Connection,
12 connection::SimpleConnection,
13 r2d2::{self, CustomizeConnection, PooledConnection},
14};
15use parking_lot::Mutex;
16use std::sync::Arc;
17use thiserror::Error;
18use xmtp_common::{BoxDynError, ErrorCode, RetryableError, retryable};
19use xmtp_configuration::{BUSY_TIMEOUT, MAX_DB_POOL_SIZE, MIN_DB_POOL_SIZE};
20
21use pool::*;
22
23pub type RawDbConnection = PooledConnection<ConnectionManager>;
24
25pub use self::sqlcipher_connection::EncryptedConnection;
26use crate::{EncryptionKey, StorageOption, XmtpDb};
27
28use super::PersistentOrMem;
29
30trait XmtpConnection:
31 ValidatedConnection
32 + ConnectionOptions
33 + CustomizeConnection<SqliteConnection, r2d2::Error>
34 + dyn_clone::DynClone
35{
36}
37
38trait ConnectionOptions {
39 fn options(&self) -> &StorageOption;
40 fn is_persistent(&self) -> bool {
41 matches!(self.options(), StorageOption::Persistent(_))
42 }
43}
44
45impl<T> XmtpConnection for T where
46 T: ValidatedConnection
47 + CustomizeConnection<SqliteConnection, r2d2::Error>
48 + ConnectionOptions
49 + dyn_clone::DynClone
50{
51}
52
53dyn_clone::clone_trait_object!(XmtpConnection);
54
55pub(crate) trait ValidatedConnection {
56 fn validate(&self, _conn: &mut SqliteConnection) -> Result<(), PlatformStorageError> {
57 Ok(())
58 }
59}
60
61fn connection_pragmas(c: &mut impl SimpleConnection) -> diesel::result::QueryResult<()> {
66 c.batch_execute(&format!("PRAGMA busy_timeout = {};", BUSY_TIMEOUT))?; c.batch_execute("PRAGMA synchronous = NORMAL;")?; c.batch_execute("PRAGMA wal_autocheckpoint = 1000;")?; c.batch_execute("PRAGMA wal_checkpoint(TRUNCATE);")?; c.batch_execute("PRAGMA query_only = OFF;")?; c.batch_execute("PRAGMA journal_size_limit = 67108864")?; c.batch_execute("PRAGMA mmap_size = 134217728")?; c.batch_execute("PRAGMA cache_size = 2000")?; c.batch_execute("PRAGMA foreign_keys = ON;")?; Ok(())
79}
80
81#[derive(Clone, Debug)]
86pub struct UnencryptedConnection {
87 options: StorageOption,
88}
89
90impl UnencryptedConnection {
91 pub fn new(options: StorageOption) -> Self {
92 Self { options }
93 }
94}
95
96impl ValidatedConnection for UnencryptedConnection {}
97
98impl ConnectionOptions for UnencryptedConnection {
99 fn options(&self) -> &StorageOption {
100 &self.options
101 }
102}
103
104impl CustomizeConnection<SqliteConnection, r2d2::Error> for UnencryptedConnection {
105 fn on_acquire(&self, c: &mut SqliteConnection) -> Result<(), r2d2::Error> {
106 if cfg!(any(test, feature = "test-utils")) {
107 c.set_instrumentation(TestInstrumentation);
108 }
109 connection_pragmas(c)?;
110 Ok(())
111 }
112}
113
114impl ConnectionOptions for NopConnection {
115 fn options(&self) -> &StorageOption {
116 &self.options
117 }
118}
119
120#[derive(Clone, Debug)]
121pub struct NopConnection {
122 options: StorageOption,
123}
124
125impl Default for NopConnection {
126 fn default() -> Self {
127 NopConnection {
128 options: StorageOption::Ephemeral,
129 }
130 }
131}
132
133impl ValidatedConnection for NopConnection {}
134impl CustomizeConnection<SqliteConnection, r2d2::Error> for NopConnection {
135 fn on_acquire(&self, c: &mut SqliteConnection) -> Result<(), r2d2::Error> {
136 if cfg!(any(test, feature = "test-utils")) {
137 c.set_instrumentation(TestInstrumentation);
138 }
139 Ok(())
140 }
141}
142
143impl StorageOption {
144 pub(super) fn path(&self) -> Option<&String> {
145 use StorageOption::*;
146 match self {
147 Persistent(path) => Some(path),
148 _ => None,
149 }
150 }
151}
152
153#[derive(Debug, Error, ErrorCode)]
154pub enum PlatformStorageError {
155 #[error("Pool error: {0}")]
159 Pool(#[from] diesel::r2d2::PoolError),
160 #[error("Error with connection to Sqlite {0}")]
165 DbConnection(#[from] diesel::r2d2::Error),
166 #[error("Pool needs to reconnect before use")]
170 PoolNeedsConnection,
171 #[error("Using a DB Pool requires a persistent path")]
175 PoolRequiresPath,
176 #[error("The SQLCipher Sqlite extension is not present, but an encryption key is given")]
180 SqlCipherNotLoaded,
181 #[error("PRAGMA key or salt has incorrect value")]
185 SqlCipherKeyIncorrect,
186 #[error("Database is locked")]
190 DatabaseLocked,
191 #[error(transparent)]
195 DieselResult(#[from] diesel::result::Error),
196 #[error(transparent)]
200 NotFound(#[from] NotFound),
201 #[error(transparent)]
205 Io(#[from] std::io::Error),
206 #[error(transparent)]
210 FromHex(#[from] hex::FromHexError),
211 #[error(transparent)]
215 DieselConnect(#[from] diesel::ConnectionError),
216 #[error(transparent)]
220 Boxed(#[from] BoxDynError),
221}
222
223impl RetryableError for PlatformStorageError {
224 fn is_retryable(&self) -> bool {
225 match self {
226 Self::Pool(_) => true,
227 Self::DbConnection(_) => true,
231 Self::SqlCipherNotLoaded => true,
232 Self::PoolNeedsConnection => true,
233 Self::SqlCipherKeyIncorrect => false,
234 Self::DatabaseLocked => true,
235 Self::DieselResult(result) => retryable!(result),
236 Self::Io(_) => true,
237 Self::DieselConnect(_) => true,
238
239 _ => false,
240 }
241 }
242}
243
244#[derive(Clone, Debug)]
246pub struct NativeDb {
247 customizer: Box<dyn XmtpConnection>,
248 conn: Arc<PersistentOrMem<NativeDbConnection, SingleDbConnection, EphemeralDbConnection>>,
249 opts: StorageOption,
250}
251
252use native_db_builder::{Empty, IsComplete, IsSet, IsUnset, SetKey, SetOpts, SetSingleConnection};
253
254impl NativeDb {
255 pub fn builder() -> NativeDbBuilder<Empty> {
256 native_db()
257 }
258}
259
260#[bon::builder]
261pub fn native_db(
262 #[builder(setters(vis = "", name = opts_internal))] opts: StorageOption,
263 #[builder(required, setters(vis = "", name = key_internal))] key: Option<EncryptionKey>,
264 #[builder(default = MAX_DB_POOL_SIZE)] max_pool_size: u32,
265 #[builder(default = MIN_DB_POOL_SIZE)]
267 min_pool_size: u32,
268 #[builder(default = false, setters(vis = "", name = single_connection_internal))]
272 single_connection: bool,
273) -> Result<NativeDb, StorageError> {
274 NativeDb::new_inner(&opts, key, max_pool_size, min_pool_size, single_connection)
275 .map_err(Into::into)
276}
277
278impl<S: native_db_builder::State> NativeDbBuilder<S> {
279 pub fn ephemeral(self) -> NativeDbBuilder<SetOpts<S>>
280 where
281 S::Opts: IsUnset,
282 {
283 self.opts_internal(StorageOption::Ephemeral)
284 }
285
286 pub fn persistent(self, path: impl Into<String>) -> NativeDbBuilder<SetOpts<S>>
287 where
288 S::Opts: IsUnset,
289 {
290 self.opts_internal(StorageOption::Persistent(path.into()))
291 }
292
293 pub fn key(self, key: impl Into<EncryptionKey>) -> NativeDbBuilder<SetKey<S>>
294 where
295 S::Key: IsUnset,
296 {
297 self.key_internal(Some(key.into()))
298 }
299
300 pub fn single_connection(self) -> NativeDbBuilder<SetSingleConnection<S>>
304 where
305 S::SingleConnection: IsUnset,
306 {
307 self.single_connection_internal(true)
308 }
309
310 pub fn build_unencrypted(self) -> Result<NativeDb, StorageError>
312 where
313 S::Key: IsUnset,
314 S::Opts: IsSet,
315 {
316 let this = self.key_internal(Option::<EncryptionKey>::None);
317 this.call()
318 }
319
320 pub fn build(self) -> Result<NativeDb, StorageError>
322 where
323 S: IsComplete,
324 {
325 self.call()
326 }
327}
328
329impl NativeDb {
330 fn new_inner(
332 opts: &StorageOption,
333 enc_key: Option<EncryptionKey>,
334 max_pool_size: u32,
335 min_pool_size: u32,
336 single_connection: bool,
337 ) -> Result<Self, PlatformStorageError> {
338 let customizer = if let Some(key) = enc_key {
339 let enc_connection = EncryptedConnection::new(key, opts)?;
340 if let Some(path) = enc_connection.options().path() {
341 let mut conn = SqliteConnection::establish(path)?;
342 enc_connection.validate(&mut conn)?;
343 }
344 Box::new(enc_connection) as Box<dyn XmtpConnection>
345 } else if matches!(opts, StorageOption::Persistent(_)) {
346 Box::new(UnencryptedConnection::new(opts.clone())) as Box<dyn XmtpConnection>
347 } else {
348 Box::new(NopConnection::default()) as Box<dyn XmtpConnection>
349 };
350 let conn = if customizer.is_persistent() {
351 if single_connection {
352 PersistentOrMem::Single(SingleDbConnection::new(customizer.clone())?)
353 } else {
354 PersistentOrMem::Persistent(NativeDbConnection::new(
355 customizer.clone(),
356 max_pool_size,
357 min_pool_size,
358 )?)
359 }
360 } else {
361 if single_connection {
362 tracing::info!(
363 "single_connection requested for an ephemeral database; ignoring (ephemeral is already single-connection)"
364 );
365 }
366 PersistentOrMem::Mem(EphemeralDbConnection::new()?)
367 };
368
369 Ok(Self {
370 opts: opts.clone(),
371 conn: conn.into(),
372 customizer,
373 })
374 }
375}
376
377impl XmtpDb for NativeDb {
378 type Connection =
379 Arc<PersistentOrMem<NativeDbConnection, SingleDbConnection, EphemeralDbConnection>>;
380 type DbQuery = DbConnection<Self::Connection>;
381
382 fn conn(&self) -> Self::Connection {
383 self.conn.clone()
384 }
385
386 fn db(&self) -> Self::DbQuery {
387 DbConnection::new(self.conn.clone())
388 }
389
390 fn opts(&self) -> &StorageOption {
391 &self.opts
392 }
393
394 fn validate(&self, conn: &mut SqliteConnection) -> Result<(), ConnectionError> {
395 self.customizer.validate(conn)?;
396 Ok(())
397 }
398
399 fn disconnect(&self) -> Result<(), ConnectionError> {
400 self.conn.disconnect()
401 }
402
403 fn reconnect(&self) -> Result<(), ConnectionError> {
404 self.conn.reconnect()
405 }
406}
407
408pub struct EphemeralDbConnection {
409 conn: Arc<Mutex<SqliteConnection>>,
410}
411
412impl std::fmt::Debug for EphemeralDbConnection {
413 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414 write!(
415 f,
416 "EphemeralConnection {{ is_locked={} }}",
417 self.conn.is_locked()
418 )
419 }
420}
421
422impl EphemeralDbConnection {
423 pub fn new() -> Result<Self, PlatformStorageError> {
424 let mut c = SqliteConnection::establish(":memory:")?;
425 UnencryptedConnection::on_acquire(
426 &UnencryptedConnection::new(StorageOption::Ephemeral),
427 &mut c,
428 )?;
429 Ok(Self {
430 conn: Arc::new(Mutex::new(c)),
431 })
432 }
433
434 fn db_disconnect(&self) -> Result<(), PlatformStorageError> {
435 Ok(())
436 }
437
438 fn db_reconnect(&self) -> Result<(), PlatformStorageError> {
439 let mut w = self.conn.lock();
440 let conn = SqliteConnection::establish(":memory:")?;
441 *w = conn;
442 Ok(())
443 }
444}
445
446impl ConnectionExt for EphemeralDbConnection {
447 fn raw_query<T, F>(&self, fun: F) -> Result<T, crate::ConnectionError>
448 where
449 F: FnOnce(&mut SqliteConnection) -> Result<T, diesel::result::Error>,
450 Self: Sized,
451 {
452 let mut conn = self.conn.lock();
453 fun(&mut conn).map_err(ConnectionError::from)
454 }
455
456 fn disconnect(&self) -> Result<(), crate::ConnectionError> {
457 Ok(self.db_disconnect()?)
458 }
459
460 fn reconnect(&self) -> Result<(), crate::ConnectionError> {
461 Ok(self.db_reconnect()?)
462 }
463}
464
465pub struct SingleDbConnection {
481 conn: Arc<Mutex<Option<SqliteConnection>>>,
482 customizer: Box<dyn XmtpConnection>,
483}
484
485impl std::fmt::Debug for SingleDbConnection {
486 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
487 let connected = match self.conn.try_lock() {
492 Some(guard) => guard.is_some().to_string(),
493 None => "<locked>".to_string(),
494 };
495 write!(
496 f,
497 "SingleDbConnection {{ path: {}, connected={} }}",
498 self.customizer.options(),
499 connected
500 )
501 }
502}
503
504impl SingleDbConnection {
505 fn new(customizer: Box<dyn XmtpConnection>) -> Result<Self, PlatformStorageError> {
506 let StorageOption::Persistent(path) = customizer.options() else {
507 return Err(PlatformStorageError::PoolRequiresPath);
508 };
509 let conn = Self::establish(path, &*customizer)?;
510 Ok(Self {
511 conn: Arc::new(Mutex::new(Some(conn))),
512 customizer,
513 })
514 }
515
516 fn establish(
520 path: &str,
521 customizer: &dyn XmtpConnection,
522 ) -> Result<SqliteConnection, PlatformStorageError> {
523 let mut conn = SqliteConnection::establish(path)?;
524 customizer
531 .on_acquire(&mut conn)
532 .map_err(PlatformStorageError::DbConnection)?;
533 conn.batch_execute(&format!("PRAGMA busy_timeout = {};", BUSY_TIMEOUT))?;
535 conn.batch_execute("PRAGMA journal_mode = WAL;")?;
536 Ok(conn)
537 }
538
539 fn db_disconnect(&self) -> Result<(), PlatformStorageError> {
544 tracing::warn!("single-connection: dropping sqlite connection (releasing file descriptor)");
545 *self.conn.lock() = None;
547 Ok(())
548 }
549
550 fn db_reconnect(&self) -> Result<(), PlatformStorageError> {
551 tracing::info!("single-connection: reconnecting sqlite database connection");
552 let StorageOption::Persistent(path) = self.customizer.options() else {
553 return Err(PlatformStorageError::PoolRequiresPath);
554 };
555 let mut guard = self.conn.lock();
562 *guard = None;
563 *guard = Some(Self::establish(path, &*self.customizer)?);
564 Ok(())
565 }
566}
567
568impl ConnectionExt for SingleDbConnection {
569 fn raw_query<T, F>(&self, fun: F) -> Result<T, crate::ConnectionError>
570 where
571 F: FnOnce(&mut SqliteConnection) -> Result<T, diesel::result::Error>,
572 Self: Sized,
573 {
574 let mut guard = self.conn.lock();
575 match guard.as_mut() {
576 Some(conn) => fun(conn).map_err(ConnectionError::from),
577 None => Err(ConnectionError::from(
580 PlatformStorageError::PoolNeedsConnection,
581 )),
582 }
583 }
584
585 fn disconnect(&self) -> Result<(), crate::ConnectionError> {
586 Ok(self.db_disconnect()?)
587 }
588
589 fn reconnect(&self) -> Result<(), crate::ConnectionError> {
590 Ok(self.db_reconnect()?)
591 }
592}
593
594pub struct NativeDbConnection {
595 pub(super) pool: ArcSwapOption<DbPool>,
596 customizer: Box<dyn XmtpConnection>,
597 max_pool_size: u32,
598 min_pool_size: u32,
599}
600
601impl std::fmt::Debug for NativeDbConnection {
602 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
603 write!(
604 f,
605 "NativeDbConnection {{ path: {}, state={:?} }}",
606 self.customizer.options(),
607 self.pool.load().as_ref().map(|s| s.state()),
608 )
609 }
610}
611
612impl NativeDbConnection {
613 fn new(
614 customizer: Box<dyn XmtpConnection>,
615 max_pool_size: u32,
616 min_pool_size: u32,
617 ) -> Result<Self, PlatformStorageError> {
618 let pool = DbPool::builder()
619 .customizer(customizer.clone())
620 .max_size(max_pool_size)
621 .min_size(min_pool_size)
622 .build()?;
623
624 Ok(Self {
625 pool: ArcSwapOption::new(Some(Arc::new(pool))),
626 customizer,
627 max_pool_size,
628 min_pool_size,
629 })
630 }
631
632 fn db_disconnect(&self) -> Result<(), PlatformStorageError> {
633 tracing::warn!("released sqlite database connection");
634 self.pool.store(None);
635 Ok(())
636 }
637
638 fn db_reconnect(&self) -> Result<(), PlatformStorageError> {
639 tracing::info!("reconnecting sqlite database connection");
640 let pool = DbPool::builder()
641 .max_size(self.max_pool_size)
642 .min_size(self.min_pool_size)
643 .customizer(self.customizer.clone())
644 .build()?;
645 self.pool.store(Some(Arc::new(pool)));
646 Ok(())
647 }
648}
649
650impl ConnectionExt for NativeDbConnection {
651 fn raw_query<T, F>(&self, fun: F) -> Result<T, crate::ConnectionError>
652 where
653 F: FnOnce(&mut SqliteConnection) -> Result<T, diesel::result::Error>,
654 Self: Sized,
655 {
656 if let Some(pool) = &*self.pool.load() {
657 let mut conn = pool.get()?;
658 fun(&mut conn).map_err(ConnectionError::from)
659 } else {
660 Err(ConnectionError::from(
661 PlatformStorageError::PoolNeedsConnection,
662 ))
663 }
664 }
665
666 fn disconnect(&self) -> Result<(), ConnectionError> {
667 Ok(self.db_disconnect()?)
668 }
669
670 fn reconnect(&self) -> Result<(), ConnectionError> {
671 Ok(self.db_reconnect()?)
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use crate::{EncryptedMessageStore, XmtpTestDb};
678
679 use super::*;
680 use crate::{Fetch, Store, identity::StoredIdentity};
681 use xmtp_common::{rand_vec, tmp_path};
682
683 #[tokio::test]
684 async fn releases_db_lock() {
685 let db_path = tmp_path();
686 {
687 let store = crate::TestDb::create_persistent_store(Some(db_path.clone())).await;
688 let conn = &store.conn();
689
690 let inbox_id = "inbox_id";
691 StoredIdentity::new(inbox_id.to_string(), rand_vec::<24>(), rand_vec::<24>())
692 .store(conn)
693 .unwrap();
694
695 let fetched_identity: StoredIdentity = conn.fetch(&()).unwrap().unwrap();
696
697 assert_eq!(fetched_identity.inbox_id, inbox_id);
698
699 store.release_connection().unwrap();
700 if let PersistentOrMem::Persistent(p) = &*store.db.conn() {
701 assert!(p.pool.load().is_none())
702 } else {
703 panic!("conn expected")
704 }
705 store.reconnect().unwrap();
706 let fetched_identity2: StoredIdentity = conn.fetch(&()).unwrap().unwrap();
707
708 assert_eq!(fetched_identity2.inbox_id, inbox_id);
709 }
710
711 EncryptedMessageStore::<()>::remove_db_files(db_path)
712 }
713
714 #[xmtp_common::test(unwrap_try = true)]
715 async fn mismatched_encryption_key() {
716 use crate::database::PlatformStorageError;
717 use xmtp_common::{ErrorCode, RetryableError};
718 let mut enc_key = [1u8; 32];
719
720 let db_path = tmp_path();
721 {
722 let db = NativeDb::builder()
723 .persistent(db_path.clone())
724 .key(enc_key)
725 .build()
726 .unwrap();
727 db.init().unwrap();
728
729 StoredIdentity::new(
730 "dummy_address".to_string(),
731 rand_vec::<24>(),
732 rand_vec::<24>(),
733 )
734 .store(&db.conn())
735 .unwrap();
736 } enc_key[3] = 145; let err = NativeDb::builder()
739 .persistent(db_path.clone())
740 .key(enc_key)
741 .build()
742 .unwrap_err();
743 assert!(
745 matches!(
746 err,
747 crate::StorageError::Platform(PlatformStorageError::SqlCipherKeyIncorrect)
748 ),
749 "Expected SqlCipherKeyIncorrect error, got {}",
750 err
751 );
752 assert_eq!(err.error_code(), "StorageError::Platform");
753 assert!(!err.is_retryable());
754 EncryptedMessageStore::<()>::remove_db_files(db_path)
755 }
756
757 #[tokio::test]
758 async fn single_connection_roundtrip_and_reconnect() {
759 use crate::{Fetch, Store, identity::StoredIdentity};
760
761 let db_path = tmp_path();
762 {
763 let db = NativeDb::builder()
764 .persistent(db_path.clone())
765 .key([7u8; 32])
766 .single_connection()
767 .build()
768 .unwrap();
769 db.init().unwrap();
770
771 assert!(
772 matches!(&*db.conn(), PersistentOrMem::Single(_)),
773 "expected Single arm for single_connection() persistent db"
774 );
775
776 let conn = db.conn();
777 let inbox_id = "single_conn_inbox";
778 StoredIdentity::new(inbox_id.to_string(), rand_vec::<24>(), rand_vec::<24>())
779 .store(&conn)
780 .unwrap();
781
782 let fetched: StoredIdentity = conn.fetch(&()).unwrap().unwrap();
783 assert_eq!(fetched.inbox_id, inbox_id);
784
785 conn.reconnect().unwrap();
786 let fetched2: StoredIdentity = conn.fetch(&()).unwrap().unwrap();
787 assert_eq!(fetched2.inbox_id, inbox_id);
788 }
789 EncryptedMessageStore::<()>::remove_db_files(db_path)
790 }
791
792 #[tokio::test]
798 async fn single_connection_disconnect_releases_then_reconnect() {
799 use crate::{Fetch, Store, identity::StoredIdentity};
800
801 let db_path = tmp_path();
802 {
803 let db = NativeDb::builder()
804 .persistent(db_path.clone())
805 .key([8u8; 32])
806 .single_connection()
807 .build()
808 .unwrap();
809 db.init().unwrap();
810
811 let conn = db.conn();
812 let inbox_id = "fd_release_inbox";
813 StoredIdentity::new(inbox_id.to_string(), rand_vec::<24>(), rand_vec::<24>())
814 .store(&conn)
815 .unwrap();
816
817 let ok: Result<Option<StoredIdentity>, _> = conn.fetch(&());
819 assert!(ok.is_ok());
820
821 conn.disconnect().unwrap();
823 if let PersistentOrMem::Single(s) = &*db.conn() {
824 assert!(
825 s.conn.lock().is_none(),
826 "single connection should be dropped (fd released) after disconnect"
827 );
828 } else {
829 panic!("expected Single arm");
830 }
831
832 let res: Result<Option<StoredIdentity>, _> = conn.fetch(&());
835 let err = res.expect_err("query against a disconnected single connection should fail");
836 assert!(
837 err.db_needs_connection(),
838 "expected db_needs_connection() after disconnect, got: {err:?}"
839 );
840
841 conn.reconnect().unwrap();
843 let fetched: StoredIdentity = conn.fetch(&()).unwrap().unwrap();
844 assert_eq!(fetched.inbox_id, inbox_id);
845 }
846 EncryptedMessageStore::<()>::remove_db_files(db_path)
847 }
848
849 #[xmtp_common::test(unwrap_try = true)]
850 async fn single_connection_mismatched_key_fails() {
851 use crate::database::PlatformStorageError;
852 use xmtp_common::{ErrorCode, RetryableError};
853
854 let db_path = tmp_path();
855 {
856 let db = NativeDb::builder()
857 .persistent(db_path.clone())
858 .key([1u8; 32])
859 .single_connection()
860 .build()
861 .unwrap();
862 db.init().unwrap();
863 StoredIdentity::new("addr".to_string(), rand_vec::<24>(), rand_vec::<24>())
864 .store(&db.conn())
865 .unwrap();
866 }
867 let mut bad = [1u8; 32];
868 bad[3] = 200;
869 let err = NativeDb::builder()
870 .persistent(db_path.clone())
871 .key(bad)
872 .single_connection()
873 .build()
874 .unwrap_err();
875 assert!(
876 matches!(
877 err,
878 crate::StorageError::Platform(PlatformStorageError::SqlCipherKeyIncorrect)
879 ),
880 "expected SqlCipherKeyIncorrect, got {err}"
881 );
882 assert_eq!(err.error_code(), "StorageError::Platform");
883 assert!(!err.is_retryable());
884 EncryptedMessageStore::<()>::remove_db_files(db_path)
885 }
886
887 #[tokio::test]
895 async fn single_connection_nested_transaction_no_deadlock() {
896 use crate::{
897 ConnectionExt, StorageError, Store, StoreOrIgnore,
898 TransactionOutcome::Continue,
899 TransactionalKeyStore, XmtpMlsStorageProvider,
900 refresh_state::{EntityKind, RefreshState},
901 sql_key_store::SqlKeyStore,
902 };
903 use diesel::prelude::*;
904
905 let db_path = tmp_path();
906 {
907 let db = NativeDb::builder()
908 .persistent(db_path.clone())
909 .key([9u8; 32])
910 .single_connection()
911 .build()
912 .unwrap();
913 db.init().unwrap();
914
915 let provider = SqlKeyStore::new(db.conn());
919
920 provider
921 .transaction(|conn| {
922 let storage = conn.key_store();
928 StoredIdentity::new(
929 "txn_outer".to_string(),
930 rand_vec::<24>(),
931 rand_vec::<24>(),
932 )
933 .store(&storage.db())?;
934
935 storage.savepoint(|sp_conn| {
938 let inner = sp_conn.key_store();
939 RefreshState {
940 entity_id: rand_vec::<24>(),
941 entity_kind: EntityKind::Welcome,
942 sequence_id: 1,
943 received_sequence_id: None,
944 }
945 .store_or_ignore(&inner.db())?;
946 Ok::<_, StorageError>(Continue(()))
947 })?;
948 Ok::<_, StorageError>(Continue(()))
949 })
950 .unwrap();
951
952 let (identity_count, refresh_count): (i64, i64) = db
957 .conn()
958 .raw_query(|c| {
959 use diesel::dsl::sql;
960 use diesel::sql_types::BigInt;
961 let identity_count =
962 diesel::select(sql::<BigInt>("(SELECT COUNT(*) FROM identity)"))
963 .get_result(c)?;
964 let refresh_count =
965 diesel::select(sql::<BigInt>("(SELECT COUNT(*) FROM refresh_state)"))
966 .get_result(c)?;
967 Ok((identity_count, refresh_count))
968 })
969 .unwrap();
970 assert!(identity_count >= 1, "expected the outer identity row");
971 assert!(
972 refresh_count >= 1,
973 "expected the nested savepoint's refresh_state row to persist"
974 );
975 }
976 EncryptedMessageStore::<()>::remove_db_files(db_path)
977 }
978}