1use std::{
18 collections::HashMap,
19 fmt,
20 iter::once,
21 ops::{Deref, Not},
22 path::{Path, PathBuf},
23 sync::Arc,
24};
25
26use async_trait::async_trait;
27use deadpool::managed::PoolConfig;
28use matrix_sdk_base::{
29 cross_process_lock::CrossProcessLockGeneration,
30 deserialized_responses::TimelineEvent,
31 event_cache::{
32 Event, Gap,
33 store::{EventCacheStore, extract_event_relation},
34 thread::ThreadInfo,
35 },
36 linked_chunk::{
37 ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId,
38 Position, RawChunk, Update,
39 },
40 timer,
41};
42use matrix_sdk_store_encryption::StoreCipher;
43use ruma::{
44 EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, events::relation::RelationType,
45};
46use rusqlite::{
47 OptionalExtension, ToSql, Transaction, TransactionBehavior, params, params_from_iter,
48};
49use tokio::{
50 fs,
51 sync::{Mutex, OwnedMutexGuard},
52};
53use tracing::{debug, error, instrument, trace};
54
55use crate::{
56 OpenStoreError, RuntimeConfig, Secret, SqliteStoreConfig,
57 connection::{
58 self, Connection as SqliteAsyncConn, Pool as SqlitePool, SqliteConnections,
59 SqliteConnectionsInner,
60 },
61 error::{Error, Result},
62 utils::{
63 EncryptableStore, Key, SqliteAsyncConnExt, SqliteKeyValueStoreAsyncConnExt,
64 SqliteKeyValueStoreConnExt, SqliteTransactionExt, host_parameters,
65 },
66};
67
68mod keys {
69 pub const LINKED_CHUNKS: &str = "linked_chunks";
71 pub const EVENTS: &str = "events";
72}
73
74const DATABASE_NAME: &str = "matrix-sdk-event-cache.sqlite3";
76
77const CHUNK_TYPE_EVENT_TYPE_STRING: &str = "E";
80const CHUNK_TYPE_GAP_TYPE_STRING: &str = "G";
83
84struct Encryption {
89 cipher: Option<StoreCipher>,
90}
91
92impl Encryption {
93 fn encode_event(&self, event: &TimelineEvent) -> Result<EncodedEvent> {
94 let serialized = serde_json::to_vec(event)?;
95
96 let raw_event = event.raw();
98 let (relates_to, rel_type) = extract_event_relation(raw_event).unzip();
99
100 let content = self.encode_value(serialized)?;
102
103 Ok(EncodedEvent {
104 content,
105 rel_type,
106 relates_to: relates_to
107 .map(|relates_to| self.encode_event_id(keys::EVENTS, &relates_to)),
108 })
109 }
110
111 fn decode_event(&self, raw_encoded_event: &[u8]) -> Result<Event> {
112 Ok(serde_json::from_slice(&self.decode_value(raw_encoded_event)?)?)
113 }
114
115 fn encode_event_id(&self, table_name: &str, event_id: &EventId) -> Key {
118 self.encode_key(table_name, event_id)
119 }
120
121 fn encode_room_id(&self, table_name: &str, room_id: &RoomId) -> Key {
123 self.encode_key(table_name, room_id)
124 }
125
126 fn encode_linked_chunk(&self, table_name: &str, linked_chunk_id: &LinkedChunkId<'_>) -> Key {
128 self.encode_key(table_name, linked_chunk_id.storage_key())
129 }
130
131 fn encode_thread_id(&self, thread_id: &EventId) -> Result<Vec<u8>> {
133 self.encode_value(String::from(thread_id.as_str()))
134 }
135
136 #[cfg(test)]
138 fn decode_thread_id(&self, encoded_thread_id: &[u8]) -> Result<OwnedEventId> {
139 let as_slice = self.decode_value(encoded_thread_id)?;
140 let as_str = str::from_utf8(as_slice.as_ref())?;
141
142 Ok(EventId::parse(as_str)?)
143 }
144
145 fn encode_thread_info(&self, thread_info: &ThreadInfo) -> Result<Vec<u8>> {
147 self.encode_value(serde_json::to_vec(thread_info)?)
148 }
149
150 fn decode_thread_info(&self, encoded_thread_info: &[u8]) -> Result<ThreadInfo> {
152 Ok(serde_json::from_slice(&self.decode_value(encoded_thread_info)?)?)
153 }
154}
155
156impl EncryptableStore for Encryption {
157 fn get_cypher(&self) -> Option<&StoreCipher> {
158 self.cipher.as_ref()
159 }
160}
161
162#[derive(Clone)]
164pub struct SqliteEventCacheStore {
165 encryption: Arc<Encryption>,
167
168 connections: Arc<SqliteConnections>,
170
171 db_path: PathBuf,
173
174 pool_config: PoolConfig,
176
177 runtime_config: RuntimeConfig,
179}
180
181#[cfg(not(tarpaulin_include))]
182impl fmt::Debug for SqliteEventCacheStore {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.debug_struct("SqliteEventCacheStore").finish_non_exhaustive()
185 }
186}
187
188impl SqliteEventCacheStore {
189 pub async fn open(
192 path: impl AsRef<Path>,
193 passphrase: Option<&str>,
194 ) -> Result<Self, OpenStoreError> {
195 Self::open_with_config(&SqliteStoreConfig::new(path).passphrase(passphrase)).await
196 }
197
198 pub async fn open_with_key(
201 path: impl AsRef<Path>,
202 key: Option<&[u8]>,
203 ) -> Result<Self, OpenStoreError> {
204 Self::open_with_config(&SqliteStoreConfig::new(path).key(key)).await
205 }
206
207 #[instrument(skip(config), fields(path = ?config.path))]
209 pub async fn open_with_config(config: &SqliteStoreConfig) -> Result<Self, OpenStoreError> {
210 debug!(?config);
211
212 let _timer = timer!("open_with_config");
213
214 fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir)?;
215
216 let db_path = config.path.join(DATABASE_NAME);
217 let pool_config = config.pool_config();
218 let runtime_config = config.runtime_config();
219
220 let pool = config.build_pool_of_connections(DATABASE_NAME)?;
221
222 let this =
223 Self::open_with_pool(pool, db_path, pool_config, runtime_config, config.secret.clone())
224 .await?;
225
226 this.write().await?.apply_runtime_config(runtime_config).await?;
228
229 Ok(this)
230 }
231
232 async fn open_with_pool(
235 pool: SqlitePool,
236 db_path: PathBuf,
237 pool_config: PoolConfig,
238 runtime_config: RuntimeConfig,
239 secret: Option<Secret>,
240 ) -> Result<Self, OpenStoreError> {
241 let conn = pool.get().await?;
242
243 let version = conn.db_version().await?;
244
245 run_migrations(&conn, version).await?;
246
247 conn.wal_checkpoint().await;
248
249 let cipher = match secret {
250 Some(s) => Some(conn.get_or_create_store_cipher(s).await?),
251 None => None,
252 };
253
254 let connections = SqliteConnectionsInner {
255 pool,
256 write_connection: Arc::new(Mutex::new(conn)),
258 };
259
260 Ok(Self {
261 encryption: Arc::new(Encryption { cipher }),
262 connections: Arc::new(SqliteConnections::new(connections).await),
263 db_path,
264 pool_config,
265 runtime_config,
266 })
267 }
268
269 #[instrument(skip_all)]
271 async fn read(&self) -> Result<SqliteAsyncConn> {
272 let pool = {
273 let guard = self.connections.lock().await;
274 let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
275 conns.pool.clone()
276 };
277
278 let connection = pool.get().await?;
279
280 connection.execute_batch("PRAGMA foreign_keys = ON;").await?;
285
286 Ok(connection)
287 }
288
289 #[instrument(skip_all)]
291 async fn write(&self) -> Result<OwnedMutexGuard<SqliteAsyncConn>> {
292 let write_connection = {
293 let guard = self.connections.lock().await;
294 let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
295 conns.write_connection.clone()
296 };
297
298 let connection = write_connection.lock_owned().await;
299
300 connection.execute_batch("PRAGMA foreign_keys = ON;").await?;
305
306 Ok(connection)
307 }
308
309 fn map_row_to_chunk(
310 row: &rusqlite::Row<'_>,
311 ) -> Result<(u64, Option<u64>, Option<u64>, String), rusqlite::Error> {
312 Ok((
313 row.get::<_, u64>(0)?,
314 row.get::<_, Option<u64>>(1)?,
315 row.get::<_, Option<u64>>(2)?,
316 row.get::<_, String>(3)?,
317 ))
318 }
319
320 pub async fn vacuum(&self) -> Result<()> {
321 let write_connection = {
322 let guard = self.connections.lock().await;
323 let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
324 conns.write_connection.clone()
325 };
326 write_connection.lock().await.vacuum().await
327 }
328
329 async fn get_db_size(&self) -> Result<Option<usize>> {
330 let pool = {
331 let guard = self.connections.lock().await;
332 let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
333 conns.pool.clone()
334 };
335 Ok(Some(pool.get().await?.get_db_size().await?))
336 }
337
338 pub async fn close(&self) -> Result<()> {
339 connection::close_connections(&self.connections, "Event cache store").await;
340 Ok(())
341 }
342
343 pub async fn reopen(&self) -> Result<()> {
344 connection::reopen_connections(
345 &self.connections,
346 self.db_path.clone(),
347 self.pool_config,
348 self.runtime_config,
349 )
350 .await?;
351 Ok(())
352 }
353
354 #[cfg(test)]
356 async fn pool_max_size(&self) -> Option<usize> {
357 let guard = self.connections.lock().await;
358 guard.as_ref().map(|conns| conns.pool.status().max_size)
359 }
360}
361
362struct EncodedEvent {
363 content: Vec<u8>,
364 rel_type: Option<String>,
365 relates_to: Option<Key>,
366}
367
368trait TransactionExtForLinkedChunks {
369 fn rebuild_chunk(
370 &self,
371 encryption: &Encryption,
372 linked_chunk_id: &Key,
373 previous: Option<u64>,
374 index: u64,
375 next: Option<u64>,
376 chunk_type: &str,
377 ) -> Result<RawChunk<Event, Gap>>;
378
379 fn load_gap_content(
380 &self,
381 encryption: &Encryption,
382 linked_chunk_id: &Key,
383 chunk_id: ChunkIdentifier,
384 ) -> Result<Gap>;
385
386 fn load_events_content(
387 &self,
388 encryption: &Encryption,
389 linked_chunk_id: &Key,
390 chunk_id: ChunkIdentifier,
391 ) -> Result<Vec<Event>>;
392}
393
394impl TransactionExtForLinkedChunks for Transaction<'_> {
395 fn rebuild_chunk(
396 &self,
397 encryption: &Encryption,
398 linked_chunk_id: &Key,
399 previous: Option<u64>,
400 id: u64,
401 next: Option<u64>,
402 chunk_type: &str,
403 ) -> Result<RawChunk<Event, Gap>> {
404 let previous = previous.map(ChunkIdentifier::new);
405 let next = next.map(ChunkIdentifier::new);
406 let id = ChunkIdentifier::new(id);
407
408 match chunk_type {
409 CHUNK_TYPE_GAP_TYPE_STRING => {
410 let gap = self.load_gap_content(encryption, linked_chunk_id, id)?;
412 Ok(RawChunk { content: ChunkContent::Gap(gap), previous, identifier: id, next })
413 }
414
415 CHUNK_TYPE_EVENT_TYPE_STRING => {
416 let events = self.load_events_content(encryption, linked_chunk_id, id)?;
418 Ok(RawChunk {
419 content: ChunkContent::Items(events),
420 previous,
421 identifier: id,
422 next,
423 })
424 }
425
426 other => {
427 Err(Error::InvalidData {
429 details: format!("a linked chunk has an unknown type {other}"),
430 })
431 }
432 }
433 }
434
435 fn load_gap_content(
436 &self,
437 encryption: &Encryption,
438 linked_chunk_id: &Key,
439 chunk_id: ChunkIdentifier,
440 ) -> Result<Gap> {
441 let encoded_prev_token: Vec<u8> = self.query_one(
444 "SELECT prev_token FROM gap_chunks WHERE chunk_id = ? AND linked_chunk_id = ?",
445 (chunk_id.index(), &linked_chunk_id),
446 |row| row.get(0),
447 )?;
448 let prev_token_bytes = encryption.decode_value(&encoded_prev_token)?;
449 let prev_token = String::from_utf8(prev_token_bytes.into_owned())?;
450 Ok(Gap { token: prev_token })
451 }
452
453 fn load_events_content(
454 &self,
455 encryption: &Encryption,
456 linked_chunk_id: &Key,
457 chunk_id: ChunkIdentifier,
458 ) -> Result<Vec<Event>> {
459 let mut events = Vec::new();
461
462 for event_data in self
463 .prepare(
464 "SELECT events.content \
465 FROM event_chunks ec, events \
466 WHERE events.event_id = ec.event_id AND ec.chunk_id = ? AND ec.linked_chunk_id = ? \
467 ORDER BY ec.position ASC",
468 )?
469 .query_map((chunk_id.index(), &linked_chunk_id), |row| row.get::<_, Vec<u8>>(0))?
470 {
471 events.push(encryption.decode_event(&event_data?)?);
472 }
473
474 Ok(events)
475 }
476}
477
478async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> {
480 conn.execute_batch("PRAGMA foreign_keys = ON;").await?;
482
483 if version < 1 {
484 debug!("Creating database");
485 conn.execute_batch("PRAGMA journal_mode = wal;").await?;
489 conn.with_transaction(|txn| {
490 txn.execute_batch(include_str!("../migrations/event_cache_store/001_init.sql"))?;
491 txn.set_db_version(1)
492 })
493 .await?;
494 }
495
496 if version < 2 {
497 debug!("Upgrading database to version 2");
498 conn.with_transaction(|txn| {
499 txn.execute_batch(include_str!("../migrations/event_cache_store/002_lease_locks.sql"))?;
500 txn.set_db_version(2)
501 })
502 .await?;
503 }
504
505 if version < 3 {
506 debug!("Upgrading database to version 3");
507 conn.with_transaction(|txn| {
508 txn.execute_batch(include_str!("../migrations/event_cache_store/003_events.sql"))?;
509 txn.set_db_version(3)
510 })
511 .await?;
512 }
513
514 if version < 4 {
515 debug!("Upgrading database to version 4");
516 conn.with_transaction(|txn| {
517 txn.execute_batch(include_str!(
518 "../migrations/event_cache_store/004_ignore_policy.sql"
519 ))?;
520 txn.set_db_version(4)
521 })
522 .await?;
523 }
524
525 if version < 5 {
526 debug!("Upgrading database to version 5");
527 conn.with_transaction(|txn| {
528 txn.execute_batch(include_str!(
529 "../migrations/event_cache_store/005_events_index_on_event_id.sql"
530 ))?;
531 txn.set_db_version(5)
532 })
533 .await?;
534 }
535
536 if version < 6 {
537 debug!("Upgrading database to version 6");
538 conn.with_transaction(|txn| {
539 txn.execute_batch(include_str!("../migrations/event_cache_store/006_events.sql"))?;
540 txn.set_db_version(6)
541 })
542 .await?;
543 }
544
545 if version < 7 {
546 debug!("Upgrading database to version 7");
547 conn.with_transaction(|txn| {
548 txn.execute_batch(include_str!(
549 "../migrations/event_cache_store/007_event_chunks.sql"
550 ))?;
551 txn.set_db_version(7)
552 })
553 .await?;
554 }
555
556 if version < 8 {
557 debug!("Upgrading database to version 8");
558 conn.with_transaction(|txn| {
559 txn.execute_batch(include_str!(
560 "../migrations/event_cache_store/008_linked_chunk_id.sql"
561 ))?;
562 txn.set_db_version(8)
563 })
564 .await?;
565 }
566
567 if version < 9 {
568 debug!("Upgrading database to version 9");
569 conn.with_transaction(|txn| {
570 txn.execute_batch(include_str!(
571 "../migrations/event_cache_store/009_related_event_index.sql"
572 ))?;
573 txn.set_db_version(9)
574 })
575 .await?;
576 }
577
578 if version < 10 {
579 debug!("Upgrading database to version 10");
580 conn.with_transaction(|txn| {
581 txn.execute_batch(include_str!("../migrations/event_cache_store/010_drop_media.sql"))?;
582 txn.set_db_version(10)
583 })
584 .await?;
585
586 if version >= 1 {
587 conn.vacuum().await?;
590 }
591 }
592
593 if version < 11 {
594 debug!("Upgrading database to version 11");
595 conn.with_transaction(|txn| {
596 txn.execute_batch(include_str!(
597 "../migrations/event_cache_store/011_empty_event_cache.sql"
598 ))?;
599 txn.set_db_version(11)
600 })
601 .await?;
602 }
603
604 if version < 12 {
605 debug!("Upgrading database to version 12");
606 conn.with_transaction(|txn| {
607 txn.execute_batch(include_str!(
608 "../migrations/event_cache_store/012_store_event_type.sql"
609 ))?;
610 txn.set_db_version(12)
611 })
612 .await?;
613 }
614
615 if version < 13 {
616 debug!("Upgrading database to version 13");
617 conn.with_transaction(|txn| {
618 txn.execute_batch(include_str!(
619 "../migrations/event_cache_store/013_lease_locks_with_generation.sql"
620 ))?;
621 txn.set_db_version(13)
622 })
623 .await?;
624 }
625
626 if version < 14 {
627 debug!("Upgrading database to version 14");
628 conn.with_transaction(|txn| {
629 txn.execute_batch(include_str!(
630 "../migrations/event_cache_store/014_event_chunks_event_id_index.sql"
631 ))?;
632 txn.set_db_version(14)
633 })
634 .await?;
635 }
636
637 if version < 15 {
638 debug!("Upgrading database to version 15");
639 conn.with_transaction(|txn| {
640 txn.execute_batch(include_str!(
641 "../migrations/event_cache_store/015_event_ids_are_encoded.sql"
642 ))?;
643 txn.set_db_version(15)
644 })
645 .await?;
646 }
647
648 if version < 16 {
649 debug!("Upgrading database to version 16");
650 conn.with_transaction(|txn| {
651 txn.execute_batch(include_str!("../migrations/event_cache_store/016_threads.sql"))?;
652 txn.set_db_version(16)
653 })
654 .await?;
655 }
656
657 if version < 17 {
658 debug!("Upgrading database to version 17");
659 conn.with_transaction(|txn| {
660 txn.execute_batch(include_str!(
661 "../migrations/event_cache_store/017_threads_with_thread_infos.sql"
662 ))?;
663 txn.set_db_version(17)
664 })
665 .await?;
666 }
667
668 if version < 18 {
669 debug!("Upgrading database to version 18");
670 conn.with_transaction(|txn| {
671 txn.execute_batch(include_str!("../migrations/event_cache_store/018_event_chunks_unique_linked_chunk_id_event_id.sql"))?;
672 txn.set_db_version(18)
673 })
674 .await?;
675 }
676
677 Ok(())
678}
679
680#[async_trait]
681impl EventCacheStore for SqliteEventCacheStore {
682 type Error = Error;
683
684 #[instrument(skip(self))]
685 async fn try_take_leased_lock(
686 &self,
687 lease_duration_ms: u32,
688 key: &str,
689 holder: &str,
690 ) -> Result<Option<CrossProcessLockGeneration>> {
691 let key = key.to_owned();
692 let holder = holder.to_owned();
693
694 let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
695 let expiration = now + lease_duration_ms as u64;
696
697 let generation = self
699 .write()
700 .await?
701 .with_transaction(move |txn| {
702 txn.query_one(
703 "INSERT INTO lease_locks (key, holder, expiration) \
704 VALUES (?1, ?2, ?3) \
705 ON CONFLICT (key) \
706 DO \
707 UPDATE SET \
708 holder = excluded.holder, \
709 expiration = excluded.expiration, \
710 generation = \
711 CASE holder \
712 WHEN excluded.holder THEN generation \
713 ELSE generation + 1 \
714 END \
715 WHERE \
716 holder = excluded.holder \
717 OR expiration < ?4 \
718 RETURNING generation",
719 (key, holder, expiration, now),
720 |row| row.get(0),
721 )
722 .optional()
723 })
724 .await?;
725
726 Ok(generation)
727 }
728
729 #[instrument(skip(self, updates))]
730 async fn handle_linked_chunk_updates(
731 &self,
732 linked_chunk_id: LinkedChunkId<'_>,
733 updates: Vec<Update<Event, Gap>>,
734 ) -> Result<(), Self::Error> {
735 let _timer = timer!("method");
736
737 let hashed_linked_chunk_id =
738 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
739 let hashed_room_id =
740 self.encryption.encode_room_id(keys::EVENTS, linked_chunk_id.room_id());
741 let encryption = self.encryption.clone();
742
743 with_immediate_transaction(self, move |txn| {
746 for update in updates {
747 match update {
748 Update::NewItemsChunk { previous, new, next } => {
749 let previous = previous.as_ref().map(ChunkIdentifier::index);
750 let new = new.index();
751 let next = next.as_ref().map(ChunkIdentifier::index);
752
753 trace!("new events chunk (prev={previous:?}, i={new}, next={next:?})");
754
755 insert_chunk(
756 txn,
757 &hashed_linked_chunk_id,
758 previous,
759 new,
760 next,
761 CHUNK_TYPE_EVENT_TYPE_STRING,
762 )?;
763 }
764
765 Update::NewGapChunk { previous, new, next, gap } => {
766 let hashed_prev_token = encryption.encode_value(gap.token)?;
767
768 let previous = previous.as_ref().map(ChunkIdentifier::index);
769 let new = new.index();
770 let next = next.as_ref().map(ChunkIdentifier::index);
771
772 trace!("new gap chunk (prev={previous:?}, i={new}, next={next:?})");
773
774 insert_chunk(
776 txn,
777 &hashed_linked_chunk_id,
778 previous,
779 new,
780 next,
781 CHUNK_TYPE_GAP_TYPE_STRING,
782 )?;
783
784 txn.execute(
786 r#"
787 INSERT INTO gap_chunks(chunk_id, linked_chunk_id, prev_token)
788 VALUES (?, ?, ?)
789 "#,
790 (new, &hashed_linked_chunk_id, hashed_prev_token),
791 )?;
792 }
793
794 Update::RemoveChunk(chunk_identifier) => {
795 let chunk_id = chunk_identifier.index();
796
797 trace!("removing chunk @ {chunk_id}");
798
799 let (previous, next): (Option<usize>, Option<usize>) = txn.query_one(
801 "SELECT previous, next FROM linked_chunks WHERE id = ? AND linked_chunk_id = ?",
802 (chunk_id, &hashed_linked_chunk_id),
803 |row| Ok((row.get(0)?, row.get(1)?))
804 )?;
805
806 if let Some(previous) = previous {
808 txn.execute("UPDATE linked_chunks SET next = ? WHERE id = ? AND linked_chunk_id = ?", (next, previous, &hashed_linked_chunk_id))?;
809 }
810
811 if let Some(next) = next {
813 txn.execute("UPDATE linked_chunks SET previous = ? WHERE id = ? AND linked_chunk_id = ?", (previous, next, &hashed_linked_chunk_id))?;
814 }
815
816 txn.execute("DELETE FROM linked_chunks WHERE id = ? AND linked_chunk_id = ?", (chunk_id, &hashed_linked_chunk_id))?;
819 }
820
821 Update::PushItems { at, items } => {
822 if items.is_empty() {
823 continue;
825 }
826
827 let chunk_id = at.chunk_identifier().index();
828
829 trace!("pushing {} items @ {chunk_id}", items.len());
830
831 let mut chunk_statement = txn.prepare(
832 "INSERT INTO event_chunks(chunk_id, linked_chunk_id, event_id, position) VALUES (?, ?, ?, ?)"
833 )?;
834
835 let mut content_statement = txn.prepare(
842 "INSERT OR REPLACE INTO events(room_id, event_id, event_type, session_id, content, relates_to, rel_type) VALUES (?, ?, ?, ?, ?, ?, ?)"
843 )?;
844
845 let invalid_event = |event: TimelineEvent| {
846 let Some(event_id) = event.event_id() else {
847 error!("Trying to push an event with no ID");
848 return None;
849 };
850
851 let Some(event_type) = event.kind.event_type() else {
852 error!(%event_id, "Trying to save an event with no event type");
853 return None;
854 };
855
856 Some((event_id.to_owned(), event_type, event))
857 };
858
859 for (i, (event_id, event_type, event)) in items.into_iter().filter_map(invalid_event).enumerate() {
860 let hashed_event_id = encryption.encode_event_id(
861 keys::EVENTS,
867 &event_id,
868 );
869
870 {
872 let index = at.index() + i;
873
874 chunk_statement.execute((chunk_id, &hashed_linked_chunk_id, &hashed_event_id, index))?;
875 }
876
877 {
879 let hashed_session_id = event.kind.session_id().map(|s| encryption.encode_key(keys::EVENTS, s));
880 let hashed_event_type = encryption.encode_key(keys::EVENTS, event_type);
881 let encoded_event = encryption.encode_event(&event)?;
882
883 content_statement.execute((
884 &hashed_room_id,
885 &hashed_event_id,
886 hashed_event_type,
887 hashed_session_id,
888 encoded_event.content,
889 encoded_event.relates_to,
890 encoded_event.rel_type
891 ))?;
892 }
893 }
894 }
895
896 Update::ReplaceItem { at, item: event } => {
897 let chunk_id = at.chunk_identifier().index();
898 let index = at.index();
899
900 trace!("replacing item @ {chunk_id}:{index}");
901
902 let Some(event_id) = event.event_id().map(|event_id| event_id.to_owned()) else {
904 error!("Trying to replace an event with a new one that has no ID");
905 continue;
906 };
907
908 let Some(event_type) = event.kind.event_type() else {
909 error!(%event_id, "Trying to save an event with no event type");
910 continue;
911 };
912
913 let hashed_event_id = encryption.encode_event_id(keys::EVENTS, &event_id);
914
915 {
923 let hashed_session_id = event.kind.session_id().map(|s| encryption.encode_key(keys::EVENTS, s));
924 let hashed_event_type = encryption.encode_key(keys::EVENTS, event_type);
925 let encoded_event = encryption.encode_event(&event)?;
926
927 txn.execute(
928 "INSERT OR REPLACE INTO events(room_id, event_id, event_type, session_id, content, relates_to, rel_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
929 (
930 &hashed_room_id,
931 &hashed_event_id,
932 hashed_event_type,
933 hashed_session_id,
934 encoded_event.content,
935 encoded_event.relates_to,
936 encoded_event.rel_type
937 ),
938 )?;
939 }
940
941 {
943 txn.execute(
945 r#"UPDATE event_chunks SET event_id = ? WHERE linked_chunk_id = ? AND chunk_id = ? AND position = ?"#,
946 (&hashed_event_id, &hashed_linked_chunk_id, chunk_id, index)
947 )?;
948 }
949 }
950
951 Update::RemoveItem { at } => {
952 let chunk_id = at.chunk_identifier().index();
953 let index = at.index();
954
955 trace!("removing item @ {chunk_id}:{index}");
956
957 txn.execute("DELETE FROM event_chunks WHERE linked_chunk_id = ? AND chunk_id = ? AND position = ?", (&hashed_linked_chunk_id, chunk_id, index))?;
959
960 txn.execute(
1061 r#"
1062 UPDATE event_chunks
1063 SET position = -(position - 1)
1064 WHERE linked_chunk_id = ? AND chunk_id = ? AND position > ?
1065 "#,
1066 (&hashed_linked_chunk_id, chunk_id, index)
1067 )?;
1068 txn.execute(
1069 r#"
1070 UPDATE event_chunks
1071 SET position = -position
1072 WHERE position < 0 AND linked_chunk_id = ? AND chunk_id = ?
1073 "#,
1074 (&hashed_linked_chunk_id, chunk_id)
1075 )?;
1076
1077 }
1080
1081 Update::DetachLastItems { at } => {
1082 let chunk_id = at.chunk_identifier().index();
1083 let index = at.index();
1084
1085 trace!("truncating items >= {chunk_id}:{index}");
1086
1087 txn.execute("DELETE FROM event_chunks WHERE linked_chunk_id = ? AND chunk_id = ? AND position >= ?", (&hashed_linked_chunk_id, chunk_id, index))?;
1089
1090 }
1093
1094 Update::Clear => {
1095 trace!("clearing items");
1096
1097 txn.execute(
1099 "DELETE FROM linked_chunks WHERE linked_chunk_id = ?",
1100 (&hashed_linked_chunk_id,),
1101 )?;
1102
1103 }
1106
1107 Update::StartReattachItems | Update::EndReattachItems => {
1108 }
1110 }
1111 }
1112
1113 Ok(())
1114 })
1115 .await?;
1116
1117 Ok(())
1118 }
1119
1120 #[instrument(skip(self))]
1121 async fn load_all_chunks(
1122 &self,
1123 linked_chunk_id: LinkedChunkId<'_>,
1124 ) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
1125 let _timer = timer!("method");
1126
1127 let hashed_linked_chunk_id =
1128 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1129 let encryption = self.encryption.clone();
1130
1131 let result = self
1132 .read()
1133 .await?
1134 .with_transaction(move |txn| -> Result<_> {
1135 let mut items = Vec::new();
1136
1137 for data in txn
1139 .prepare(
1140 "SELECT id, previous, next, type FROM linked_chunks WHERE linked_chunk_id = ? ORDER BY id",
1141 )?
1142 .query_map((&hashed_linked_chunk_id,), Self::map_row_to_chunk)?
1143 {
1144 let (id, previous, next, chunk_type) = data?;
1145 let new = txn.rebuild_chunk(
1146 &encryption,
1147 &hashed_linked_chunk_id,
1148 previous,
1149 id,
1150 next,
1151 chunk_type.as_str(),
1152 )?;
1153 items.push(new);
1154 }
1155
1156 Ok(items)
1157 })
1158 .await?;
1159
1160 Ok(result)
1161 }
1162
1163 #[instrument(skip(self))]
1164 async fn load_all_chunks_metadata(
1165 &self,
1166 linked_chunk_id: LinkedChunkId<'_>,
1167 ) -> Result<Vec<ChunkMetadata>, Self::Error> {
1168 let _timer = timer!("method");
1169
1170 let hashed_linked_chunk_id =
1171 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1172
1173 self.read()
1174 .await?
1175 .with_transaction(move |txn| -> Result<_> {
1176 let num_events_by_chunk_ids = txn
1209 .prepare(
1210 "SELECT ec.chunk_id, COUNT(ec.event_id) \
1211 FROM event_chunks as ec \
1212 WHERE ec.linked_chunk_id = ? \
1213 GROUP BY ec.chunk_id",
1214 )?
1215 .query_map((&hashed_linked_chunk_id,), |row| {
1216 Ok((row.get::<_, u64>(0)?, row.get::<_, usize>(1)?))
1217 })?
1218 .collect::<Result<HashMap<_, _>, _>>()?;
1219
1220 txn.prepare(
1221 "SELECT \
1222 lc.id, \
1223 lc.previous, \
1224 lc.next, \
1225 lc.type \
1226 FROM linked_chunks as lc \
1227 WHERE lc.linked_chunk_id = ? \
1228 ORDER BY lc.id",
1229 )?
1230 .query_map((&hashed_linked_chunk_id,), |row| {
1231 Ok((
1232 row.get::<_, u64>(0)?,
1233 row.get::<_, Option<u64>>(1)?,
1234 row.get::<_, Option<u64>>(2)?,
1235 row.get::<_, String>(3)?,
1236 ))
1237 })?
1238 .map(|data| -> Result<_> {
1239 let (id, previous, next, chunk_type) = data?;
1240
1241 let num_items = if chunk_type == CHUNK_TYPE_GAP_TYPE_STRING {
1248 0
1249 } else {
1250 num_events_by_chunk_ids.get(&id).copied().unwrap_or(0)
1251 };
1252
1253 Ok(ChunkMetadata {
1254 identifier: ChunkIdentifier::new(id),
1255 previous: previous.map(ChunkIdentifier::new),
1256 next: next.map(ChunkIdentifier::new),
1257 num_items,
1258 })
1259 })
1260 .collect::<Result<Vec<_>, _>>()
1261 })
1262 .await
1263 }
1264
1265 #[instrument(skip(self))]
1266 async fn load_last_chunk(
1267 &self,
1268 linked_chunk_id: LinkedChunkId<'_>,
1269 ) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
1270 let _timer = timer!("method");
1271
1272 let hashed_linked_chunk_id =
1273 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1274 let encryption = self.encryption.clone();
1275
1276 self
1277 .read()
1278 .await?
1279 .with_transaction(move |txn| -> Result<_> {
1280 let (observed_max_identifier, number_of_chunks) = txn
1282 .prepare(
1283 "SELECT MAX(id), COUNT(*) FROM linked_chunks WHERE linked_chunk_id = ?"
1284 )?
1285 .query_one(
1286 (&hashed_linked_chunk_id,),
1287 |row| {
1288 Ok((
1289 row.get::<_, Option<u64>>(0)?,
1294 row.get::<_, u64>(1)?,
1295 ))
1296 }
1297 )?;
1298
1299 let chunk_identifier_generator = match observed_max_identifier {
1300 Some(max_observed_identifier) => {
1301 ChunkIdentifierGenerator::new_from_previous_chunk_identifier(
1302 ChunkIdentifier::new(max_observed_identifier)
1303 )
1304 },
1305 None => ChunkIdentifierGenerator::new_from_scratch(),
1306 };
1307
1308 let Some((chunk_identifier, previous_chunk, chunk_type)) = txn
1310 .prepare(
1311 "SELECT id, previous, type FROM linked_chunks WHERE linked_chunk_id = ? AND next IS NULL"
1312 )?
1313 .query_one(
1314 (&hashed_linked_chunk_id,),
1315 |row| {
1316 Ok((
1317 row.get::<_, u64>(0)?,
1318 row.get::<_, Option<u64>>(1)?,
1319 row.get::<_, String>(2)?,
1320 ))
1321 }
1322 )
1323 .optional()?
1324 else {
1325 if number_of_chunks == 0 {
1328 return Ok((None, chunk_identifier_generator));
1329 }
1330 else {
1337 return Err(Error::InvalidData {
1338 details:
1339 "last chunk is not found but chunks exist: the linked chunk contains a cycle"
1340 .to_owned()
1341 }
1342 )
1343 }
1344 };
1345
1346 let last_chunk = txn.rebuild_chunk(
1348 &encryption,
1349 &hashed_linked_chunk_id,
1350 previous_chunk,
1351 chunk_identifier,
1352 None,
1353 &chunk_type
1354 )?;
1355
1356 Ok((Some(last_chunk), chunk_identifier_generator))
1357 })
1358 .await
1359 }
1360
1361 #[instrument(skip(self))]
1362 async fn load_previous_chunk(
1363 &self,
1364 linked_chunk_id: LinkedChunkId<'_>,
1365 before_chunk_identifier: ChunkIdentifier,
1366 ) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
1367 let _timer = timer!("method");
1368
1369 let hashed_linked_chunk_id =
1370 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1371 let encryption = self.encryption.clone();
1372
1373 self
1374 .read()
1375 .await?
1376 .with_transaction(move |txn| -> Result<_> {
1377 let Some((chunk_identifier, previous_chunk, next_chunk, chunk_type)) = txn
1379 .prepare(
1380 "SELECT id, previous, next, type FROM linked_chunks WHERE linked_chunk_id = ? AND next = ?"
1381 )?
1382 .query_one(
1383 (&hashed_linked_chunk_id, before_chunk_identifier.index()),
1384 |row| {
1385 Ok((
1386 row.get::<_, u64>(0)?,
1387 row.get::<_, Option<u64>>(1)?,
1388 row.get::<_, Option<u64>>(2)?,
1389 row.get::<_, String>(3)?,
1390 ))
1391 }
1392 )
1393 .optional()?
1394 else {
1395 return Ok(None);
1397 };
1398
1399 let last_chunk = txn.rebuild_chunk(
1401 &encryption,
1402 &hashed_linked_chunk_id,
1403 previous_chunk,
1404 chunk_identifier,
1405 next_chunk,
1406 &chunk_type
1407 )?;
1408
1409 Ok(Some(last_chunk))
1410 })
1411 .await
1412 }
1413
1414 async fn load_thread_info(
1415 &self,
1416 room_id: &RoomId,
1417 thread_id: &EventId,
1418 insert_default_if_missing: bool,
1419 ) -> Result<Option<ThreadInfo>, Self::Error> {
1420 let linked_chunk_id = LinkedChunkId::Thread(room_id, thread_id);
1421 let hashed_linked_chunk_id =
1422 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1423 let encryption = self.encryption.clone();
1424
1425 let maybe_thread_info = self
1430 .read()
1431 .await?
1432 .with_transaction(move |txn| {
1433 let maybe_encoded_thread_info = txn
1434 .query_one(
1435 "SELECT info FROM threads WHERE linked_chunk_id = ?",
1436 (hashed_linked_chunk_id,),
1437 |row| row.get::<_, Vec<u8>>(0),
1438 )
1439 .optional()?;
1440
1441 maybe_encoded_thread_info
1442 .map(|encoded_thread_info| {
1443 encryption.decode_thread_info(encoded_thread_info.as_slice())
1444 })
1445 .transpose()
1446 })
1447 .await?;
1448
1449 if let Some(thread_info) = maybe_thread_info {
1450 return Ok(Some(thread_info));
1451 } else if !insert_default_if_missing {
1452 return Ok(None);
1454 }
1455
1456 let thread_info = ThreadInfo::default();
1459 let hashed_linked_chunk_id =
1460 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1461 let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1462 let hashed_thread_id = self.encryption.encode_thread_id(thread_id)?;
1463 let encoded_thread_id = self.encryption.encode_thread_info(&thread_info)?;
1464
1465 self.write()
1466 .await?
1467 .with_transaction(move |txn| {
1468 txn.execute(
1469 "INSERT INTO threads VALUES (?, ?, ?, ?)",
1470 (hashed_linked_chunk_id, hashed_room_id, hashed_thread_id, encoded_thread_id),
1471 )?;
1472
1473 Ok::<(), Self::Error>(())
1474 })
1475 .await?;
1476
1477 Ok(Some(thread_info))
1478 }
1479
1480 async fn update_thread_info(
1481 &self,
1482 room_id: &RoomId,
1483 thread_id: &EventId,
1484 thread_info: &ThreadInfo,
1485 ) -> Result<(), Self::Error> {
1486 let hashed_linked_chunk_id = self
1487 .encryption
1488 .encode_linked_chunk(keys::LINKED_CHUNKS, &LinkedChunkId::Thread(room_id, thread_id));
1489 let encoded_thread_info = self.encryption.encode_thread_info(thread_info)?;
1490
1491 self.write()
1492 .await?
1493 .with_transaction(move |txn| {
1494 txn.execute(
1495 "UPDATE threads SET info = ? WHERE linked_chunk_id = ?",
1496 (encoded_thread_info, hashed_linked_chunk_id),
1497 )?;
1498
1499 Ok(())
1500 })
1501 .await
1502 }
1503
1504 #[instrument(skip(self))]
1505 async fn clear_all_events(&self, room_id: Option<&RoomId>) -> Result<(), Self::Error> {
1506 let _timer = timer!("method");
1507
1508 match room_id {
1509 None => {
1511 self.write()
1512 .await?
1513 .with_transaction(move |txn| {
1514 txn.execute("DELETE FROM linked_chunks", ())?;
1516
1517 txn.execute("DELETE FROM events", ())?;
1520
1521 Ok(())
1522 })
1523 .await
1524 }
1525
1526 Some(room_id) => {
1528 let encryption = self.encryption.clone();
1529 let room_id = room_id.to_owned();
1530
1531 self.write()
1532 .await?
1533 .with_transaction(move |txn| {
1534 {
1536 let mut delete =
1537 txn.prepare("DELETE FROM linked_chunks WHERE linked_chunk_id = ?")?;
1538
1539 for linked_chunk_id in
1540 [LinkedChunkId::Room(&room_id), LinkedChunkId::PinnedEvents(&room_id)]
1541 {
1542 let linked_chunk_id = encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1543
1544 delete.execute((&linked_chunk_id,))?;
1548 }
1549 }
1550
1551 let encoded_room_id = encryption.encode_room_id(keys::EVENTS, &room_id);
1552
1553 {
1555 txn.execute(
1556 "DELETE FROM linked_chunks WHERE linked_chunk_id IN (SELECT linked_chunk_id FROM threads WHERE room_id = ?)",
1557 (&encoded_room_id,),
1558 )?;
1559 }
1560
1561 txn.execute(
1563 "DELETE FROM events WHERE room_id = ?",
1564 (encoded_room_id,),
1565 )?;
1566
1567 Ok(())
1568 })
1569 .await
1570 }
1571 }
1572 }
1573
1574 #[instrument(skip(self, event_ids))]
1575 async fn filter_duplicated_events(
1576 &self,
1577 linked_chunk_id: LinkedChunkId<'_>,
1578 event_ids: Vec<OwnedEventId>,
1579 ) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
1580 let _timer = timer!("method");
1581
1582 if event_ids.is_empty() {
1586 return Ok(Vec::new());
1587 }
1588
1589 let hashed_linked_chunk_id =
1591 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1592 let event_ids_and_hashed_event_ids = event_ids
1593 .into_iter()
1594 .map(|event_id| {
1595 let hashed_event_id = self.encryption.encode_event_id(
1596 keys::EVENTS,
1601 &event_id,
1602 );
1603
1604 (event_id, hashed_event_id)
1605 })
1606 .collect::<Vec<_>>();
1607
1608 self.read()
1609 .await?
1610 .with_transaction(move |txn| -> Result<_> {
1611 txn.chunk_large_query_over(
1612 event_ids_and_hashed_event_ids,
1613 None,
1614 move |txn, event_ids_and_hashed_event_ids| {
1615 let query = format!(
1616 "SELECT event_id, chunk_id, position \
1617 FROM event_chunks \
1618 WHERE linked_chunk_id = ? AND event_id IN ({}) \
1619 ORDER BY chunk_id ASC, position ASC",
1620 event_ids_and_hashed_event_ids.host_parameters(),
1621 );
1622
1623 let parameters = params_from_iter(
1624 once(
1626 hashed_linked_chunk_id
1627 .to_sql()
1628 .unwrap(),
1630 )
1631 .chain(
1633 event_ids_and_hashed_event_ids.iter().map(
1634 |(_event_id, hashed_event_id)| {
1635 hashed_event_id
1636 .to_sql()
1637 .unwrap()
1640 },
1641 ),
1642 ),
1643 );
1644
1645 let mut duplicated_events = Vec::new();
1646
1647 for duplicated_event in
1648 txn.prepare(&query)?.query_map(parameters, |row| {
1649 Ok((
1650 row.get::<_, Vec<u8>>(0)?,
1651 row.get::<_, u64>(1)?,
1652 row.get::<_, usize>(2)?,
1653 ))
1654 })?
1655 {
1656 let (duplicated_hashed_event_id, chunk_identifier, index) =
1657 duplicated_event?;
1658
1659 let Some(duplicated_event_id) = event_ids_and_hashed_event_ids
1664 .iter()
1665 .find_map(|(event_id, hashed_event_id)| {
1666 (hashed_event_id.deref() == duplicated_hashed_event_id)
1667 .then_some(event_id.clone())
1668 })
1669 else {
1670 error!(
1671 "Unreachable: found a duplicated event that was not requested"
1672 );
1673 continue;
1674 };
1675
1676 duplicated_events.push((
1677 duplicated_event_id,
1678 Position::new(ChunkIdentifier::new(chunk_identifier), index),
1679 ));
1680 }
1681
1682 Ok(duplicated_events)
1683 },
1684 )
1685 })
1686 .await
1687 }
1688
1689 #[instrument(skip(self, event_id))]
1690 async fn find_event(
1691 &self,
1692 room_id: &RoomId,
1693 event_id: &EventId,
1694 ) -> Result<Option<Event>, Self::Error> {
1695 let _timer = timer!("method");
1696
1697 let encryption = self.encryption.clone();
1698
1699 let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1700 let hashed_event_id = self.encryption.encode_event_id(keys::EVENTS, event_id);
1701
1702 self.read()
1703 .await?
1704 .with_transaction(move |txn| -> Result<_> {
1705 let Some(event) = txn
1706 .prepare("SELECT content FROM events WHERE event_id = ? AND room_id = ?")?
1707 .query_one((hashed_event_id, hashed_room_id), |row| row.get::<_, Vec<u8>>(0))
1708 .optional()?
1709 else {
1710 return Ok(None);
1712 };
1713
1714 Ok(Some(encryption.decode_event(&event)?))
1715 })
1716 .await
1717 }
1718
1719 #[instrument(skip(self, event_id, filters))]
1720 async fn find_event_relations(
1721 &self,
1722 room_id: &RoomId,
1723 event_id: &EventId,
1724 filters: Option<&[RelationType]>,
1725 ) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
1726 let _timer = timer!("method");
1727
1728 let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1729 let hashed_linked_chunk_id =
1730 self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &LinkedChunkId::Room(room_id));
1731 let hashed_event_id = self.encryption.encode_event_id(keys::EVENTS, event_id);
1732
1733 let filters = filters.map(ToOwned::to_owned);
1734 let encryption = self.encryption.clone();
1735
1736 self.read()
1737 .await?
1738 .with_transaction(move |txn| -> Result<_> {
1739 find_event_relations_transaction(
1740 &encryption,
1741 hashed_room_id,
1742 hashed_linked_chunk_id,
1743 hashed_event_id,
1744 filters,
1745 txn,
1746 )
1747 })
1748 .await
1749 }
1750
1751 #[instrument(skip(self))]
1752 async fn get_room_events(
1753 &self,
1754 room_id: &RoomId,
1755 event_type: Option<&str>,
1756 session_id: Option<&str>,
1757 ) -> Result<Vec<Event>, Self::Error> {
1758 let _timer = timer!("method");
1759
1760 let encryption = self.encryption.clone();
1761
1762 let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1763 let hashed_event_type = event_type.map(|e| self.encryption.encode_key(keys::EVENTS, e));
1764 let hashed_session_id = session_id.map(|s| self.encryption.encode_key(keys::EVENTS, s));
1765
1766 self.read()
1767 .await?
1768 .with_transaction(move |txn| -> Result<_> {
1769 #[allow(clippy::redundant_clone)]
1774 let (query, keys) = match (hashed_event_type, hashed_session_id) {
1775 (None, None) => {
1776 ("SELECT content FROM events WHERE room_id = ?", params![hashed_room_id])
1777 }
1778 (None, Some(session_id)) => (
1779 "SELECT content FROM events WHERE room_id = ?1 AND session_id = ?2",
1780 params![hashed_room_id, session_id.to_owned()],
1781 ),
1782 (Some(event_type), None) => (
1783 "SELECT content FROM events WHERE room_id = ? AND event_type = ?",
1784 params![hashed_room_id, event_type.to_owned()]
1785 ),
1786 (Some(event_type), Some(session_id)) => (
1787 "SELECT content FROM events WHERE room_id = ?1 AND event_type = ?2 AND session_id = ?3",
1788 params![hashed_room_id, event_type.to_owned(), session_id.to_owned()],
1789 ),
1790 };
1791
1792 let mut statement = txn.prepare(query)?;
1793
1794 statement
1795 .query_map(keys, |row| row.get::<_, Vec<u8>>(0))?
1796 .map(|maybe_encoded_event| {
1797 encryption.decode_event(&maybe_encoded_event?)
1798 })
1799 .collect::<Result<Vec<_>>>()
1800 })
1801 .await
1802 }
1803
1804 #[instrument(skip(self, event))]
1805 async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
1806 let _timer = timer!("method");
1807
1808 let Some(event_id) = event.event_id() else {
1809 error!("Trying to save an event with no ID");
1810 return Ok(());
1811 };
1812
1813 let Some(event_type) = event.kind.event_type() else {
1814 error!(%event_id, "Trying to save an event with no event type");
1815 return Ok(());
1816 };
1817
1818 let hashed_event_type = self.encryption.encode_key(keys::EVENTS, event_type);
1819 let hashed_session_id =
1820 event.kind.session_id().map(|s| self.encryption.encode_key(keys::EVENTS, s));
1821
1822 let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1823 let hashed_event_id = self.encryption.encode_event_id(keys::EVENTS, event_id);
1824 let encoded_event = self.encryption.encode_event(&event)?;
1825
1826 self.write()
1827 .await?
1828 .with_transaction(move |txn| -> Result<_> {
1829 txn.execute(
1830 "INSERT OR REPLACE INTO events(room_id, event_id, event_type, session_id, content, relates_to, rel_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
1831 (
1832 &hashed_room_id,
1833 hashed_event_id,
1834 hashed_event_type,
1835 hashed_session_id,
1836 encoded_event.content,
1837 encoded_event.relates_to,
1838 encoded_event.rel_type
1839 )
1840 )?;
1841
1842 Ok(())
1843 })
1844 .await
1845 }
1846
1847 async fn close(&self) -> Result<(), Self::Error> {
1848 SqliteEventCacheStore::close(self).await
1849 }
1850
1851 async fn reopen(&self) -> Result<(), Self::Error> {
1852 SqliteEventCacheStore::reopen(self).await
1853 }
1854
1855 async fn optimize(&self) -> Result<(), Self::Error> {
1856 Ok(self.vacuum().await?)
1857 }
1858
1859 async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
1860 self.get_db_size().await
1861 }
1862}
1863
1864fn find_event_relations_transaction(
1865 encryption: &Encryption,
1866 hashed_room_id: Key,
1867 hashed_linked_chunk_id: Key,
1868 hashed_event_id: Key,
1869 filters: Option<Vec<RelationType>>,
1870 txn: &Transaction<'_>,
1871) -> Result<Vec<(Event, Option<Position>)>> {
1872 let get_rows = |row: &rusqlite::Row<'_>| {
1873 Ok((
1874 row.get::<_, Vec<u8>>(0)?,
1875 row.get::<_, Option<u64>>(1)?,
1876 row.get::<_, Option<usize>>(2)?,
1877 ))
1878 };
1879
1880 let collect_results = |transaction| {
1882 let mut related = Vec::new();
1883
1884 for result in transaction {
1885 let (event, chunk_id, index): (Vec<u8>, Option<u64>, _) = result?;
1886 let event = encryption.decode_event(&event)?;
1887
1888 let pos = chunk_id
1892 .zip(index)
1893 .map(|(chunk_id, index)| Position::new(ChunkIdentifier::new(chunk_id), index));
1894
1895 related.push((event, pos));
1896 }
1897
1898 Ok(related)
1899 };
1900
1901 if let Some(filters) = filters
1902 && filters.is_empty().not()
1903 {
1904 let query = format!(
1905 "SELECT events.content, event_chunks.chunk_id, event_chunks.position \
1906 FROM events \
1907 LEFT JOIN event_chunks ON events.event_id = event_chunks.event_id AND event_chunks.linked_chunk_id = ? \
1908 WHERE events.relates_to = ? AND events.room_id = ? AND events.rel_type IN ({})",
1909 host_parameters(filters.len())
1910 );
1911
1912 let filter_strings: Vec<_> = filters.iter().map(|f| f.to_string()).collect();
1917 let filters_params: Vec<_> = filter_strings
1918 .iter()
1919 .map(|f| f.to_sql().expect("converting a string to SQL should work"))
1920 .collect();
1921
1922 let parameters = params_from_iter(
1923 [
1924 hashed_linked_chunk_id.to_sql().expect(
1925 "We should be able to convert a hashed linked chunk ID to a SQLite value",
1926 ),
1927 hashed_event_id
1928 .to_sql()
1929 .expect("We should be able to convert an event ID to a SQLite value"),
1930 hashed_room_id
1931 .to_sql()
1932 .expect("We should be able to convert a room ID to a SQLite value"),
1933 ]
1934 .into_iter()
1935 .chain(filters_params),
1936 );
1937
1938 let mut transaction = txn.prepare(&query)?;
1939 let transaction = transaction.query_map(parameters, get_rows)?;
1940
1941 collect_results(transaction)
1942 } else {
1943 let query = "SELECT events.content, event_chunks.chunk_id, event_chunks.position \
1944 FROM events \
1945 LEFT JOIN event_chunks ON events.event_id = event_chunks.event_id AND event_chunks.linked_chunk_id = ? \
1946 WHERE events.relates_to = ? AND events.room_id = ?";
1947 let parameters = (hashed_linked_chunk_id, hashed_event_id, hashed_room_id);
1948
1949 let mut transaction = txn.prepare(query)?;
1950 let transaction = transaction.query_map(parameters, get_rows)?;
1951
1952 collect_results(transaction)
1953 }
1954}
1955
1956async fn with_immediate_transaction<
1961 T: Send + 'static,
1962 F: FnOnce(&Transaction<'_>) -> Result<T, Error> + Send + 'static,
1963>(
1964 this: &SqliteEventCacheStore,
1965 f: F,
1966) -> Result<T, Error> {
1967 this.write()
1968 .await?
1969 .interact(move |conn| -> Result<T, Error> {
1970 conn.set_transaction_behavior(TransactionBehavior::Immediate);
1975
1976 let code = || -> Result<T, Error> {
1977 let txn = conn.transaction()?;
1978 let res = f(&txn)?;
1979 txn.commit()?;
1980 Ok(res)
1981 };
1982
1983 let res = code();
1984
1985 conn.set_transaction_behavior(TransactionBehavior::Deferred);
1988
1989 res
1990 })
1991 .await
1992 .unwrap()
1994}
1995
1996fn insert_chunk(
1997 txn: &Transaction<'_>,
1998 linked_chunk_id: &Key,
1999 previous: Option<u64>,
2000 new: u64,
2001 next: Option<u64>,
2002 type_str: &str,
2003) -> rusqlite::Result<()> {
2004 txn.execute(
2006 r#"
2007 INSERT INTO linked_chunks(id, linked_chunk_id, previous, next, type)
2008 VALUES (?, ?, ?, ?, ?)
2009 "#,
2010 (new, linked_chunk_id, previous, next, type_str),
2011 )?;
2012
2013 if let Some(previous) = previous {
2015 let updated = txn.execute(
2016 r#"
2017 UPDATE linked_chunks
2018 SET next = ?
2019 WHERE id = ? AND linked_chunk_id = ?
2020 "#,
2021 (new, previous, linked_chunk_id),
2022 )?;
2023 if updated < 1 {
2024 return Err(rusqlite::Error::QueryReturnedNoRows);
2025 }
2026 if updated > 1 {
2027 return Err(rusqlite::Error::QueryReturnedMoreThanOneRow);
2028 }
2029 }
2030
2031 if let Some(next) = next {
2033 let updated = txn.execute(
2034 r#"
2035 UPDATE linked_chunks
2036 SET previous = ?
2037 WHERE id = ? AND linked_chunk_id = ?
2038 "#,
2039 (new, next, linked_chunk_id),
2040 )?;
2041 if updated < 1 {
2042 return Err(rusqlite::Error::QueryReturnedNoRows);
2043 }
2044 if updated > 1 {
2045 return Err(rusqlite::Error::QueryReturnedMoreThanOneRow);
2046 }
2047 }
2048
2049 Ok(())
2050}
2051
2052#[cfg(test)]
2053mod tests {
2054 use std::{
2055 path::PathBuf,
2056 sync::{
2057 LazyLock,
2058 atomic::{AtomicU32, Ordering::SeqCst},
2059 },
2060 };
2061
2062 use assert_matches::assert_matches;
2063 use matrix_sdk_base::{
2064 event_cache::store::{
2065 EventCacheStore, EventCacheStoreError, IntoEventCacheStore,
2066 integration_tests::EventCacheStoreIntegrationTests,
2067 },
2068 event_cache_store_integration_tests, event_cache_store_integration_tests_time,
2069 linked_chunk::{ChunkIdentifier, LinkedChunkId, Update},
2070 };
2071 use matrix_sdk_test::{DEFAULT_TEST_ROOM_ID, async_test};
2072 use ruma::{OwnedEventId, event_id};
2073 use tempfile::{TempDir, tempdir};
2074
2075 use super::{SqliteEventCacheStore, keys};
2076 use crate::{SqliteStoreConfig, utils::SqliteAsyncConnExt};
2077
2078 static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
2079 static NUM: AtomicU32 = AtomicU32::new(0);
2080
2081 fn new_event_cache_store_workspace() -> PathBuf {
2082 let name = NUM.fetch_add(1, SeqCst).to_string();
2083 TMP_DIR.path().join(name)
2084 }
2085
2086 async fn get_event_cache_store() -> Result<SqliteEventCacheStore, EventCacheStoreError> {
2087 let tmpdir_path = new_event_cache_store_workspace();
2088
2089 tracing::info!("using event cache store @ {}", tmpdir_path.to_str().unwrap());
2090
2091 Ok(SqliteEventCacheStore::open(tmpdir_path.to_str().unwrap(), None).await.unwrap())
2092 }
2093
2094 event_cache_store_integration_tests!();
2095 event_cache_store_integration_tests_time!();
2096
2097 #[async_test]
2098 async fn test_encryption_encode_decode_thread_id_roundtrip() {
2099 let store = get_event_cache_store().await.expect("creating cache store failed");
2100 let thread_id: OwnedEventId = event_id!("$event").to_owned();
2101
2102 let encoded_thread_id = store.encryption.encode_thread_id(&thread_id).unwrap();
2103 let decoded_thread_id: OwnedEventId =
2104 store.encryption.decode_thread_id(&encoded_thread_id).unwrap();
2105
2106 assert_eq!(thread_id, decoded_thread_id);
2107 }
2108
2109 #[async_test]
2110 async fn test_pool_size() {
2111 let tmpdir_path = new_event_cache_store_workspace();
2112 let store_open_config = SqliteStoreConfig::new(tmpdir_path).pool_max_size(42);
2113
2114 let store = SqliteEventCacheStore::open_with_config(&store_open_config).await.unwrap();
2115
2116 assert_eq!(store.pool_max_size().await, Some(42));
2117 }
2118
2119 #[async_test]
2120 async fn test_linked_chunk_remove_chunk() {
2121 let store = get_event_cache_store().await.expect("creating cache store failed");
2122
2123 store.clone().into_event_cache_store().test_linked_chunk_remove_chunk().await;
2125
2126 let gaps = store
2128 .read()
2129 .await
2130 .unwrap()
2131 .with_transaction(|txn| -> rusqlite::Result<_> {
2132 let mut gaps = Vec::new();
2133 for data in txn
2134 .prepare("SELECT chunk_id FROM gap_chunks ORDER BY chunk_id")?
2135 .query_map((), |row| row.get::<_, u64>(0))?
2136 {
2137 gaps.push(data?);
2138 }
2139 Ok(gaps)
2140 })
2141 .await
2142 .unwrap();
2143
2144 assert_eq!(gaps, vec![42, 44]);
2147 }
2148
2149 #[async_test]
2150 async fn test_linked_chunk_remove_item() {
2151 let store = get_event_cache_store().await.expect("creating cache store failed");
2152
2153 store.clone().into_event_cache_store().test_linked_chunk_remove_item().await;
2155
2156 let room_id = *DEFAULT_TEST_ROOM_ID;
2157 let hashed_linked_chunk_id = store
2158 .encryption
2159 .encode_linked_chunk(keys::LINKED_CHUNKS, &LinkedChunkId::Room(room_id));
2160
2161 let num_rows: u64 = store
2163 .read()
2164 .await
2165 .unwrap()
2166 .with_transaction(move |txn| {
2167 txn.query_one(
2168 "SELECT COUNT(*) FROM event_chunks WHERE chunk_id = 42 AND linked_chunk_id = ? AND position IN (2, 3, 4)",
2169 (hashed_linked_chunk_id,),
2170 |row| row.get(0),
2171 )
2172 })
2173 .await
2174 .unwrap();
2175 assert_eq!(num_rows, 3);
2176 }
2177
2178 #[async_test]
2179 async fn test_linked_chunk_clear() {
2180 let store = get_event_cache_store().await.expect("creating cache store failed");
2181
2182 store.clone().into_event_cache_store().test_linked_chunk_clear().await;
2184
2185 store
2187 .read()
2188 .await
2189 .unwrap()
2190 .with_transaction(|txn| -> rusqlite::Result<_> {
2191 let num_gaps = txn
2192 .prepare("SELECT COUNT(chunk_id) FROM gap_chunks ORDER BY chunk_id")?
2193 .query_one((), |row| row.get::<_, u64>(0))?;
2194 assert_eq!(num_gaps, 0);
2195
2196 let num_events = txn
2197 .prepare("SELECT COUNT(event_id) FROM event_chunks ORDER BY chunk_id")?
2198 .query_one((), |row| row.get::<_, u64>(0))?;
2199 assert_eq!(num_events, 0);
2200
2201 Ok(())
2202 })
2203 .await
2204 .unwrap();
2205 }
2206
2207 #[async_test]
2208 async fn test_linked_chunk_update_is_a_transaction() {
2209 let store = get_event_cache_store().await.expect("creating cache store failed");
2210
2211 let room_id = *DEFAULT_TEST_ROOM_ID;
2212 let linked_chunk_id = LinkedChunkId::Room(room_id);
2213
2214 let err = store
2217 .handle_linked_chunk_updates(
2218 linked_chunk_id,
2219 vec![
2220 Update::NewItemsChunk {
2221 previous: None,
2222 new: ChunkIdentifier::new(42),
2223 next: None,
2224 },
2225 Update::NewItemsChunk {
2226 previous: None,
2227 new: ChunkIdentifier::new(42),
2228 next: None,
2229 },
2230 ],
2231 )
2232 .await
2233 .unwrap_err();
2234
2235 assert_matches!(err, crate::error::Error::Sqlite(err) => {
2237 assert_matches!(err.sqlite_error_code(), Some(rusqlite::ErrorCode::ConstraintViolation));
2238 });
2239
2240 let chunks = store.load_all_chunks(linked_chunk_id).await.unwrap();
2244 assert!(chunks.is_empty());
2245 }
2246}
2247
2248#[cfg(test)]
2249mod encrypted_tests {
2250 use std::sync::{
2251 LazyLock,
2252 atomic::{AtomicU32, Ordering::SeqCst},
2253 };
2254
2255 use matrix_sdk_base::{
2256 event_cache::store::{EventCacheStore, EventCacheStoreError},
2257 event_cache_store_integration_tests, event_cache_store_integration_tests_time,
2258 };
2259 use matrix_sdk_test::{async_test, event_factory::EventFactory};
2260 use ruma::{
2261 event_id,
2262 events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
2263 room_id, user_id,
2264 };
2265 use tempfile::{TempDir, tempdir};
2266
2267 use super::SqliteEventCacheStore;
2268
2269 static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
2270 static NUM: AtomicU32 = AtomicU32::new(0);
2271
2272 async fn get_event_cache_store() -> Result<SqliteEventCacheStore, EventCacheStoreError> {
2273 let name = NUM.fetch_add(1, SeqCst).to_string();
2274 let tmpdir_path = TMP_DIR.path().join(name);
2275
2276 tracing::info!("using event cache store @ {}", tmpdir_path.to_str().unwrap());
2277
2278 Ok(SqliteEventCacheStore::open(
2279 tmpdir_path.to_str().unwrap(),
2280 Some("default_test_password"),
2281 )
2282 .await
2283 .unwrap())
2284 }
2285
2286 event_cache_store_integration_tests!();
2287 event_cache_store_integration_tests_time!();
2288
2289 #[async_test]
2290 async fn test_no_sqlite_injection_in_find_event_relations() {
2291 let room_id = room_id!("!test:localhost");
2292 let another_room_id = room_id!("!r1:matrix.org");
2293 let sender = user_id!("@alice:localhost");
2294
2295 let store = get_event_cache_store()
2296 .await
2297 .expect("We should be able to create a new, empty, event cache store");
2298
2299 let f = EventFactory::new().room(room_id).sender(sender);
2300
2301 let event_id = event_id!("$DO_NOT_FIND_ME:matrix.org");
2303 let event = f.text_msg("DO NOT FIND").event_id(event_id).into_event();
2304
2305 let edit_id = event_id!("$find_me:matrix.org");
2307 let edit = f
2308 .text_msg("Find me")
2309 .event_id(edit_id)
2310 .edit(event_id, RoomMessageEventContentWithoutRelation::text_plain("jebote"))
2311 .into_event();
2312
2313 let f = f.room(another_room_id);
2315
2316 let another_event_id = event_id!("$DO_NOT_FIND_ME_EITHER:matrix.org");
2317 let another_event =
2318 f.text_msg("DO NOT FIND ME EITHER").event_id(another_event_id).into_event();
2319
2320 store.save_event(room_id, event).await.unwrap();
2322 store.save_event(room_id, edit).await.unwrap();
2323 store.save_event(another_room_id, another_event).await.unwrap();
2324
2325 let filter = Some(vec![RelationType::Replacement, "x\") OR 1=1; --".into()]);
2329
2330 let results = store
2332 .find_event_relations(room_id, event_id, filter.as_deref())
2333 .await
2334 .expect("We should be able to attempt to find event relations");
2335
2336 similar_asserts::assert_eq!(
2339 results.len(),
2340 1,
2341 "We should only have loaded events for the first room {results:#?}"
2342 );
2343
2344 let (found_event, _) = &results[0];
2346 assert_eq!(
2347 found_event.event_id(),
2348 Some(edit_id),
2349 "The single event we found should be the edit event"
2350 );
2351 }
2352}
2353
2354#[cfg(test)]
2355mod close_reopen_tests {
2356 use std::sync::{
2357 LazyLock,
2358 atomic::{AtomicU32, Ordering::SeqCst},
2359 };
2360
2361 use matrix_sdk_base::{event_cache::store::EventCacheStore, linked_chunk::LinkedChunkId};
2362 use matrix_sdk_test::{DEFAULT_TEST_ROOM_ID, async_test};
2363 use tempfile::{TempDir, tempdir};
2364
2365 use super::SqliteEventCacheStore;
2366
2367 static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
2368 static NUM: AtomicU32 = AtomicU32::new(0);
2369
2370 async fn new_store() -> SqliteEventCacheStore {
2371 let name = NUM.fetch_add(1, SeqCst).to_string();
2372 let tmpdir_path = TMP_DIR.path().join(name);
2373 SqliteEventCacheStore::open(tmpdir_path, None).await.unwrap()
2374 }
2375
2376 #[async_test]
2377 async fn test_close_completes_without_timeout() {
2378 let store = new_store().await;
2379
2380 let start = std::time::Instant::now();
2382 store.close().await.unwrap();
2383 let elapsed = start.elapsed();
2384
2385 assert!(
2386 elapsed < std::time::Duration::from_secs(2),
2387 "close() took {elapsed:?}, expected < 2s (no timeout)"
2388 );
2389
2390 let guard = store.connections.lock().await;
2392 assert!(guard.is_none(), "connections should be None after close");
2393 }
2394
2395 #[async_test]
2396 async fn test_reopen_restores_connections() {
2397 let store = new_store().await;
2398
2399 store.close().await.unwrap();
2400
2401 {
2402 let guard = store.connections.lock().await;
2403 assert!(guard.is_none());
2404 }
2405
2406 store.reopen().await.unwrap();
2407
2408 {
2409 let guard = store.connections.lock().await;
2410 assert!(guard.is_some(), "connections should be Some after reopen");
2411 }
2412 }
2413
2414 #[async_test]
2415 async fn test_close_is_idempotent() {
2416 let store = new_store().await;
2417
2418 store.close().await.unwrap();
2419 store.close().await.unwrap();
2421
2422 let guard = store.connections.lock().await;
2423 assert!(guard.is_none());
2424 }
2425
2426 #[async_test]
2427 async fn test_reopen_is_idempotent() {
2428 let store = new_store().await;
2429
2430 store.reopen().await.unwrap();
2432
2433 let guard = store.connections.lock().await;
2434 assert!(guard.is_some());
2435 }
2436
2437 #[async_test]
2438 async fn test_read_fails_when_closed() {
2439 let store = new_store().await;
2440 store.close().await.unwrap();
2441
2442 let err = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2443 assert!(err.is_err(), "read should fail when closed");
2444
2445 let err_msg = err.unwrap_err().to_string();
2446 assert!(err_msg.contains("closed"), "error should mention 'closed', got: {err_msg}");
2447 }
2448
2449 #[async_test]
2450 async fn test_write_fails_when_closed() {
2451 let store = new_store().await;
2452 store.close().await.unwrap();
2453
2454 let err = store.try_take_leased_lock(1000, "test_lock", "holder").await;
2455 assert!(err.is_err(), "write should fail when closed");
2456
2457 let err_msg = err.unwrap_err().to_string();
2458 assert!(err_msg.contains("closed"), "error should mention 'closed', got: {err_msg}");
2459 }
2460
2461 #[async_test]
2462 async fn test_data_persists_across_close_reopen() {
2463 let store = new_store().await;
2464
2465 let result = store.try_take_leased_lock(60_000, "test_lock", "holder").await.unwrap();
2467 assert!(result.is_some(), "should have acquired the lock");
2468
2469 store.close().await.unwrap();
2471 store.reopen().await.unwrap();
2472
2473 let result = store.try_take_leased_lock(60_000, "test_lock", "other_holder").await.unwrap();
2475 assert!(result.is_none(), "lock should still be held by the original holder after reopen");
2476 }
2477
2478 #[async_test]
2479 async fn test_multiple_close_reopen_cycles() {
2480 let store = new_store().await;
2481
2482 for _ in 0..5 {
2483 store.close().await.unwrap();
2484 store.reopen().await.unwrap();
2485
2486 let result = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2488 assert!(result.is_ok(), "store should work after close/reopen cycle");
2489 }
2490 }
2491
2492 #[async_test]
2493 async fn test_pool_is_fully_drained_after_close() {
2494 let store = new_store().await;
2495
2496 let _ = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2498 let _ = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2499
2500 store.close().await.unwrap();
2501
2502 let guard = store.connections.lock().await;
2505 assert!(guard.is_none(), "all connections should be released after close");
2506 }
2507
2508 #[async_test]
2509 async fn test_operations_work_immediately_after_reopen() {
2510 let store = new_store().await;
2511
2512 store.close().await.unwrap();
2513 store.reopen().await.unwrap();
2514
2515 let result = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2517 assert!(result.is_ok(), "read should succeed immediately after reopen");
2518
2519 let result = store.try_take_leased_lock(1000, "test_lock", "holder").await;
2521 assert!(result.is_ok(), "write should succeed immediately after reopen");
2522 }
2523
2524 #[async_test]
2525 async fn test_close_waits_for_held_read_connection_to_drain() {
2526 let store = new_store().await;
2527
2528 let held_conn = store.read().await.unwrap();
2530
2531 let store_clone = store.clone();
2534 let close_handle = tokio::spawn(async move {
2535 store_clone.close().await.unwrap();
2536 });
2537
2538 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2540
2541 assert!(!close_handle.is_finished(), "close should be waiting for the held connection");
2543
2544 drop(held_conn);
2546
2547 let timeout = tokio::time::timeout(std::time::Duration::from_secs(3), close_handle).await;
2549 assert!(timeout.is_ok(), "close should complete after the held connection is released");
2550 timeout.unwrap().unwrap();
2551
2552 let guard = store.connections.lock().await;
2554 assert!(guard.is_none(), "connections should be None after close");
2555 }
2556}