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