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