matrix_sdk_sqlite/
lib.rs

1// Copyright 2022 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#![cfg_attr(
15    not(any(feature = "state-store", feature = "crypto-store", feature = "event-cache")),
16    allow(dead_code, unused_imports)
17)]
18
19#[cfg(feature = "crypto-store")]
20mod crypto_store;
21mod error;
22#[cfg(feature = "event-cache")]
23mod event_cache_store;
24#[cfg(feature = "event-cache")]
25mod media_store;
26#[cfg(feature = "state-store")]
27mod state_store;
28mod utils;
29use std::{
30    cmp::max,
31    fmt,
32    path::{Path, PathBuf},
33};
34
35use deadpool_sqlite::PoolConfig;
36use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
37
38#[cfg(feature = "crypto-store")]
39pub use self::crypto_store::SqliteCryptoStore;
40pub use self::error::OpenStoreError;
41#[cfg(feature = "event-cache")]
42pub use self::event_cache_store::SqliteEventCacheStore;
43#[cfg(feature = "event-cache")]
44pub use self::media_store::SqliteMediaStore;
45#[cfg(feature = "state-store")]
46pub use self::state_store::{SqliteStateStore, DATABASE_NAME as STATE_STORE_DATABASE_NAME};
47
48#[cfg(test)]
49matrix_sdk_test_utils::init_tracing_for_tests!();
50
51/// An enum used to store the secret that gives access to a store
52#[derive(Clone, Debug, PartialEq, Zeroize, ZeroizeOnDrop)]
53pub enum Secret {
54    // Cryptographic key used to open the store
55    Key(Box<[u8; 32]>),
56    // Passphrase used to open the store
57    PassPhrase(Zeroizing<String>),
58}
59
60/// A configuration structure used for opening a store.
61#[derive(Clone)]
62pub struct SqliteStoreConfig {
63    /// Path to the database, without the file name.
64    path: PathBuf,
65    /// Secret to open the store, if any
66    secret: Option<Secret>,
67    /// The pool configuration for [`deadpool_sqlite`].
68    pool_config: PoolConfig,
69    /// The runtime configuration to apply when opening an SQLite connection.
70    runtime_config: RuntimeConfig,
71}
72
73impl fmt::Debug for SqliteStoreConfig {
74    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
75        formatter
76            .debug_struct("SqliteStoreConfig")
77            .field("path", &self.path)
78            .field("pool_config", &self.pool_config)
79            .field("runtime_config", &self.runtime_config)
80            .finish_non_exhaustive()
81    }
82}
83
84/// The minimum size of the connections pool.
85///
86/// We need at least 2 connections: one connection for write operations, and one
87/// connection for read operations.
88const POOL_MINIMUM_SIZE: usize = 2;
89
90impl SqliteStoreConfig {
91    /// Create a new [`SqliteStoreConfig`] with a path representing the
92    /// directory containing the store database.
93    pub fn new<P>(path: P) -> Self
94    where
95        P: AsRef<Path>,
96    {
97        Self {
98            path: path.as_ref().to_path_buf(),
99            pool_config: PoolConfig::new(max(POOL_MINIMUM_SIZE, num_cpus::get_physical() * 4)),
100            runtime_config: RuntimeConfig::default(),
101            secret: None,
102        }
103    }
104
105    /// Similar to [`SqliteStoreConfig::new`], but with defaults tailored for a
106    /// low memory usage environment.
107    ///
108    /// The following defaults are set:
109    ///
110    /// * The `pool_max_size` is set to the number of physical CPU, so one
111    ///   connection per physical thread,
112    /// * The `cache_size` is set to 500Kib,
113    /// * The `journal_size_limit` is set to 2Mib.
114    pub fn with_low_memory_config<P>(path: P) -> Self
115    where
116        P: AsRef<Path>,
117    {
118        Self::new(path)
119            // Maximum one connection per physical thread.
120            .pool_max_size(num_cpus::get_physical())
121            // Cache size is 500Kib.
122            .cache_size(500_000)
123            // Journal size limit is 2Mib.
124            .journal_size_limit(2_000_000)
125    }
126
127    /// Override the path.
128    pub fn path<P>(mut self, path: P) -> Self
129    where
130        P: AsRef<Path>,
131    {
132        self.path = path.as_ref().to_path_buf();
133        self
134    }
135
136    /// Define the passphrase if the store is encoded.
137    pub fn passphrase(mut self, passphrase: Option<&str>) -> Self {
138        self.secret =
139            passphrase.map(|passphrase| Secret::PassPhrase(Zeroizing::new(passphrase.to_owned())));
140        self
141    }
142
143    /// Define the key if the store is encoded.
144    pub fn key(mut self, key: Option<&[u8; 32]>) -> Self {
145        self.secret = key.map(|key| Secret::Key(Box::new(*key)));
146        self
147    }
148
149    /// Define the maximum pool size for [`deadpool_sqlite`].
150    ///
151    /// See [`deadpool_sqlite::PoolConfig::max_size`] to learn more.
152    pub fn pool_max_size(mut self, max_size: usize) -> Self {
153        self.pool_config.max_size = max(POOL_MINIMUM_SIZE, max_size);
154        self
155    }
156
157    /// Optimize the database.
158    ///
159    /// The SQLite documentation recommends to run this regularly and after any
160    /// schema change. The easiest is to do it consistently when the store is
161    /// constructed, after eventual migrations.
162    ///
163    /// See [`PRAGMA optimize`] to learn more.
164    ///
165    /// The default value is `true`.
166    ///
167    /// [`PRAGMA optimize`]: https://www.sqlite.org/pragma.html#pragma_optimize
168    pub fn optimize(mut self, optimize: bool) -> Self {
169        self.runtime_config.optimize = optimize;
170        self
171    }
172
173    /// Define the maximum size in **bytes** the SQLite cache can use.
174    ///
175    /// See [`PRAGMA cache_size`] to learn more.
176    ///
177    /// The default value is 2Mib.
178    ///
179    /// [`PRAGMA cache_size`]: https://www.sqlite.org/pragma.html#pragma_cache_size
180    pub fn cache_size(mut self, cache_size: u32) -> Self {
181        self.runtime_config.cache_size = cache_size;
182        self
183    }
184
185    /// Limit the size of the WAL file, in **bytes**.
186    ///
187    /// By default, while the DB connections of the databases are open, [the
188    /// size of the WAL file can keep increasing][size_wal_file] depending on
189    /// the size needed for the transactions. A critical case is `VACUUM`
190    /// which basically writes the content of the DB file to the WAL file
191    /// before writing it back to the DB file, so we end up taking twice the
192    /// size of the database.
193    ///
194    /// By setting this limit, the WAL file is truncated after its content is
195    /// written to the database, if it is bigger than the limit.
196    ///
197    /// See [`PRAGMA journal_size_limit`] to learn more. The value `limit`
198    /// corresponds to `N` in `PRAGMA journal_size_limit = N`.
199    ///
200    /// The default value is 10Mib.
201    ///
202    /// [size_wal_file]: https://www.sqlite.org/wal.html#avoiding_excessively_large_wal_files
203    /// [`PRAGMA journal_size_limit`]: https://www.sqlite.org/pragma.html#pragma_journal_size_limit
204    pub fn journal_size_limit(mut self, limit: u32) -> Self {
205        self.runtime_config.journal_size_limit = limit;
206        self
207    }
208}
209
210/// This type represents values to set at runtime when a database is opened.
211///
212/// This configuration is applied by
213/// [`utils::SqliteAsyncConnExt::apply_runtime_config`].
214#[derive(Clone, Debug)]
215struct RuntimeConfig {
216    /// If `true`, [`utils::SqliteAsyncConnExt::optimize`] will be called.
217    optimize: bool,
218
219    /// Regardless of the value, [`utils::SqliteAsyncConnExt::cache_size`] will
220    /// always be called with this value.
221    cache_size: u32,
222
223    /// Regardless of the value,
224    /// [`utils::SqliteAsyncConnExt::journal_size_limit`] will always be called
225    /// with this value.
226    journal_size_limit: u32,
227}
228
229impl Default for RuntimeConfig {
230    fn default() -> Self {
231        Self {
232            // Optimize is always applied.
233            optimize: true,
234            // A cache of 2Mib.
235            cache_size: 2_000_000,
236            // A limit of 10Mib.
237            journal_size_limit: 10_000_000,
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use std::{
245        ops::Not,
246        path::{Path, PathBuf},
247    };
248
249    use super::{Secret, SqliteStoreConfig, POOL_MINIMUM_SIZE};
250
251    #[test]
252    fn test_new() {
253        let store_config = SqliteStoreConfig::new(Path::new("foo"));
254
255        assert_eq!(store_config.pool_config.max_size, num_cpus::get_physical() * 4);
256        assert!(store_config.runtime_config.optimize);
257        assert_eq!(store_config.runtime_config.cache_size, 2_000_000);
258        assert_eq!(store_config.runtime_config.journal_size_limit, 10_000_000);
259    }
260
261    #[test]
262    fn test_with_low_memory_config() {
263        let store_config = SqliteStoreConfig::with_low_memory_config(Path::new("foo"));
264
265        assert_eq!(store_config.pool_config.max_size, num_cpus::get_physical());
266        assert!(store_config.runtime_config.optimize);
267        assert_eq!(store_config.runtime_config.cache_size, 500_000);
268        assert_eq!(store_config.runtime_config.journal_size_limit, 2_000_000);
269    }
270
271    #[test]
272    fn test_store_config_when_passphrase() {
273        let store_config = SqliteStoreConfig::new(Path::new("foo"))
274            .passphrase(Some("bar"))
275            .pool_max_size(42)
276            .optimize(false)
277            .cache_size(43)
278            .journal_size_limit(44);
279
280        assert_eq!(store_config.path, PathBuf::from("foo"));
281        assert_eq!(store_config.secret, Some(Secret::PassPhrase("bar".to_owned().into())));
282        assert_eq!(store_config.pool_config.max_size, 42);
283        assert!(store_config.runtime_config.optimize.not());
284        assert_eq!(store_config.runtime_config.cache_size, 43);
285        assert_eq!(store_config.runtime_config.journal_size_limit, 44);
286    }
287
288    #[test]
289    fn test_store_config_when_key() {
290        let store_config = SqliteStoreConfig::new(Path::new("foo"))
291            .key(Some(&[
292                143, 27, 202, 78, 96, 55, 13, 149, 247, 8, 33, 120, 204, 92, 171, 66, 19, 238, 61,
293                107, 132, 211, 40, 244, 71, 190, 99, 14, 173, 225, 6, 156,
294            ]))
295            .pool_max_size(42)
296            .optimize(false)
297            .cache_size(43)
298            .journal_size_limit(44);
299
300        assert_eq!(store_config.path, PathBuf::from("foo"));
301        assert_eq!(
302            store_config.secret,
303            Some(Secret::Key(Box::new([
304                143, 27, 202, 78, 96, 55, 13, 149, 247, 8, 33, 120, 204, 92, 171, 66, 19, 238, 61,
305                107, 132, 211, 40, 244, 71, 190, 99, 14, 173, 225, 6, 156,
306            ])))
307        );
308        assert_eq!(store_config.pool_config.max_size, 42);
309        assert!(store_config.runtime_config.optimize.not());
310        assert_eq!(store_config.runtime_config.cache_size, 43);
311        assert_eq!(store_config.runtime_config.journal_size_limit, 44);
312    }
313
314    #[test]
315    fn test_store_config_path() {
316        let store_config = SqliteStoreConfig::new(Path::new("foo")).path(Path::new("bar"));
317
318        assert_eq!(store_config.path, PathBuf::from("bar"));
319    }
320
321    #[test]
322    fn test_pool_size_has_a_minimum() {
323        let store_config = SqliteStoreConfig::new(Path::new("foo")).pool_max_size(1);
324
325        assert_eq!(store_config.pool_config.max_size, POOL_MINIMUM_SIZE);
326    }
327}