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