Skip to main content

matrix_sdk_sqlite/
event_cache_store.rs

1// Copyright 2024 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! An SQLite-based backend for the [`EventCacheStore`].
16
17use 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    },
35    linked_chunk::{
36        ChunkContent, ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId,
37        Position, RawChunk, Update,
38    },
39    timer,
40};
41use matrix_sdk_store_encryption::StoreCipher;
42use ruma::{
43    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, events::relation::RelationType,
44};
45use rusqlite::{
46    OptionalExtension, ToSql, Transaction, TransactionBehavior, params, params_from_iter,
47};
48use tokio::{
49    fs,
50    sync::{Mutex, OwnedMutexGuard},
51};
52use tracing::{debug, error, instrument, trace};
53
54use crate::{
55    OpenStoreError, RuntimeConfig, Secret, SqliteStoreConfig,
56    connection::{self, Connection as SqliteAsyncConn, Pool as SqlitePool, SqliteConnections},
57    error::{Error, Result},
58    utils::{
59        EncryptableStore, Key, SqliteAsyncConnExt, SqliteKeyValueStoreAsyncConnExt,
60        SqliteKeyValueStoreConnExt, SqliteTransactionExt, repeat_vars,
61    },
62};
63
64mod keys {
65    // Tables
66    pub const LINKED_CHUNKS: &str = "linked_chunks";
67    pub const EVENTS: &str = "events";
68}
69
70/// The database name.
71const DATABASE_NAME: &str = "matrix-sdk-event-cache.sqlite3";
72
73/// The string used to identify a chunk of type events, in the `type` field in
74/// the database.
75const CHUNK_TYPE_EVENT_TYPE_STRING: &str = "E";
76/// The string used to identify a chunk of type gap, in the `type` field in the
77/// database.
78const CHUNK_TYPE_GAP_TYPE_STRING: &str = "G";
79
80/// Type to support (de)encryption of keys and values for the
81/// [`SqliteEventCacheStore`].
82///
83/// See the [`SqliteEventCacheStore::encryption`] field.
84struct Encryption {
85    cipher: Option<StoreCipher>,
86}
87
88impl Encryption {
89    fn encode_event(&self, event: &TimelineEvent) -> Result<EncodedEvent> {
90        let serialized = serde_json::to_vec(event)?;
91
92        // Extract the relationship info here.
93        let raw_event = event.raw();
94        let (relates_to, rel_type) = extract_event_relation(raw_event).unzip();
95
96        // The content may be encrypted.
97        let content = self.encode_value(serialized)?;
98
99        Ok(EncodedEvent {
100            content,
101            rel_type,
102            relates_to: relates_to
103                .map(|relates_to| self.encode_event_id(keys::EVENTS, &relates_to)),
104        })
105    }
106
107    fn decode_event(&self, raw_encoded_event: &[u8]) -> Result<Event> {
108        Ok(serde_json::from_slice(&self.decode_value(raw_encoded_event)?)?)
109    }
110
111    /// Encode the event ID as a _key_: it cannot be decoded, but this is
112    /// stable.
113    fn encode_event_id(&self, table_name: &str, event_id: &EventId) -> Key {
114        self.encode_key(table_name, event_id)
115    }
116
117    /// Encode the room ID as a _key_: it cannot be decoded, but this is
118    /// stable.
119    fn encode_room_id(&self, table_name: &str, room_id: &RoomId) -> Key {
120        self.encode_key(table_name, room_id)
121    }
122
123    /// Encode a [`LinkedChunkId`].
124    fn encode_linked_chunk(&self, table_name: &str, linked_chunk_id: &LinkedChunkId<'_>) -> Key {
125        self.encode_key(table_name, linked_chunk_id.storage_key())
126    }
127
128    /// Encode a thread ID (which is an [`EventId`]).
129    fn encode_thread_id(&self, thread_id: &EventId) -> Result<Vec<u8>> {
130        self.encode_value(String::from(thread_id.as_str()))
131    }
132
133    /// Decode a thread ID (which is an [`EventId`]).
134    #[cfg(test)]
135    fn decode_thread_id(&self, encoded_thread_id: &[u8]) -> Result<OwnedEventId> {
136        let as_slice = self.decode_value(encoded_thread_id)?;
137        let as_str = str::from_utf8(as_slice.as_ref())?;
138
139        Ok(EventId::parse(as_str)?)
140    }
141}
142
143impl EncryptableStore for Encryption {
144    fn get_cypher(&self) -> Option<&StoreCipher> {
145        self.cipher.as_ref()
146    }
147}
148
149/// An SQLite-based event cache store.
150#[derive(Clone)]
151pub struct SqliteEventCacheStore {
152    /// Type to encrypt keys and values.
153    encryption: Arc<Encryption>,
154
155    /// `Some` when active, `None` when closed.
156    connections: Arc<Mutex<Option<SqliteConnections>>>,
157
158    /// Retained so we can rebuild the pool on reopen.
159    db_path: PathBuf,
160
161    /// Retained so we can rebuild the pool on reopen.
162    pool_config: PoolConfig,
163
164    /// Retained so we can re-apply runtime config on reopen.
165    runtime_config: RuntimeConfig,
166}
167
168#[cfg(not(tarpaulin_include))]
169impl fmt::Debug for SqliteEventCacheStore {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.debug_struct("SqliteEventCacheStore").finish_non_exhaustive()
172    }
173}
174
175impl SqliteEventCacheStore {
176    /// Open the SQLite-based event cache store at the given path using the
177    /// given passphrase to encrypt private data.
178    pub async fn open(
179        path: impl AsRef<Path>,
180        passphrase: Option<&str>,
181    ) -> Result<Self, OpenStoreError> {
182        Self::open_with_config(&SqliteStoreConfig::new(path).passphrase(passphrase)).await
183    }
184
185    /// Open the SQLite-based event cache store at the given path using the
186    /// given key to encrypt private data.
187    pub async fn open_with_key(
188        path: impl AsRef<Path>,
189        key: Option<&[u8; 32]>,
190    ) -> Result<Self, OpenStoreError> {
191        Self::open_with_config(&SqliteStoreConfig::new(path).key(key)).await
192    }
193
194    /// Open the SQLite-based event cache store with the config open config.
195    #[instrument(skip(config), fields(path = ?config.path))]
196    pub async fn open_with_config(config: &SqliteStoreConfig) -> Result<Self, OpenStoreError> {
197        debug!(?config);
198
199        let _timer = timer!("open_with_config");
200
201        fs::create_dir_all(&config.path).await.map_err(OpenStoreError::CreateDir)?;
202
203        let db_path = config.path.join(DATABASE_NAME);
204        let pool_config = config.pool_config();
205        let runtime_config = config.runtime_config();
206
207        let pool = config.build_pool_of_connections(DATABASE_NAME)?;
208
209        let this =
210            Self::open_with_pool(pool, db_path, pool_config, runtime_config, config.secret.clone())
211                .await?;
212
213        // Apply runtime config on the write connection.
214        this.write().await?.apply_runtime_config(runtime_config).await?;
215
216        Ok(this)
217    }
218
219    /// Open an SQLite-based event cache store using the given SQLite database
220    /// pool. The given secret will be used to encrypt private data.
221    async fn open_with_pool(
222        pool: SqlitePool,
223        db_path: PathBuf,
224        pool_config: PoolConfig,
225        runtime_config: RuntimeConfig,
226        secret: Option<Secret>,
227    ) -> Result<Self, OpenStoreError> {
228        let conn = pool.get().await?;
229
230        let version = conn.db_version().await?;
231
232        run_migrations(&conn, version).await?;
233
234        conn.wal_checkpoint().await;
235
236        let cipher = match secret {
237            Some(s) => Some(conn.get_or_create_store_cipher(s).await?),
238            None => None,
239        };
240
241        let connections = SqliteConnections {
242            pool,
243            // Use `conn` as our selected write connection.
244            write_connection: Arc::new(Mutex::new(conn)),
245        };
246
247        Ok(Self {
248            encryption: Arc::new(Encryption { cipher }),
249            connections: Arc::new(Mutex::new(Some(connections))),
250            db_path,
251            pool_config,
252            runtime_config,
253        })
254    }
255
256    /// Acquire a connection for executing read operations.
257    #[instrument(skip_all)]
258    async fn read(&self) -> Result<SqliteAsyncConn> {
259        let pool = {
260            let guard = self.connections.lock().await;
261            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
262            conns.pool.clone()
263        };
264
265        let connection = pool.get().await?;
266
267        // Per https://www.sqlite.org/foreignkeys.html#fk_enable, foreign key
268        // support must be enabled on a per-connection basis. Execute it every
269        // time we try to get a connection, since we can't guarantee a previous
270        // connection did enable it before.
271        connection.execute_batch("PRAGMA foreign_keys = ON;").await?;
272
273        Ok(connection)
274    }
275
276    /// Acquire a connection for executing write operations.
277    #[instrument(skip_all)]
278    async fn write(&self) -> Result<OwnedMutexGuard<SqliteAsyncConn>> {
279        let write_connection = {
280            let guard = self.connections.lock().await;
281            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
282            conns.write_connection.clone()
283        };
284
285        let connection = write_connection.lock_owned().await;
286
287        // Per https://www.sqlite.org/foreignkeys.html#fk_enable, foreign key
288        // support must be enabled on a per-connection basis. Execute it every
289        // time we try to get a connection, since we can't guarantee a previous
290        // connection did enable it before.
291        connection.execute_batch("PRAGMA foreign_keys = ON;").await?;
292
293        Ok(connection)
294    }
295
296    fn map_row_to_chunk(
297        row: &rusqlite::Row<'_>,
298    ) -> Result<(u64, Option<u64>, Option<u64>, String), rusqlite::Error> {
299        Ok((
300            row.get::<_, u64>(0)?,
301            row.get::<_, Option<u64>>(1)?,
302            row.get::<_, Option<u64>>(2)?,
303            row.get::<_, String>(3)?,
304        ))
305    }
306
307    pub async fn vacuum(&self) -> Result<()> {
308        let write_connection = {
309            let guard = self.connections.lock().await;
310            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
311            conns.write_connection.clone()
312        };
313        write_connection.lock().await.vacuum().await
314    }
315
316    async fn get_db_size(&self) -> Result<Option<usize>> {
317        let pool = {
318            let guard = self.connections.lock().await;
319            let conns = guard.as_ref().ok_or(Error::StoreClosed)?;
320            conns.pool.clone()
321        };
322        Ok(Some(pool.get().await?.get_db_size().await?))
323    }
324
325    pub async fn close(&self) -> Result<()> {
326        connection::close_connections(&self.connections, "Event cache store").await;
327        Ok(())
328    }
329
330    pub async fn reopen(&self) -> Result<()> {
331        connection::reopen_connections(
332            &self.connections,
333            self.db_path.clone(),
334            self.pool_config,
335            self.runtime_config,
336        )
337        .await?;
338        Ok(())
339    }
340
341    /// Returns the pool size status, for testing purposes.
342    #[cfg(test)]
343    async fn pool_max_size(&self) -> Option<usize> {
344        let guard = self.connections.lock().await;
345        guard.as_ref().map(|conns| conns.pool.status().max_size)
346    }
347}
348
349struct EncodedEvent {
350    content: Vec<u8>,
351    rel_type: Option<String>,
352    relates_to: Option<Key>,
353}
354
355trait TransactionExtForLinkedChunks {
356    fn rebuild_chunk(
357        &self,
358        encryption: &Encryption,
359        linked_chunk_id: &Key,
360        previous: Option<u64>,
361        index: u64,
362        next: Option<u64>,
363        chunk_type: &str,
364    ) -> Result<RawChunk<Event, Gap>>;
365
366    fn load_gap_content(
367        &self,
368        encryption: &Encryption,
369        linked_chunk_id: &Key,
370        chunk_id: ChunkIdentifier,
371    ) -> Result<Gap>;
372
373    fn load_events_content(
374        &self,
375        encryption: &Encryption,
376        linked_chunk_id: &Key,
377        chunk_id: ChunkIdentifier,
378    ) -> Result<Vec<Event>>;
379}
380
381impl TransactionExtForLinkedChunks for Transaction<'_> {
382    fn rebuild_chunk(
383        &self,
384        encryption: &Encryption,
385        linked_chunk_id: &Key,
386        previous: Option<u64>,
387        id: u64,
388        next: Option<u64>,
389        chunk_type: &str,
390    ) -> Result<RawChunk<Event, Gap>> {
391        let previous = previous.map(ChunkIdentifier::new);
392        let next = next.map(ChunkIdentifier::new);
393        let id = ChunkIdentifier::new(id);
394
395        match chunk_type {
396            CHUNK_TYPE_GAP_TYPE_STRING => {
397                // It's a gap!
398                let gap = self.load_gap_content(encryption, linked_chunk_id, id)?;
399                Ok(RawChunk { content: ChunkContent::Gap(gap), previous, identifier: id, next })
400            }
401
402            CHUNK_TYPE_EVENT_TYPE_STRING => {
403                // It's events!
404                let events = self.load_events_content(encryption, linked_chunk_id, id)?;
405                Ok(RawChunk {
406                    content: ChunkContent::Items(events),
407                    previous,
408                    identifier: id,
409                    next,
410                })
411            }
412
413            other => {
414                // It's an error!
415                Err(Error::InvalidData {
416                    details: format!("a linked chunk has an unknown type {other}"),
417                })
418            }
419        }
420    }
421
422    fn load_gap_content(
423        &self,
424        encryption: &Encryption,
425        linked_chunk_id: &Key,
426        chunk_id: ChunkIdentifier,
427    ) -> Result<Gap> {
428        // There's at most one row for it in the database, so a call to `query_row` is
429        // sufficient.
430        let encoded_prev_token: Vec<u8> = self.query_row(
431            "SELECT prev_token FROM gap_chunks WHERE chunk_id = ? AND linked_chunk_id = ?",
432            (chunk_id.index(), &linked_chunk_id),
433            |row| row.get(0),
434        )?;
435        let prev_token_bytes = encryption.decode_value(&encoded_prev_token)?;
436        let prev_token = String::from_utf8(prev_token_bytes.into_owned())?;
437        Ok(Gap { token: prev_token })
438    }
439
440    fn load_events_content(
441        &self,
442        encryption: &Encryption,
443        linked_chunk_id: &Key,
444        chunk_id: ChunkIdentifier,
445    ) -> Result<Vec<Event>> {
446        // Retrieve all the events from the database.
447        let mut events = Vec::new();
448
449        for event_data in self
450            .prepare(
451                "SELECT events.content \
452                    FROM event_chunks ec, events \
453                    WHERE events.event_id = ec.event_id AND ec.chunk_id = ? AND ec.linked_chunk_id = ? \
454                    ORDER BY ec.position ASC",
455            )?
456            .query_map((chunk_id.index(), &linked_chunk_id), |row| row.get::<_, Vec<u8>>(0))?
457        {
458            events.push(encryption.decode_event(&event_data?)?);
459        }
460
461        Ok(events)
462    }
463}
464
465/// Run migrations for the given version of the database.
466async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> {
467    // Always enable foreign keys for the current connection.
468    conn.execute_batch("PRAGMA foreign_keys = ON;").await?;
469
470    if version < 1 {
471        debug!("Creating database");
472        // First turn on WAL mode, this can't be done in the transaction, it fails with
473        // the error message: "cannot change into wal mode from within a transaction".
474        conn.execute_batch("PRAGMA journal_mode = wal;").await?;
475        conn.with_transaction(|txn| {
476            txn.execute_batch(include_str!("../migrations/event_cache_store/001_init.sql"))?;
477            txn.set_db_version(1)
478        })
479        .await?;
480    }
481
482    if version < 2 {
483        debug!("Upgrading database to version 2");
484        conn.with_transaction(|txn| {
485            txn.execute_batch(include_str!("../migrations/event_cache_store/002_lease_locks.sql"))?;
486            txn.set_db_version(2)
487        })
488        .await?;
489    }
490
491    if version < 3 {
492        debug!("Upgrading database to version 3");
493        conn.with_transaction(|txn| {
494            txn.execute_batch(include_str!("../migrations/event_cache_store/003_events.sql"))?;
495            txn.set_db_version(3)
496        })
497        .await?;
498    }
499
500    if version < 4 {
501        debug!("Upgrading database to version 4");
502        conn.with_transaction(|txn| {
503            txn.execute_batch(include_str!(
504                "../migrations/event_cache_store/004_ignore_policy.sql"
505            ))?;
506            txn.set_db_version(4)
507        })
508        .await?;
509    }
510
511    if version < 5 {
512        debug!("Upgrading database to version 5");
513        conn.with_transaction(|txn| {
514            txn.execute_batch(include_str!(
515                "../migrations/event_cache_store/005_events_index_on_event_id.sql"
516            ))?;
517            txn.set_db_version(5)
518        })
519        .await?;
520    }
521
522    if version < 6 {
523        debug!("Upgrading database to version 6");
524        conn.with_transaction(|txn| {
525            txn.execute_batch(include_str!("../migrations/event_cache_store/006_events.sql"))?;
526            txn.set_db_version(6)
527        })
528        .await?;
529    }
530
531    if version < 7 {
532        debug!("Upgrading database to version 7");
533        conn.with_transaction(|txn| {
534            txn.execute_batch(include_str!(
535                "../migrations/event_cache_store/007_event_chunks.sql"
536            ))?;
537            txn.set_db_version(7)
538        })
539        .await?;
540    }
541
542    if version < 8 {
543        debug!("Upgrading database to version 8");
544        conn.with_transaction(|txn| {
545            txn.execute_batch(include_str!(
546                "../migrations/event_cache_store/008_linked_chunk_id.sql"
547            ))?;
548            txn.set_db_version(8)
549        })
550        .await?;
551    }
552
553    if version < 9 {
554        debug!("Upgrading database to version 9");
555        conn.with_transaction(|txn| {
556            txn.execute_batch(include_str!(
557                "../migrations/event_cache_store/009_related_event_index.sql"
558            ))?;
559            txn.set_db_version(9)
560        })
561        .await?;
562    }
563
564    if version < 10 {
565        debug!("Upgrading database to version 10");
566        conn.with_transaction(|txn| {
567            txn.execute_batch(include_str!("../migrations/event_cache_store/010_drop_media.sql"))?;
568            txn.set_db_version(10)
569        })
570        .await?;
571
572        if version >= 1 {
573            // Defragment the DB and optimize its size on the filesystem now that we removed
574            // the media cache.
575            conn.vacuum().await?;
576        }
577    }
578
579    if version < 11 {
580        debug!("Upgrading database to version 11");
581        conn.with_transaction(|txn| {
582            txn.execute_batch(include_str!(
583                "../migrations/event_cache_store/011_empty_event_cache.sql"
584            ))?;
585            txn.set_db_version(11)
586        })
587        .await?;
588    }
589
590    if version < 12 {
591        debug!("Upgrading database to version 12");
592        conn.with_transaction(|txn| {
593            txn.execute_batch(include_str!(
594                "../migrations/event_cache_store/012_store_event_type.sql"
595            ))?;
596            txn.set_db_version(12)
597        })
598        .await?;
599    }
600
601    if version < 13 {
602        debug!("Upgrading database to version 13");
603        conn.with_transaction(|txn| {
604            txn.execute_batch(include_str!(
605                "../migrations/event_cache_store/013_lease_locks_with_generation.sql"
606            ))?;
607            txn.set_db_version(13)
608        })
609        .await?;
610    }
611
612    if version < 14 {
613        debug!("Upgrading database to version 14");
614        conn.with_transaction(|txn| {
615            txn.execute_batch(include_str!(
616                "../migrations/event_cache_store/014_event_chunks_event_id_index.sql"
617            ))?;
618            txn.set_db_version(14)
619        })
620        .await?;
621    }
622
623    if version < 15 {
624        debug!("Upgrading database to version 15");
625        conn.with_transaction(|txn| {
626            txn.execute_batch(include_str!(
627                "../migrations/event_cache_store/015_event_ids_are_encoded.sql"
628            ))?;
629            txn.set_db_version(15)
630        })
631        .await?;
632    }
633
634    if version < 16 {
635        debug!("Upgrading database to version 16");
636        conn.with_transaction(|txn| {
637            txn.execute_batch(include_str!("../migrations/event_cache_store/016_threads.sql"))?;
638            txn.set_db_version(16)
639        })
640        .await?;
641    }
642
643    Ok(())
644}
645
646#[async_trait]
647impl EventCacheStore for SqliteEventCacheStore {
648    type Error = Error;
649
650    #[instrument(skip(self))]
651    async fn try_take_leased_lock(
652        &self,
653        lease_duration_ms: u32,
654        key: &str,
655        holder: &str,
656    ) -> Result<Option<CrossProcessLockGeneration>> {
657        let key = key.to_owned();
658        let holder = holder.to_owned();
659
660        let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
661        let expiration = now + lease_duration_ms as u64;
662
663        // Learn about the `excluded` keyword in https://sqlite.org/lang_upsert.html.
664        let generation = self
665            .write()
666            .await?
667            .with_transaction(move |txn| {
668                txn.query_row(
669                    "INSERT INTO lease_locks (key, holder, expiration) \
670                    VALUES (?1, ?2, ?3) \
671                    ON CONFLICT (key) \
672                    DO \
673                        UPDATE SET \
674                            holder = excluded.holder, \
675                            expiration = excluded.expiration, \
676                            generation = \
677                                CASE holder \
678                                    WHEN excluded.holder THEN generation \
679                                    ELSE generation + 1 \
680                                END \
681                        WHERE \
682                            holder = excluded.holder \
683                            OR expiration < ?4 \
684                    RETURNING generation",
685                    (key, holder, expiration, now),
686                    |row| row.get(0),
687                )
688                .optional()
689            })
690            .await?;
691
692        Ok(generation)
693    }
694
695    #[instrument(skip(self, updates))]
696    async fn handle_linked_chunk_updates(
697        &self,
698        linked_chunk_id: LinkedChunkId<'_>,
699        updates: Vec<Update<Event, Gap>>,
700    ) -> Result<(), Self::Error> {
701        let _timer = timer!("method");
702
703        let hashed_linked_chunk_id =
704            self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
705        let hashed_room_id =
706            self.encryption.encode_room_id(keys::EVENTS, linked_chunk_id.room_id());
707        let encryption = self.encryption.clone();
708
709        // Use a single transaction throughout this function, so that either all updates
710        // work, or none is taken into account.
711        with_immediate_transaction(self, move |txn| {
712            for update in updates {
713                match update {
714                    Update::NewItemsChunk { previous, new, next } => {
715                        let previous = previous.as_ref().map(ChunkIdentifier::index);
716                        let new = new.index();
717                        let next = next.as_ref().map(ChunkIdentifier::index);
718
719                        trace!("new events chunk (prev={previous:?}, i={new}, next={next:?})");
720
721                        insert_chunk(
722                            txn,
723                            &hashed_linked_chunk_id,
724                            previous,
725                            new,
726                            next,
727                            CHUNK_TYPE_EVENT_TYPE_STRING,
728                        )?;
729                    }
730
731                    Update::NewGapChunk { previous, new, next, gap } => {
732                        let hashed_prev_token = encryption.encode_value(gap.token)?;
733
734                        let previous = previous.as_ref().map(ChunkIdentifier::index);
735                        let new = new.index();
736                        let next = next.as_ref().map(ChunkIdentifier::index);
737
738                        trace!("new gap chunk (prev={previous:?}, i={new}, next={next:?})");
739
740                        // Insert the chunk as a gap.
741                        insert_chunk(
742                            txn,
743                            &hashed_linked_chunk_id,
744                            previous,
745                            new,
746                            next,
747                            CHUNK_TYPE_GAP_TYPE_STRING,
748                        )?;
749
750                        // Insert the gap's value.
751                        txn.execute(
752                            r#"
753                            INSERT INTO gap_chunks(chunk_id, linked_chunk_id, prev_token)
754                            VALUES (?, ?, ?)
755                        "#,
756                            (new, &hashed_linked_chunk_id, hashed_prev_token),
757                        )?;
758                    }
759
760                    Update::RemoveChunk(chunk_identifier) => {
761                        let chunk_id = chunk_identifier.index();
762
763                        trace!("removing chunk @ {chunk_id}");
764
765                        // Find chunk to delete.
766                        let (previous, next): (Option<usize>, Option<usize>) = txn.query_row(
767                            "SELECT previous, next FROM linked_chunks WHERE id = ? AND linked_chunk_id = ?",
768                            (chunk_id, &hashed_linked_chunk_id),
769                            |row| Ok((row.get(0)?, row.get(1)?))
770                        )?;
771
772                        // Replace its previous' next to its own next.
773                        if let Some(previous) = previous {
774                            txn.execute("UPDATE linked_chunks SET next = ? WHERE id = ? AND linked_chunk_id = ?", (next, previous, &hashed_linked_chunk_id))?;
775                        }
776
777                        // Replace its next' previous to its own previous.
778                        if let Some(next) = next {
779                            txn.execute("UPDATE linked_chunks SET previous = ? WHERE id = ? AND linked_chunk_id = ?", (previous, next, &hashed_linked_chunk_id))?;
780                        }
781
782                        // Now delete it, and let cascading delete corresponding entries in the
783                        // other data tables.
784                        txn.execute("DELETE FROM linked_chunks WHERE id = ? AND linked_chunk_id = ?", (chunk_id, &hashed_linked_chunk_id))?;
785                    }
786
787                    Update::PushItems { at, items } => {
788                        if items.is_empty() {
789                            // Should never happens, but better be safe.
790                            continue;
791                        }
792
793                        let chunk_id = at.chunk_identifier().index();
794
795                        trace!("pushing {} items @ {chunk_id}", items.len());
796
797                        let mut chunk_statement = txn.prepare(
798                            "INSERT INTO event_chunks(chunk_id, linked_chunk_id, event_id, position) VALUES (?, ?, ?, ?)"
799                        )?;
800
801                        // Note: we use `OR REPLACE` here, because the event might have been
802                        // already inserted in the database. This is the case when an event is
803                        // deduplicated and moved to another position; or because it was inserted
804                        // outside the context of a linked chunk (e.g. pinned event).
805                        let mut content_statement = txn.prepare(
806                            "INSERT OR REPLACE INTO events(room_id, event_id, event_type, session_id, content, relates_to, rel_type) VALUES (?, ?, ?, ?, ?, ?, ?)"
807                        )?;
808
809                        let invalid_event = |event: TimelineEvent| {
810                            let Some(event_id) = event.event_id() else {
811                                error!("Trying to push an event with no ID");
812                                return None;
813                            };
814
815                            let Some(event_type) = event.kind.event_type() else {
816                                error!(%event_id, "Trying to save an event with no event type");
817                                return None;
818                            };
819
820                            Some((event_id.to_owned(), event_type, event))
821                        };
822
823                        for (i, (event_id, event_type, event)) in items.into_iter().filter_map(invalid_event).enumerate() {
824                            let hashed_event_id = encryption.encode_event_id(
825                                // For the event ID, we need a stable hash between the `events`
826                                // and `event_chunks` tables. That's why we use `keys::EVENTS`
827                                // even if `hashed_event_id` is sometimes only used in
828                                // `events_chunks`.
829                                keys::EVENTS,
830                                &event_id,
831                            );
832
833                            // Table `event_chunks`.
834                            {
835                                let index = at.index() + i;
836
837                                chunk_statement.execute((chunk_id, &hashed_linked_chunk_id, &hashed_event_id, index))?;
838                            }
839
840                            // Table `events`.
841                            {
842                                let hashed_session_id = event.kind.session_id().map(|s| encryption.encode_key(keys::EVENTS, s));
843                                let hashed_event_type = encryption.encode_key(keys::EVENTS, event_type);
844                                let encoded_event = encryption.encode_event(&event)?;
845
846                                content_statement.execute((
847                                    &hashed_room_id,
848                                    &hashed_event_id,
849                                    hashed_event_type,
850                                    hashed_session_id,
851                                    encoded_event.content,
852                                    encoded_event.relates_to,
853                                    encoded_event.rel_type
854                                ))?;
855                            }
856                        }
857                    }
858
859                    Update::ReplaceItem { at, item: event } => {
860                        let chunk_id = at.chunk_identifier().index();
861                        let index = at.index();
862
863                        trace!("replacing item @ {chunk_id}:{index}");
864
865                        // The event ID should be the same, but just in case it changed…
866                        let Some(event_id) = event.event_id().map(|event_id| event_id.to_owned()) else {
867                            error!("Trying to replace an event with a new one that has no ID");
868                            continue;
869                        };
870
871                        let Some(event_type) = event.kind.event_type() else {
872                            error!(%event_id, "Trying to save an event with no event type");
873                            continue;
874                        };
875
876                        let hashed_event_id = encryption.encode_event_id(keys::EVENTS, &event_id);
877
878                        // Before updating the event in its chunk, we must ensure the event exists:
879                        // either we insert it, or we update it. Note that it's possible to replace
880                        // an event by itself (with different encryption info for example, or from
881                        // UTD to decrypted, stuff like that).
882
883                        // Table `events`.
884                        {
885                            let hashed_session_id = event.kind.session_id().map(|s| encryption.encode_key(keys::EVENTS, s));
886                            let hashed_event_type = encryption.encode_key(keys::EVENTS, event_type);
887                            let encoded_event = encryption.encode_event(&event)?;
888
889                            txn.execute(
890                                "INSERT OR REPLACE INTO events(room_id, event_id, event_type, session_id, content, relates_to, rel_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
891                                (
892                                    &hashed_room_id,
893                                    &hashed_event_id,
894                                    hashed_event_type,
895                                    hashed_session_id,
896                                    encoded_event.content,
897                                    encoded_event.relates_to,
898                                    encoded_event.rel_type
899                                ),
900                            )?;
901                        }
902
903                        // Table `event_chunks`.
904                        {
905                            // Replace the event at position `index` in chunk `chunk_id` by updating the event ID.
906                            txn.execute(
907                                r#"UPDATE event_chunks SET event_id = ? WHERE linked_chunk_id = ? AND chunk_id = ? AND position = ?"#,
908                                (&hashed_event_id, &hashed_linked_chunk_id, chunk_id, index)
909                            )?;
910                        }
911                    }
912
913                    Update::RemoveItem { at } => {
914                        let chunk_id = at.chunk_identifier().index();
915                        let index = at.index();
916
917                        trace!("removing item @ {chunk_id}:{index}");
918
919                        // Remove the entry in the chunk table.
920                        txn.execute("DELETE FROM event_chunks WHERE linked_chunk_id = ? AND chunk_id = ? AND position = ?", (&hashed_linked_chunk_id, chunk_id, index))?;
921
922                        // Decrement the index of each item after the one we are
923                        // going to remove.
924                        //
925                        // Imagine we have the following events:
926                        //
927                        // | event_id | linked_chunk_id | chunk_id | position |
928                        // |----------|-----------------|----------|----------|
929                        // | $ev0     | !r0             | 42       | 0        |
930                        // | $ev1     | !r0             | 42       | 1        |
931                        // | $ev2     | !r0             | 42       | 2        |
932                        // | $ev3     | !r0             | 42       | 3        |
933                        // | $ev4     | !r0             | 42       | 4        |
934                        //
935                        // `$ev2` has been removed, then we end up in this
936                        // state:
937                        //
938                        // | event_id | linked_chunk_id    | chunk_id | position |
939                        // |----------|--------------------|----------|----------|
940                        // | $ev0     | !r0                | 42       | 0        |
941                        // | $ev1     | !r0                | 42       | 1        |
942                        // |          |                    |          |          | <- no more `$ev2`
943                        // | $ev3     | !r0                | 42       | 3        |
944                        // | $ev4     | !r0                | 42       | 4        |
945                        //
946                        // We need to shift the `position` of `$ev3` and `$ev4`
947                        // to `position - 1`, like so:
948                        //
949                        // | event_id | linked_chunk_id | chunk_id | position |
950                        // |----------|-----------------|----------|----------|
951                        // | $ev0     | !r0             | 42       | 0        |
952                        // | $ev1     | !r0             | 42       | 1        |
953                        // | $ev3     | !r0             | 42       | 2        |
954                        // | $ev4     | !r0             | 42       | 3        |
955                        //
956                        // Usually, it boils down to run the following query:
957                        //
958                        // ```sql
959                        // UPDATE event_chunks
960                        // SET position = position - 1
961                        // WHERE position > 2 AND …
962                        // ```
963                        //
964                        // Okay. But `UPDATE` runs on rows in no particular
965                        // order. It means that it can update `$ev4` before
966                        // `$ev3` for example. What happens in this particular
967                        // case? The `position` of `$ev4` becomes `3`, however
968                        // `$ev3` already has `position = 3`. Because there
969                        // is a `UNIQUE` constraint on `(linked_chunk_id, chunk_id,
970                        // position)`, it will result in a constraint violation.
971                        //
972                        // There is **no way** to control the execution order of
973                        // `UPDATE` in SQLite. To persuade yourself, try:
974                        //
975                        // ```sql
976                        // UPDATE event_chunks
977                        // SET position = position - 1
978                        // FROM (
979                        //     SELECT event_id
980                        //     FROM event_chunks
981                        //     WHERE position > 2 AND …
982                        //     ORDER BY position ASC
983                        // ) as ordered
984                        // WHERE event_chunks.event_id = ordered.event_id
985                        // ```
986                        //
987                        // It will fail the same way.
988                        //
989                        // Thus, we have 2 solutions:
990                        //
991                        // 1. Remove the `UNIQUE` constraint,
992                        // 2. Be creative.
993                        //
994                        // The `UNIQUE` constraint is a safe belt. Normally, we
995                        // have `event_cache::Deduplicator` that is responsible
996                        // to ensure there is no duplicated event. However,
997                        // relying on this is “fragile” in the sense it can
998                        // contain bugs. Relying on the `UNIQUE` constraint from
999                        // SQLite is more robust. It's “braces and belt” as we
1000                        // say here.
1001                        //
1002                        // So. We need to be creative.
1003                        //
1004                        // Many solutions exist. Amongst the most popular, we
1005                        // see _dropping and re-creating the index_, which is
1006                        // no-go for us, it's too expensive. I (@hywan) have
1007                        // adopted the following one:
1008                        //
1009                        // - Do `position = position - 1` but in the negative
1010                        //   space, so `position = -(position - 1)`. A position
1011                        //   cannot be negative; we are sure it is unique!
1012                        // - Once all candidate rows are updated, do `position =
1013                        //   -position` to move back to the positive space.
1014                        //
1015                        // 'told you it's gonna be creative.
1016                        //
1017                        // This solution is a hack, **but** it is a small
1018                        // number of operations, and we can keep the `UNIQUE`
1019                        // constraint in place.
1020                        txn.execute(
1021                            r#"
1022                                UPDATE event_chunks
1023                                SET position = -(position - 1)
1024                                WHERE linked_chunk_id = ? AND chunk_id = ? AND position > ?
1025                            "#,
1026                            (&hashed_linked_chunk_id, chunk_id, index)
1027                        )?;
1028                        txn.execute(
1029                            r#"
1030                                UPDATE event_chunks
1031                                SET position = -position
1032                                WHERE position < 0 AND linked_chunk_id = ? AND chunk_id = ?
1033                            "#,
1034                            (&hashed_linked_chunk_id, chunk_id)
1035                        )?;
1036
1037                        // We don't remove the events from `events` purposely
1038                        // because they can be used by another `LinkedChunkId`.
1039                    }
1040
1041                    Update::DetachLastItems { at } => {
1042                        let chunk_id = at.chunk_identifier().index();
1043                        let index = at.index();
1044
1045                        trace!("truncating items >= {chunk_id}:{index}");
1046
1047                        // Remove these entries.
1048                        txn.execute("DELETE FROM event_chunks WHERE linked_chunk_id = ? AND chunk_id = ? AND position >= ?", (&hashed_linked_chunk_id, chunk_id, index))?;
1049
1050                        // We don't remove the events from `events` purposely
1051                        // because they can be used by another `LinkedChunkId`.
1052                    }
1053
1054                    Update::Clear => {
1055                        trace!("clearing items");
1056
1057                        // Remove chunks, and let cascading do its job.
1058                        txn.execute(
1059                            "DELETE FROM linked_chunks WHERE linked_chunk_id = ?",
1060                            (&hashed_linked_chunk_id,),
1061                        )?;
1062
1063                        // We don't remove the events from `events` purposely
1064                        // because they can be used by another `LinkedChunkId`.
1065                    }
1066
1067                    Update::StartReattachItems | Update::EndReattachItems => {
1068                        // Nothing.
1069                    }
1070                }
1071            }
1072
1073            Ok(())
1074        })
1075        .await?;
1076
1077        Ok(())
1078    }
1079
1080    #[instrument(skip(self))]
1081    async fn load_all_chunks(
1082        &self,
1083        linked_chunk_id: LinkedChunkId<'_>,
1084    ) -> Result<Vec<RawChunk<Event, Gap>>, Self::Error> {
1085        let _timer = timer!("method");
1086
1087        let hashed_linked_chunk_id =
1088            self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1089        let encryption = self.encryption.clone();
1090
1091        let result = self
1092            .read()
1093            .await?
1094            .with_transaction(move |txn| -> Result<_> {
1095                let mut items = Vec::new();
1096
1097                // Use `ORDER BY id` to get a deterministic ordering for testing purposes.
1098                for data in txn
1099                    .prepare(
1100                        "SELECT id, previous, next, type FROM linked_chunks WHERE linked_chunk_id = ? ORDER BY id",
1101                    )?
1102                    .query_map((&hashed_linked_chunk_id,), Self::map_row_to_chunk)?
1103                {
1104                    let (id, previous, next, chunk_type) = data?;
1105                    let new = txn.rebuild_chunk(
1106                        &encryption,
1107                        &hashed_linked_chunk_id,
1108                        previous,
1109                        id,
1110                        next,
1111                        chunk_type.as_str(),
1112                    )?;
1113                    items.push(new);
1114                }
1115
1116                Ok(items)
1117            })
1118            .await?;
1119
1120        Ok(result)
1121    }
1122
1123    #[instrument(skip(self))]
1124    async fn load_all_chunks_metadata(
1125        &self,
1126        linked_chunk_id: LinkedChunkId<'_>,
1127    ) -> Result<Vec<ChunkMetadata>, Self::Error> {
1128        let _timer = timer!("method");
1129
1130        let hashed_linked_chunk_id =
1131            self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1132
1133        self.read()
1134            .await?
1135            .with_transaction(move |txn| -> Result<_> {
1136                // We want to collect the metadata about each chunk (id, next, previous), and
1137                // for event chunks, the number of events in it. For gaps, the
1138                // number of events is 0, by convention.
1139                //
1140                // We've tried different strategies over time:
1141                // - use a `LEFT JOIN` + `COUNT`, which was extremely inefficient because it
1142                //   caused a full table traversal for each chunk, including for gaps which
1143                //   don't have any events. This happened in
1144                //   https://github.com/matrix-org/matrix-rust-sdk/pull/5225.
1145                // - use a `CASE` statement on the chunk's type: if it's an event chunk, run an
1146                //   additional `SELECT` query. It was an immense improvement, but still caused
1147                //   one select query per event chunk. This happened in
1148                //   https://github.com/matrix-org/matrix-rust-sdk/pull/5411.
1149                //
1150                // The current solution is to run two queries:
1151                // - one to get each chunk and its number of events, by doing a single `SELECT`
1152                //   query over the `event_chunks` table, grouping by chunk ids. This gives us a
1153                //   list of `(chunk_id, num_events)` pairs, which can be transformed into a
1154                //   hashmap.
1155                // - one to get each chunk's metadata (id, previous, next, type) from the
1156                //   database with a `SELECT`, and then use the hashmap to get the number of
1157                //   events.
1158                //
1159                // This strategy minimizes the number of queries to the database, and keeps them
1160                // super simple, while doing a bit more processing here, which is much faster.
1161
1162                let num_events_by_chunk_ids = txn
1163                    .prepare(
1164                        "SELECT ec.chunk_id, COUNT(ec.event_id) \
1165                        FROM event_chunks as ec \
1166                        WHERE ec.linked_chunk_id = ? \
1167                        GROUP BY ec.chunk_id",
1168                    )?
1169                    .query_map((&hashed_linked_chunk_id,), |row| {
1170                        Ok((row.get::<_, u64>(0)?, row.get::<_, usize>(1)?))
1171                    })?
1172                    .collect::<Result<HashMap<_, _>, _>>()?;
1173
1174                txn.prepare(
1175                    "SELECT \
1176                        lc.id, \
1177                        lc.previous, \
1178                        lc.next, \
1179                        lc.type \
1180                    FROM linked_chunks as lc \
1181                    WHERE lc.linked_chunk_id = ? \
1182                    ORDER BY lc.id",
1183                )?
1184                .query_map((&hashed_linked_chunk_id,), |row| {
1185                    Ok((
1186                        row.get::<_, u64>(0)?,
1187                        row.get::<_, Option<u64>>(1)?,
1188                        row.get::<_, Option<u64>>(2)?,
1189                        row.get::<_, String>(3)?,
1190                    ))
1191                })?
1192                .map(|data| -> Result<_> {
1193                    let (id, previous, next, chunk_type) = data?;
1194
1195                    // Note: since a gap has 0 events, an alternative could be to *not* retrieve
1196                    // the chunk type, and just let the hashmap lookup fail for gaps. However,
1197                    // benchmarking shows that this is slightly slower than matching the chunk
1198                    // type (around 1%, so in the realm of noise), so we keep the explicit
1199                    // check instead.
1200                    let num_items = if chunk_type == CHUNK_TYPE_GAP_TYPE_STRING {
1201                        0
1202                    } else {
1203                        num_events_by_chunk_ids.get(&id).copied().unwrap_or(0)
1204                    };
1205
1206                    Ok(ChunkMetadata {
1207                        identifier: ChunkIdentifier::new(id),
1208                        previous: previous.map(ChunkIdentifier::new),
1209                        next: next.map(ChunkIdentifier::new),
1210                        num_items,
1211                    })
1212                })
1213                .collect::<Result<Vec<_>, _>>()
1214            })
1215            .await
1216    }
1217
1218    #[instrument(skip(self))]
1219    async fn load_last_chunk(
1220        &self,
1221        linked_chunk_id: LinkedChunkId<'_>,
1222    ) -> Result<(Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator), Self::Error> {
1223        let _timer = timer!("method");
1224
1225        let hashed_linked_chunk_id =
1226            self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1227        let encryption = self.encryption.clone();
1228
1229        self
1230            .read()
1231            .await?
1232            .with_transaction(move |txn| -> Result<_> {
1233                // Find the latest chunk identifier to generate a `ChunkIdentifierGenerator`, and count the number of chunks.
1234                let (observed_max_identifier, number_of_chunks) = txn
1235                    .prepare(
1236                        "SELECT MAX(id), COUNT(*) FROM linked_chunks WHERE linked_chunk_id = ?"
1237                    )?
1238                    .query_row(
1239                        (&hashed_linked_chunk_id,),
1240                        |row| {
1241                            Ok((
1242                                // Read the `MAX(id)` as an `Option<u64>` instead
1243                                // of `u64` in case the `SELECT` returns nothing.
1244                                // Indeed, if it returns no line, the `MAX(id)` is
1245                                // set to `Null`.
1246                                row.get::<_, Option<u64>>(0)?,
1247                                row.get::<_, u64>(1)?,
1248                            ))
1249                        }
1250                    )?;
1251
1252                let chunk_identifier_generator = match observed_max_identifier {
1253                    Some(max_observed_identifier) => {
1254                        ChunkIdentifierGenerator::new_from_previous_chunk_identifier(
1255                            ChunkIdentifier::new(max_observed_identifier)
1256                        )
1257                    },
1258                    None => ChunkIdentifierGenerator::new_from_scratch(),
1259                };
1260
1261                // Find the last chunk.
1262                let Some((chunk_identifier, previous_chunk, chunk_type)) = txn
1263                    .prepare(
1264                        "SELECT id, previous, type FROM linked_chunks WHERE linked_chunk_id = ? AND next IS NULL"
1265                    )?
1266                    .query_row(
1267                        (&hashed_linked_chunk_id,),
1268                        |row| {
1269                            Ok((
1270                                row.get::<_, u64>(0)?,
1271                                row.get::<_, Option<u64>>(1)?,
1272                                row.get::<_, String>(2)?,
1273                            ))
1274                        }
1275                    )
1276                    .optional()?
1277                else {
1278                    // Chunk is not found and there are zero chunks for this room, this is consistent, all
1279                    // good.
1280                    if number_of_chunks == 0 {
1281                        return Ok((None, chunk_identifier_generator));
1282                    }
1283                    // Chunk is not found **but** there are chunks for this room, this is inconsistent. The
1284                    // linked chunk is malformed.
1285                    //
1286                    // Returning `Ok((None, _))` would be invalid here: we must return an error.
1287                    else {
1288                        return Err(Error::InvalidData {
1289                            details:
1290                                "last chunk is not found but chunks exist: the linked chunk contains a cycle"
1291                                    .to_owned()
1292                            }
1293                        )
1294                    }
1295                };
1296
1297                // Build the chunk.
1298                let last_chunk = txn.rebuild_chunk(
1299                    &encryption,
1300                    &hashed_linked_chunk_id,
1301                    previous_chunk,
1302                    chunk_identifier,
1303                    None,
1304                    &chunk_type
1305                )?;
1306
1307                Ok((Some(last_chunk), chunk_identifier_generator))
1308            })
1309            .await
1310    }
1311
1312    #[instrument(skip(self))]
1313    async fn load_previous_chunk(
1314        &self,
1315        linked_chunk_id: LinkedChunkId<'_>,
1316        before_chunk_identifier: ChunkIdentifier,
1317    ) -> Result<Option<RawChunk<Event, Gap>>, Self::Error> {
1318        let _timer = timer!("method");
1319
1320        let hashed_linked_chunk_id =
1321            self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1322        let encryption = self.encryption.clone();
1323
1324        self
1325            .read()
1326            .await?
1327            .with_transaction(move |txn| -> Result<_> {
1328                // Find the chunk before the chunk identified by `before_chunk_identifier`.
1329                let Some((chunk_identifier, previous_chunk, next_chunk, chunk_type)) = txn
1330                    .prepare(
1331                        "SELECT id, previous, next, type FROM linked_chunks WHERE linked_chunk_id = ? AND next = ?"
1332                    )?
1333                    .query_row(
1334                        (&hashed_linked_chunk_id, before_chunk_identifier.index()),
1335                        |row| {
1336                            Ok((
1337                                row.get::<_, u64>(0)?,
1338                                row.get::<_, Option<u64>>(1)?,
1339                                row.get::<_, Option<u64>>(2)?,
1340                                row.get::<_, String>(3)?,
1341                            ))
1342                        }
1343                    )
1344                    .optional()?
1345                else {
1346                    // Chunk is not found.
1347                    return Ok(None);
1348                };
1349
1350                // Build the chunk.
1351                let last_chunk = txn.rebuild_chunk(
1352                    &encryption,
1353                    &hashed_linked_chunk_id,
1354                    previous_chunk,
1355                    chunk_identifier,
1356                    next_chunk,
1357                    &chunk_type
1358                )?;
1359
1360                Ok(Some(last_chunk))
1361            })
1362            .await
1363    }
1364
1365    async fn remember_thread(
1366        &self,
1367        room_id: &RoomId,
1368        thread_id: &EventId,
1369    ) -> Result<(), Self::Error> {
1370        let hashed_linked_chunk_id = self
1371            .encryption
1372            .encode_linked_chunk(keys::LINKED_CHUNKS, &LinkedChunkId::Thread(room_id, thread_id));
1373        let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1374        let hashed_thread_id = self.encryption.encode_thread_id(thread_id)?;
1375
1376        self.write()
1377            .await?
1378            .with_transaction(move |txn| {
1379                txn.execute(
1380                    "INSERT OR IGNORE INTO threads VALUES (?, ?, ?)",
1381                    (hashed_linked_chunk_id, hashed_room_id, hashed_thread_id),
1382                )?;
1383
1384                Ok(())
1385            })
1386            .await
1387    }
1388
1389    #[instrument(skip(self))]
1390    async fn clear_all_events(&self, room_id: Option<&RoomId>) -> Result<(), Self::Error> {
1391        let _timer = timer!("method");
1392
1393        match room_id {
1394            // Clear all events.
1395            None => {
1396                self.write()
1397                    .await?
1398                    .with_transaction(move |txn| {
1399                        // Remove all the chunks, and let cascading do its job.
1400                        txn.execute("DELETE FROM linked_chunks", ())?;
1401
1402                        // Also clear all the events' contents, and let cascading do its job.
1403                        txn.execute("DELETE FROM events", ())?;
1404
1405                        Ok(())
1406                    })
1407                    .await
1408            }
1409
1410            // Clear events for specific room.
1411            Some(room_id) => {
1412                let encryption = self.encryption.clone();
1413                let room_id = room_id.to_owned();
1414
1415                self.write()
1416                    .await?
1417                    .with_transaction(move |txn| {
1418                        // Delete linked chunks for the room and pinned-events caches.
1419                        {
1420                            let mut delete =
1421                                txn.prepare("DELETE FROM linked_chunks WHERE linked_chunk_id = ?")?;
1422
1423                            for linked_chunk_id in
1424                                [LinkedChunkId::Room(&room_id), LinkedChunkId::PinnedEvents(&room_id)]
1425                            {
1426                                let linked_chunk_id = encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1427
1428                                // Remove all the chunks about the current `LinkedChunkId`, and let cascading
1429                                // do its job.
1430                                delete.execute((&linked_chunk_id,))?;
1431                            }
1432                        }
1433
1434                        let encoded_room_id = encryption.encode_room_id(keys::EVENTS, &room_id);
1435
1436                        // Delete linked chunks for the thread caches.
1437                        {
1438                            txn.execute(
1439                                "DELETE FROM linked_chunks WHERE linked_chunk_id IN (SELECT linked_chunk_id FROM threads WHERE room_id = ?)",
1440                                (&encoded_room_id,),
1441                            )?;
1442                        }
1443
1444                        // Also clear all the events' contents.
1445                        txn.execute(
1446                            "DELETE FROM events WHERE room_id = ?",
1447                            (encoded_room_id,),
1448                        )?;
1449
1450                        Ok(())
1451                    })
1452                    .await
1453            }
1454        }
1455    }
1456
1457    #[instrument(skip(self, event_ids))]
1458    async fn filter_duplicated_events(
1459        &self,
1460        linked_chunk_id: LinkedChunkId<'_>,
1461        event_ids: Vec<OwnedEventId>,
1462    ) -> Result<Vec<(OwnedEventId, Position)>, Self::Error> {
1463        let _timer = timer!("method");
1464
1465        // If there's no events for which we want to check duplicates, we can return
1466        // early. It's not only an optimization to do so: it's required, otherwise the
1467        // `repeat_vars` call below will panic.
1468        if event_ids.is_empty() {
1469            return Ok(Vec::new());
1470        }
1471
1472        // Select all events that exist in the store, i.e. the duplicates.
1473        let hashed_linked_chunk_id =
1474            self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &linked_chunk_id);
1475        let event_ids_and_hashed_event_ids = event_ids
1476            .into_iter()
1477            .map(|event_id| {
1478                let hashed_event_id = self.encryption.encode_event_id(
1479                    // For the event ID, we need a stable hash between the
1480                    // `events` and `event_chunks` tables. That's why we use
1481                    // `keys::EVENTS` even if `hashed_event_id` is only used in
1482                    // `events_chunks`.
1483                    keys::EVENTS,
1484                    &event_id,
1485                );
1486
1487                (event_id, hashed_event_id)
1488            })
1489            .collect::<Vec<_>>();
1490
1491        self.read()
1492            .await?
1493            .with_transaction(move |txn| -> Result<_> {
1494                txn.chunk_large_query_over(
1495                    event_ids_and_hashed_event_ids,
1496                    None,
1497                    move |txn, event_ids_and_hashed_event_ids| {
1498                        let query = format!(
1499                            "SELECT event_id, chunk_id, position \
1500                            FROM event_chunks \
1501                            WHERE linked_chunk_id = ? AND event_id IN ({}) \
1502                            ORDER BY chunk_id ASC, position ASC",
1503                            repeat_vars(event_ids_and_hashed_event_ids.len()),
1504                        );
1505
1506                        let parameters = params_from_iter(
1507                            // parameter for `linked_chunk_id = ?`
1508                            once(
1509                                hashed_linked_chunk_id
1510                                    .to_sql()
1511                                    // SAFETY: it cannot fail since `Key::to_sql` never fails
1512                                    .unwrap(),
1513                            )
1514                            // parameters for `event_id IN (…)`
1515                            .chain(
1516                                event_ids_and_hashed_event_ids.iter().map(
1517                                    |(_event_id, hashed_event_id)| {
1518                                        hashed_event_id
1519                                            .to_sql()
1520                                            // SAFETY: it cannot fail since `Vec::<u8>::to_sql`
1521                                            // never fails
1522                                            .unwrap()
1523                                    },
1524                                ),
1525                            ),
1526                        );
1527
1528                        let mut duplicated_events = Vec::new();
1529
1530                        for duplicated_event in
1531                            txn.prepare(&query)?.query_map(parameters, |row| {
1532                                Ok((
1533                                    row.get::<_, Vec<u8>>(0)?,
1534                                    row.get::<_, u64>(1)?,
1535                                    row.get::<_, usize>(2)?,
1536                                ))
1537                            })?
1538                        {
1539                            let (duplicated_hashed_event_id, chunk_identifier, index) =
1540                                duplicated_event?;
1541
1542                            // The event ID is encoded in the database. We can't decode it. However,
1543                            // we can find the original event ID with the `event_ids` parameter of
1544                            // this method by comparing the encoded event ID!
1545                            let Some(duplicated_event_id) = event_ids_and_hashed_event_ids
1546                                .iter()
1547                                .find_map(|(event_id, hashed_event_id)| {
1548                                    (hashed_event_id.deref() == duplicated_hashed_event_id)
1549                                        .then_some(event_id.clone())
1550                                })
1551                            else {
1552                                error!(
1553                                    "Unreachable: found a duplicated event that was not requested"
1554                                );
1555                                continue;
1556                            };
1557
1558                            duplicated_events.push((
1559                                duplicated_event_id,
1560                                Position::new(ChunkIdentifier::new(chunk_identifier), index),
1561                            ));
1562                        }
1563
1564                        Ok(duplicated_events)
1565                    },
1566                )
1567            })
1568            .await
1569    }
1570
1571    #[instrument(skip(self, event_id))]
1572    async fn find_event(
1573        &self,
1574        room_id: &RoomId,
1575        event_id: &EventId,
1576    ) -> Result<Option<Event>, Self::Error> {
1577        let _timer = timer!("method");
1578
1579        let encryption = self.encryption.clone();
1580
1581        let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1582        let hashed_event_id = self.encryption.encode_event_id(keys::EVENTS, event_id);
1583
1584        self.read()
1585            .await?
1586            .with_transaction(move |txn| -> Result<_> {
1587                let Some(event) = txn
1588                    .prepare("SELECT content FROM events WHERE event_id = ? AND room_id = ?")?
1589                    .query_row((hashed_event_id, hashed_room_id), |row| row.get::<_, Vec<u8>>(0))
1590                    .optional()?
1591                else {
1592                    // Event is not found.
1593                    return Ok(None);
1594                };
1595
1596                Ok(Some(encryption.decode_event(&event)?))
1597            })
1598            .await
1599    }
1600
1601    #[instrument(skip(self, event_id, filters))]
1602    async fn find_event_relations(
1603        &self,
1604        room_id: &RoomId,
1605        event_id: &EventId,
1606        filters: Option<&[RelationType]>,
1607    ) -> Result<Vec<(Event, Option<Position>)>, Self::Error> {
1608        let _timer = timer!("method");
1609
1610        let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1611        let hashed_linked_chunk_id =
1612            self.encryption.encode_linked_chunk(keys::LINKED_CHUNKS, &LinkedChunkId::Room(room_id));
1613        let hashed_event_id = self.encryption.encode_event_id(keys::EVENTS, event_id);
1614
1615        let filters = filters.map(ToOwned::to_owned);
1616        let encryption = self.encryption.clone();
1617
1618        self.read()
1619            .await?
1620            .with_transaction(move |txn| -> Result<_> {
1621                find_event_relations_transaction(
1622                    &encryption,
1623                    hashed_room_id,
1624                    hashed_linked_chunk_id,
1625                    hashed_event_id,
1626                    filters,
1627                    txn,
1628                )
1629            })
1630            .await
1631    }
1632
1633    #[instrument(skip(self))]
1634    async fn get_room_events(
1635        &self,
1636        room_id: &RoomId,
1637        event_type: Option<&str>,
1638        session_id: Option<&str>,
1639    ) -> Result<Vec<Event>, Self::Error> {
1640        let _timer = timer!("method");
1641
1642        let encryption = self.encryption.clone();
1643
1644        let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1645        let hashed_event_type = event_type.map(|e| self.encryption.encode_key(keys::EVENTS, e));
1646        let hashed_session_id = session_id.map(|s| self.encryption.encode_key(keys::EVENTS, s));
1647
1648        self.read()
1649            .await?
1650            .with_transaction(move |txn| -> Result<_> {
1651                // I'm not sure why clippy claims that the clones aren't required. The compiler
1652                // tells us that the lifetimes aren't long enough if we remove them. Doesn't matter
1653                // much so let's silence things.
1654                #[allow(clippy::redundant_clone)]
1655                let (query, keys) = match (hashed_event_type, hashed_session_id) {
1656                    (None, None) => {
1657                        ("SELECT content FROM events WHERE room_id = ?", params![hashed_room_id])
1658                    }
1659                    (None, Some(session_id)) => (
1660                        "SELECT content FROM events WHERE room_id = ?1 AND session_id = ?2",
1661                        params![hashed_room_id, session_id.to_owned()],
1662                    ),
1663                    (Some(event_type), None) => (
1664                        "SELECT content FROM events WHERE room_id = ? AND event_type = ?",
1665                        params![hashed_room_id, event_type.to_owned()]
1666                    ),
1667                    (Some(event_type), Some(session_id)) => (
1668                        "SELECT content FROM events WHERE room_id = ?1 AND event_type = ?2 AND session_id = ?3",
1669                        params![hashed_room_id, event_type.to_owned(), session_id.to_owned()],
1670                    ),
1671                };
1672
1673                let mut statement = txn.prepare(query)?;
1674
1675                statement
1676                    .query_map(keys, |row| row.get::<_, Vec<u8>>(0))?
1677                    .map(|maybe_encoded_event| {
1678                        encryption.decode_event(&maybe_encoded_event?)
1679                    })
1680                    .collect::<Result<Vec<_>>>()
1681            })
1682            .await
1683    }
1684
1685    #[instrument(skip(self, event))]
1686    async fn save_event(&self, room_id: &RoomId, event: Event) -> Result<(), Self::Error> {
1687        let _timer = timer!("method");
1688
1689        let Some(event_id) = event.event_id() else {
1690            error!("Trying to save an event with no ID");
1691            return Ok(());
1692        };
1693
1694        let Some(event_type) = event.kind.event_type() else {
1695            error!(%event_id, "Trying to save an event with no event type");
1696            return Ok(());
1697        };
1698
1699        let hashed_event_type = self.encryption.encode_key(keys::EVENTS, event_type);
1700        let hashed_session_id =
1701            event.kind.session_id().map(|s| self.encryption.encode_key(keys::EVENTS, s));
1702
1703        let hashed_room_id = self.encryption.encode_room_id(keys::EVENTS, room_id);
1704        let hashed_event_id = self.encryption.encode_event_id(keys::EVENTS, event_id);
1705        let encoded_event = self.encryption.encode_event(&event)?;
1706
1707        self.write()
1708            .await?
1709            .with_transaction(move |txn| -> Result<_> {
1710                txn.execute(
1711                    "INSERT OR REPLACE INTO events(room_id, event_id, event_type, session_id, content, relates_to, rel_type) VALUES (?, ?, ?, ?, ?, ?, ?)",
1712                    (
1713                        &hashed_room_id,
1714                        hashed_event_id,
1715                        hashed_event_type,
1716                        hashed_session_id,
1717                        encoded_event.content,
1718                        encoded_event.relates_to,
1719                        encoded_event.rel_type
1720                    )
1721                )?;
1722
1723                Ok(())
1724            })
1725            .await
1726    }
1727
1728    async fn close(&self) -> Result<(), Self::Error> {
1729        SqliteEventCacheStore::close(self).await
1730    }
1731
1732    async fn reopen(&self) -> Result<(), Self::Error> {
1733        SqliteEventCacheStore::reopen(self).await
1734    }
1735
1736    async fn optimize(&self) -> Result<(), Self::Error> {
1737        Ok(self.vacuum().await?)
1738    }
1739
1740    async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
1741        self.get_db_size().await
1742    }
1743}
1744
1745fn find_event_relations_transaction(
1746    encryption: &Encryption,
1747    hashed_room_id: Key,
1748    hashed_linked_chunk_id: Key,
1749    hashed_event_id: Key,
1750    filters: Option<Vec<RelationType>>,
1751    txn: &Transaction<'_>,
1752) -> Result<Vec<(Event, Option<Position>)>> {
1753    let get_rows = |row: &rusqlite::Row<'_>| {
1754        Ok((
1755            row.get::<_, Vec<u8>>(0)?,
1756            row.get::<_, Option<u64>>(1)?,
1757            row.get::<_, Option<usize>>(2)?,
1758        ))
1759    };
1760
1761    // Collect related events.
1762    let collect_results = |transaction| {
1763        let mut related = Vec::new();
1764
1765        for result in transaction {
1766            let (event, chunk_id, index): (Vec<u8>, Option<u64>, _) = result?;
1767            let event = encryption.decode_event(&event)?;
1768
1769            // Only build the position if both the chunk_id and position were present; in
1770            // theory, they should either be present at the same time, or not at all.
1771            let pos = chunk_id
1772                .zip(index)
1773                .map(|(chunk_id, index)| Position::new(ChunkIdentifier::new(chunk_id), index));
1774
1775            related.push((event, pos));
1776        }
1777
1778        Ok(related)
1779    };
1780
1781    if let Some(filters) = filters
1782        && filters.is_empty().not()
1783    {
1784        let query = format!(
1785            "SELECT events.content, event_chunks.chunk_id, event_chunks.position \
1786            FROM events \
1787            LEFT JOIN event_chunks ON events.event_id = event_chunks.event_id AND event_chunks.linked_chunk_id = ? \
1788            WHERE events.relates_to = ? AND events.room_id = ? AND events.rel_type IN ({})",
1789            repeat_vars(filters.len())
1790        );
1791
1792        // First the filters need to be stringified; because `.to_sql()` will borrow
1793        // from them, they also need to be stringified onto the stack, so as to
1794        // get a stable address (to avoid returning a temporary reference in the
1795        // map closure below).
1796        let filter_strings: Vec<_> = filters.iter().map(|f| f.to_string()).collect();
1797        let filters_params: Vec<_> = filter_strings
1798            .iter()
1799            .map(|f| f.to_sql().expect("converting a string to SQL should work"))
1800            .collect();
1801
1802        let parameters = params_from_iter(
1803            [
1804                hashed_linked_chunk_id.to_sql().expect(
1805                    "We should be able to convert a hashed linked chunk ID to a SQLite value",
1806                ),
1807                hashed_event_id
1808                    .to_sql()
1809                    .expect("We should be able to convert an event ID to a SQLite value"),
1810                hashed_room_id
1811                    .to_sql()
1812                    .expect("We should be able to convert a room ID to a SQLite value"),
1813            ]
1814            .into_iter()
1815            .chain(filters_params),
1816        );
1817
1818        let mut transaction = txn.prepare(&query)?;
1819        let transaction = transaction.query_map(parameters, get_rows)?;
1820
1821        collect_results(transaction)
1822    } else {
1823        let query = "SELECT events.content, event_chunks.chunk_id, event_chunks.position \
1824            FROM events \
1825            LEFT JOIN event_chunks ON events.event_id = event_chunks.event_id AND event_chunks.linked_chunk_id = ? \
1826            WHERE events.relates_to = ? AND events.room_id = ?";
1827        let parameters = (hashed_linked_chunk_id, hashed_event_id, hashed_room_id);
1828
1829        let mut transaction = txn.prepare(query)?;
1830        let transaction = transaction.query_map(parameters, get_rows)?;
1831
1832        collect_results(transaction)
1833    }
1834}
1835
1836/// Like `deadpool::managed::Object::with_transaction`, but starts the
1837/// transaction in immediate (write) mode from the beginning, precluding errors
1838/// of the kind SQLITE_BUSY from happening, for transactions that may involve
1839/// both reads and writes, and start with a write.
1840async fn with_immediate_transaction<
1841    T: Send + 'static,
1842    F: FnOnce(&Transaction<'_>) -> Result<T, Error> + Send + 'static,
1843>(
1844    this: &SqliteEventCacheStore,
1845    f: F,
1846) -> Result<T, Error> {
1847    this.write()
1848        .await?
1849        .interact(move |conn| -> Result<T, Error> {
1850            // Start the transaction in IMMEDIATE mode since all updates may cause writes,
1851            // to avoid read transactions upgrading to write mode and causing
1852            // SQLITE_BUSY errors. See also: https://www.sqlite.org/lang_transaction.html#deferred_immediate_and_exclusive_transactions
1853            conn.set_transaction_behavior(TransactionBehavior::Immediate);
1854
1855            let code = || -> Result<T, Error> {
1856                let txn = conn.transaction()?;
1857                let res = f(&txn)?;
1858                txn.commit()?;
1859                Ok(res)
1860            };
1861
1862            let res = code();
1863
1864            // Reset the transaction behavior to use Deferred, after this transaction has
1865            // been run, whether it was successful or not.
1866            conn.set_transaction_behavior(TransactionBehavior::Deferred);
1867
1868            res
1869        })
1870        .await
1871        // SAFETY: same logic as in [`deadpool::managed::Object::with_transaction`].`
1872        .unwrap()
1873}
1874
1875fn insert_chunk(
1876    txn: &Transaction<'_>,
1877    linked_chunk_id: &Key,
1878    previous: Option<u64>,
1879    new: u64,
1880    next: Option<u64>,
1881    type_str: &str,
1882) -> rusqlite::Result<()> {
1883    // First, insert the new chunk.
1884    txn.execute(
1885        r#"
1886            INSERT INTO linked_chunks(id, linked_chunk_id, previous, next, type)
1887            VALUES (?, ?, ?, ?, ?)
1888        "#,
1889        (new, linked_chunk_id, previous, next, type_str),
1890    )?;
1891
1892    // If this chunk has a previous one, update its `next` field.
1893    if let Some(previous) = previous {
1894        let updated = txn.execute(
1895            r#"
1896                UPDATE linked_chunks
1897                SET next = ?
1898                WHERE id = ? AND linked_chunk_id = ?
1899            "#,
1900            (new, previous, linked_chunk_id),
1901        )?;
1902        if updated < 1 {
1903            return Err(rusqlite::Error::QueryReturnedNoRows);
1904        }
1905        if updated > 1 {
1906            return Err(rusqlite::Error::QueryReturnedMoreThanOneRow);
1907        }
1908    }
1909
1910    // If this chunk has a next one, update its `previous` field.
1911    if let Some(next) = next {
1912        let updated = txn.execute(
1913            r#"
1914                UPDATE linked_chunks
1915                SET previous = ?
1916                WHERE id = ? AND linked_chunk_id = ?
1917            "#,
1918            (new, next, linked_chunk_id),
1919        )?;
1920        if updated < 1 {
1921            return Err(rusqlite::Error::QueryReturnedNoRows);
1922        }
1923        if updated > 1 {
1924            return Err(rusqlite::Error::QueryReturnedMoreThanOneRow);
1925        }
1926    }
1927
1928    Ok(())
1929}
1930
1931#[cfg(test)]
1932mod tests {
1933    use std::{
1934        path::PathBuf,
1935        sync::{
1936            LazyLock,
1937            atomic::{AtomicU32, Ordering::SeqCst},
1938        },
1939    };
1940
1941    use assert_matches::assert_matches;
1942    use matrix_sdk_base::{
1943        event_cache::store::{
1944            EventCacheStore, EventCacheStoreError, IntoEventCacheStore,
1945            integration_tests::EventCacheStoreIntegrationTests,
1946        },
1947        event_cache_store_integration_tests, event_cache_store_integration_tests_time,
1948        linked_chunk::{ChunkIdentifier, LinkedChunkId, Update},
1949    };
1950    use matrix_sdk_test::{DEFAULT_TEST_ROOM_ID, async_test};
1951    use ruma::{OwnedEventId, event_id};
1952    use tempfile::{TempDir, tempdir};
1953
1954    use super::{SqliteEventCacheStore, keys};
1955    use crate::{SqliteStoreConfig, utils::SqliteAsyncConnExt};
1956
1957    static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
1958    static NUM: AtomicU32 = AtomicU32::new(0);
1959
1960    fn new_event_cache_store_workspace() -> PathBuf {
1961        let name = NUM.fetch_add(1, SeqCst).to_string();
1962        TMP_DIR.path().join(name)
1963    }
1964
1965    async fn get_event_cache_store() -> Result<SqliteEventCacheStore, EventCacheStoreError> {
1966        let tmpdir_path = new_event_cache_store_workspace();
1967
1968        tracing::info!("using event cache store @ {}", tmpdir_path.to_str().unwrap());
1969
1970        Ok(SqliteEventCacheStore::open(tmpdir_path.to_str().unwrap(), None).await.unwrap())
1971    }
1972
1973    event_cache_store_integration_tests!();
1974    event_cache_store_integration_tests_time!();
1975
1976    #[async_test]
1977    async fn test_encryption_encode_decode_thread_id_roundtrip() {
1978        let store = get_event_cache_store().await.expect("creating cache store failed");
1979        let thread_id: OwnedEventId = event_id!("$event").to_owned();
1980
1981        let encoded_thread_id = store.encryption.encode_thread_id(&thread_id).unwrap();
1982        let decoded_thread_id: OwnedEventId =
1983            store.encryption.decode_thread_id(&encoded_thread_id).unwrap();
1984
1985        assert_eq!(thread_id, decoded_thread_id);
1986    }
1987
1988    #[async_test]
1989    async fn test_pool_size() {
1990        let tmpdir_path = new_event_cache_store_workspace();
1991        let store_open_config = SqliteStoreConfig::new(tmpdir_path).pool_max_size(42);
1992
1993        let store = SqliteEventCacheStore::open_with_config(&store_open_config).await.unwrap();
1994
1995        assert_eq!(store.pool_max_size().await, Some(42));
1996    }
1997
1998    #[async_test]
1999    async fn test_linked_chunk_remove_chunk() {
2000        let store = get_event_cache_store().await.expect("creating cache store failed");
2001
2002        // Run corresponding integration test
2003        store.clone().into_event_cache_store().test_linked_chunk_remove_chunk().await;
2004
2005        // Check that cascading worked. Yes, SQLite, I doubt you.
2006        let gaps = store
2007            .read()
2008            .await
2009            .unwrap()
2010            .with_transaction(|txn| -> rusqlite::Result<_> {
2011                let mut gaps = Vec::new();
2012                for data in txn
2013                    .prepare("SELECT chunk_id FROM gap_chunks ORDER BY chunk_id")?
2014                    .query_map((), |row| row.get::<_, u64>(0))?
2015                {
2016                    gaps.push(data?);
2017                }
2018                Ok(gaps)
2019            })
2020            .await
2021            .unwrap();
2022
2023        // Check that the gaps match those set up in the corresponding integration test
2024        // above
2025        assert_eq!(gaps, vec![42, 44]);
2026    }
2027
2028    #[async_test]
2029    async fn test_linked_chunk_remove_item() {
2030        let store = get_event_cache_store().await.expect("creating cache store failed");
2031
2032        // Run corresponding integration test
2033        store.clone().into_event_cache_store().test_linked_chunk_remove_item().await;
2034
2035        let room_id = *DEFAULT_TEST_ROOM_ID;
2036        let hashed_linked_chunk_id = store
2037            .encryption
2038            .encode_linked_chunk(keys::LINKED_CHUNKS, &LinkedChunkId::Room(room_id));
2039
2040        // Make sure the position have been updated for the remaining events.
2041        let num_rows: u64 = store
2042            .read()
2043            .await
2044            .unwrap()
2045            .with_transaction(move |txn| {
2046                txn.query_row(
2047                    "SELECT COUNT(*) FROM event_chunks WHERE chunk_id = 42 AND linked_chunk_id = ? AND position IN (2, 3, 4)",
2048                    (hashed_linked_chunk_id,),
2049                    |row| row.get(0),
2050                )
2051            })
2052            .await
2053            .unwrap();
2054        assert_eq!(num_rows, 3);
2055    }
2056
2057    #[async_test]
2058    async fn test_linked_chunk_clear() {
2059        let store = get_event_cache_store().await.expect("creating cache store failed");
2060
2061        // Run corresponding integration test
2062        store.clone().into_event_cache_store().test_linked_chunk_clear().await;
2063
2064        // Check that cascading worked. Yes, SQLite, I doubt you.
2065        store
2066            .read()
2067            .await
2068            .unwrap()
2069            .with_transaction(|txn| -> rusqlite::Result<_> {
2070                let num_gaps = txn
2071                    .prepare("SELECT COUNT(chunk_id) FROM gap_chunks ORDER BY chunk_id")?
2072                    .query_row((), |row| row.get::<_, u64>(0))?;
2073                assert_eq!(num_gaps, 0);
2074
2075                let num_events = txn
2076                    .prepare("SELECT COUNT(event_id) FROM event_chunks ORDER BY chunk_id")?
2077                    .query_row((), |row| row.get::<_, u64>(0))?;
2078                assert_eq!(num_events, 0);
2079
2080                Ok(())
2081            })
2082            .await
2083            .unwrap();
2084    }
2085
2086    #[async_test]
2087    async fn test_linked_chunk_update_is_a_transaction() {
2088        let store = get_event_cache_store().await.expect("creating cache store failed");
2089
2090        let room_id = *DEFAULT_TEST_ROOM_ID;
2091        let linked_chunk_id = LinkedChunkId::Room(room_id);
2092
2093        // Trigger a violation of the unique constraint on the (room id, chunk id)
2094        // couple.
2095        let err = store
2096            .handle_linked_chunk_updates(
2097                linked_chunk_id,
2098                vec![
2099                    Update::NewItemsChunk {
2100                        previous: None,
2101                        new: ChunkIdentifier::new(42),
2102                        next: None,
2103                    },
2104                    Update::NewItemsChunk {
2105                        previous: None,
2106                        new: ChunkIdentifier::new(42),
2107                        next: None,
2108                    },
2109                ],
2110            )
2111            .await
2112            .unwrap_err();
2113
2114        // The operation fails with a constraint violation error.
2115        assert_matches!(err, crate::error::Error::Sqlite(err) => {
2116            assert_matches!(err.sqlite_error_code(), Some(rusqlite::ErrorCode::ConstraintViolation));
2117        });
2118
2119        // If the updates have been handled transactionally, then no new chunks should
2120        // have been added; failure of the second update leads to the first one being
2121        // rolled back.
2122        let chunks = store.load_all_chunks(linked_chunk_id).await.unwrap();
2123        assert!(chunks.is_empty());
2124    }
2125}
2126
2127#[cfg(test)]
2128mod encrypted_tests {
2129    use std::sync::{
2130        LazyLock,
2131        atomic::{AtomicU32, Ordering::SeqCst},
2132    };
2133
2134    use matrix_sdk_base::{
2135        event_cache::store::{EventCacheStore, EventCacheStoreError},
2136        event_cache_store_integration_tests, event_cache_store_integration_tests_time,
2137    };
2138    use matrix_sdk_test::{async_test, event_factory::EventFactory};
2139    use ruma::{
2140        event_id,
2141        events::{relation::RelationType, room::message::RoomMessageEventContentWithoutRelation},
2142        room_id, user_id,
2143    };
2144    use tempfile::{TempDir, tempdir};
2145
2146    use super::SqliteEventCacheStore;
2147
2148    static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
2149    static NUM: AtomicU32 = AtomicU32::new(0);
2150
2151    async fn get_event_cache_store() -> Result<SqliteEventCacheStore, EventCacheStoreError> {
2152        let name = NUM.fetch_add(1, SeqCst).to_string();
2153        let tmpdir_path = TMP_DIR.path().join(name);
2154
2155        tracing::info!("using event cache store @ {}", tmpdir_path.to_str().unwrap());
2156
2157        Ok(SqliteEventCacheStore::open(
2158            tmpdir_path.to_str().unwrap(),
2159            Some("default_test_password"),
2160        )
2161        .await
2162        .unwrap())
2163    }
2164
2165    event_cache_store_integration_tests!();
2166    event_cache_store_integration_tests_time!();
2167
2168    #[async_test]
2169    async fn test_no_sqlite_injection_in_find_event_relations() {
2170        let room_id = room_id!("!test:localhost");
2171        let another_room_id = room_id!("!r1:matrix.org");
2172        let sender = user_id!("@alice:localhost");
2173
2174        let store = get_event_cache_store()
2175            .await
2176            .expect("We should be able to create a new, empty, event cache store");
2177
2178        let f = EventFactory::new().room(room_id).sender(sender);
2179
2180        // Create an event for the first room.
2181        let event_id = event_id!("$DO_NOT_FIND_ME:matrix.org");
2182        let event = f.text_msg("DO NOT FIND").event_id(event_id).into_event();
2183
2184        // Create a related event.
2185        let edit_id = event_id!("$find_me:matrix.org");
2186        let edit = f
2187            .text_msg("Find me")
2188            .event_id(edit_id)
2189            .edit(event_id, RoomMessageEventContentWithoutRelation::text_plain("jebote"))
2190            .into_event();
2191
2192        // Create an event for the second room.
2193        let f = f.room(another_room_id);
2194
2195        let another_event_id = event_id!("$DO_NOT_FIND_ME_EITHER:matrix.org");
2196        let another_event =
2197            f.text_msg("DO NOT FIND ME EITHER").event_id(another_event_id).into_event();
2198
2199        // Save the events in the DB.
2200        store.save_event(room_id, event).await.unwrap();
2201        store.save_event(room_id, edit).await.unwrap();
2202        store.save_event(another_room_id, another_event).await.unwrap();
2203
2204        // Craft a `RelationType` that will inject some SQL to be executed. The
2205        // `OR 1=1` ensures that all the previous parameters, the room
2206        // ID and event ID are ignored.
2207        let filter = Some(vec![RelationType::Replacement, "x\") OR 1=1; --".into()]);
2208
2209        // Attempt to find events in the first room.
2210        let results = store
2211            .find_event_relations(room_id, event_id, filter.as_deref())
2212            .await
2213            .expect("We should be able to attempt to find event relations");
2214
2215        // Ensure that we only got the single related event the first room contains.
2216        similar_asserts::assert_eq!(
2217            results.len(),
2218            1,
2219            "We should only have loaded events for the first room {results:#?}"
2220        );
2221
2222        // The event needs to be the edit event, otherwise something is wrong.
2223        let (found_event, _) = &results[0];
2224        assert_eq!(
2225            found_event.event_id(),
2226            Some(edit_id),
2227            "The single event we found should be the edit event"
2228        );
2229    }
2230}
2231
2232#[cfg(test)]
2233mod close_reopen_tests {
2234    use std::sync::{
2235        LazyLock,
2236        atomic::{AtomicU32, Ordering::SeqCst},
2237    };
2238
2239    use matrix_sdk_base::{event_cache::store::EventCacheStore, linked_chunk::LinkedChunkId};
2240    use matrix_sdk_test::{DEFAULT_TEST_ROOM_ID, async_test};
2241    use tempfile::{TempDir, tempdir};
2242
2243    use super::SqliteEventCacheStore;
2244
2245    static TMP_DIR: LazyLock<TempDir> = LazyLock::new(|| tempdir().unwrap());
2246    static NUM: AtomicU32 = AtomicU32::new(0);
2247
2248    async fn new_store() -> SqliteEventCacheStore {
2249        let name = NUM.fetch_add(1, SeqCst).to_string();
2250        let tmpdir_path = TMP_DIR.path().join(name);
2251        SqliteEventCacheStore::open(tmpdir_path, None).await.unwrap()
2252    }
2253
2254    #[async_test]
2255    async fn test_close_completes_without_timeout() {
2256        let store = new_store().await;
2257
2258        // Close should complete quickly without hitting the 5s timeout.
2259        let start = std::time::Instant::now();
2260        store.close().await.unwrap();
2261        let elapsed = start.elapsed();
2262
2263        assert!(
2264            elapsed < std::time::Duration::from_secs(2),
2265            "close() took {elapsed:?}, expected < 2s (no timeout)"
2266        );
2267
2268        // Connections should be None after close.
2269        let guard = store.connections.lock().await;
2270        assert!(guard.is_none(), "connections should be None after close");
2271    }
2272
2273    #[async_test]
2274    async fn test_reopen_restores_connections() {
2275        let store = new_store().await;
2276
2277        store.close().await.unwrap();
2278
2279        {
2280            let guard = store.connections.lock().await;
2281            assert!(guard.is_none());
2282        }
2283
2284        store.reopen().await.unwrap();
2285
2286        {
2287            let guard = store.connections.lock().await;
2288            assert!(guard.is_some(), "connections should be Some after reopen");
2289        }
2290    }
2291
2292    #[async_test]
2293    async fn test_close_is_idempotent() {
2294        let store = new_store().await;
2295
2296        store.close().await.unwrap();
2297        // Second close should be a no-op.
2298        store.close().await.unwrap();
2299
2300        let guard = store.connections.lock().await;
2301        assert!(guard.is_none());
2302    }
2303
2304    #[async_test]
2305    async fn test_reopen_is_idempotent() {
2306        let store = new_store().await;
2307
2308        // Reopen on an active store should be a no-op.
2309        store.reopen().await.unwrap();
2310
2311        let guard = store.connections.lock().await;
2312        assert!(guard.is_some());
2313    }
2314
2315    #[async_test]
2316    async fn test_read_fails_when_closed() {
2317        let store = new_store().await;
2318        store.close().await.unwrap();
2319
2320        let err = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2321        assert!(err.is_err(), "read should fail when closed");
2322
2323        let err_msg = err.unwrap_err().to_string();
2324        assert!(err_msg.contains("closed"), "error should mention 'closed', got: {err_msg}");
2325    }
2326
2327    #[async_test]
2328    async fn test_write_fails_when_closed() {
2329        let store = new_store().await;
2330        store.close().await.unwrap();
2331
2332        let err = store.try_take_leased_lock(1000, "test_lock", "holder").await;
2333        assert!(err.is_err(), "write should fail when closed");
2334
2335        let err_msg = err.unwrap_err().to_string();
2336        assert!(err_msg.contains("closed"), "error should mention 'closed', got: {err_msg}");
2337    }
2338
2339    #[async_test]
2340    async fn test_data_persists_across_close_reopen() {
2341        let store = new_store().await;
2342
2343        // Take a lease lock — this is persisted in the database.
2344        let result = store.try_take_leased_lock(60_000, "test_lock", "holder").await.unwrap();
2345        assert!(result.is_some(), "should have acquired the lock");
2346
2347        // Close and reopen.
2348        store.close().await.unwrap();
2349        store.reopen().await.unwrap();
2350
2351        // The lock should still be held by the original holder after reopen.
2352        let result = store.try_take_leased_lock(60_000, "test_lock", "other_holder").await.unwrap();
2353        assert!(result.is_none(), "lock should still be held by the original holder after reopen");
2354    }
2355
2356    #[async_test]
2357    async fn test_multiple_close_reopen_cycles() {
2358        let store = new_store().await;
2359
2360        for _ in 0..5 {
2361            store.close().await.unwrap();
2362            store.reopen().await.unwrap();
2363
2364            // After each cycle, the store should be fully operational.
2365            let result = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2366            assert!(result.is_ok(), "store should work after close/reopen cycle");
2367        }
2368    }
2369
2370    #[async_test]
2371    async fn test_pool_is_fully_drained_after_close() {
2372        let store = new_store().await;
2373
2374        // Do a few reads to exercise the pool.
2375        let _ = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2376        let _ = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2377
2378        store.close().await.unwrap();
2379
2380        // After close, the connections field should be None (pool and write
2381        // connection have been fully torn down).
2382        let guard = store.connections.lock().await;
2383        assert!(guard.is_none(), "all connections should be released after close");
2384    }
2385
2386    #[async_test]
2387    async fn test_operations_work_immediately_after_reopen() {
2388        let store = new_store().await;
2389
2390        store.close().await.unwrap();
2391        store.reopen().await.unwrap();
2392
2393        // Read should work immediately after reopen.
2394        let result = store.load_all_chunks(LinkedChunkId::Room(*DEFAULT_TEST_ROOM_ID)).await;
2395        assert!(result.is_ok(), "read should succeed immediately after reopen");
2396
2397        // Write should work immediately after reopen.
2398        let result = store.try_take_leased_lock(1000, "test_lock", "holder").await;
2399        assert!(result.is_ok(), "write should succeed immediately after reopen");
2400    }
2401
2402    #[async_test]
2403    async fn test_close_waits_for_held_read_connection_to_drain() {
2404        let store = new_store().await;
2405
2406        // Acquire a read connection and hold it, simulating an in-flight read.
2407        let held_conn = store.read().await.unwrap();
2408
2409        // Spawn close in a background task — it will close the pool and then
2410        // poll-wait for pool.status().size == 0 in the drain loop.
2411        let store_clone = store.clone();
2412        let close_handle = tokio::spawn(async move {
2413            store_clone.close().await.unwrap();
2414        });
2415
2416        // Give close() a moment to close the pool and enter the drain loop.
2417        tokio::time::sleep(std::time::Duration::from_millis(200)).await;
2418
2419        // The close task should still be running because we hold a connection.
2420        assert!(!close_handle.is_finished(), "close should be waiting for the held connection");
2421
2422        // Release the held connection — this lets pool.status().size drop to 0.
2423        drop(held_conn);
2424
2425        // Now close should complete promptly (well within the 5s timeout).
2426        let timeout = tokio::time::timeout(std::time::Duration::from_secs(3), close_handle).await;
2427        assert!(timeout.is_ok(), "close should complete after the held connection is released");
2428        timeout.unwrap().unwrap();
2429
2430        // Verify the store is fully closed.
2431        let guard = store.connections.lock().await;
2432        assert!(guard.is_none(), "connections should be None after close");
2433    }
2434}