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