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}