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