Skip to main content

toasty_driver_sqlite/
pragma.rs

1//! Per-connection `PRAGMA` configuration.
2
3use std::{borrow::Cow, fmt};
4
5use indexmap::IndexMap;
6use rusqlite::{Connection as RusqliteConnection, ffi};
7use toasty_core::{
8    Result,
9    driver::{ExecResponse, QueryLogConfig, log::QueryLog},
10    stmt,
11};
12
13/// The [journal mode](https://www.sqlite.org/pragma.html#pragma_journal_mode)
14/// set by [`Sqlite::journal_mode`](crate::Sqlite::journal_mode).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum JournalMode {
17    /// Delete the rollback journal at the end of each transaction.
18    Delete,
19    /// Truncate the rollback journal to zero length instead of deleting it.
20    Truncate,
21    /// Overwrite the rollback journal header instead of deleting the file.
22    Persist,
23    /// Keep the rollback journal in memory.
24    Memory,
25    /// Use a [write-ahead log](https://www.sqlite.org/wal.html).
26    Wal,
27    /// Disable the rollback journal.
28    Off,
29}
30
31impl JournalMode {
32    fn as_str(self) -> &'static str {
33        match self {
34            Self::Delete => "DELETE",
35            Self::Truncate => "TRUNCATE",
36            Self::Persist => "PERSIST",
37            Self::Memory => "MEMORY",
38            Self::Wal => "WAL",
39            Self::Off => "OFF",
40        }
41    }
42}
43
44/// The [synchronous](https://www.sqlite.org/pragma.html#pragma_synchronous)
45/// setting set by [`Sqlite::synchronous`](crate::Sqlite::synchronous).
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum Synchronous {
48    /// Hand data to the operating system without syncing.
49    Off,
50    /// Sync at the most critical moments. Durable in WAL mode except across
51    /// power loss.
52    Normal,
53    /// Sync before each transaction commits.
54    Full,
55    /// Like `Full`, and also sync the directory after deleting a rollback
56    /// journal.
57    Extra,
58}
59
60impl Synchronous {
61    fn as_str(self) -> &'static str {
62        match self {
63            Self::Off => "OFF",
64            Self::Normal => "NORMAL",
65            Self::Full => "FULL",
66            Self::Extra => "EXTRA",
67        }
68    }
69}
70
71/// The [locking mode](https://www.sqlite.org/pragma.html#pragma_locking_mode)
72/// set by [`Sqlite::locking_mode`](crate::Sqlite::locking_mode).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum LockingMode {
75    /// Release file locks at the end of each transaction.
76    Normal,
77    /// Hold file locks until the connection closes.
78    Exclusive,
79}
80
81impl LockingMode {
82    fn as_str(self) -> &'static str {
83        match self {
84            Self::Normal => "NORMAL",
85            Self::Exclusive => "EXCLUSIVE",
86        }
87    }
88}
89
90/// The [auto-vacuum](https://www.sqlite.org/pragma.html#pragma_auto_vacuum)
91/// setting set by [`Sqlite::auto_vacuum`](crate::Sqlite::auto_vacuum).
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum AutoVacuum {
94    /// Never shrink the database file automatically.
95    None,
96    /// Shrink the database file at every commit.
97    Full,
98    /// Shrink the database file only on `PRAGMA incremental_vacuum`.
99    Incremental,
100}
101
102impl AutoVacuum {
103    fn as_str(self) -> &'static str {
104        match self {
105            Self::None => "NONE",
106            Self::Full => "FULL",
107            Self::Incremental => "INCREMENTAL",
108        }
109    }
110}
111
112impl From<JournalMode> for Cow<'static, str> {
113    fn from(mode: JournalMode) -> Self {
114        Cow::Borrowed(mode.as_str())
115    }
116}
117
118impl From<Synchronous> for Cow<'static, str> {
119    fn from(synchronous: Synchronous) -> Self {
120        Cow::Borrowed(synchronous.as_str())
121    }
122}
123
124impl From<LockingMode> for Cow<'static, str> {
125    fn from(mode: LockingMode) -> Self {
126        Cow::Borrowed(mode.as_str())
127    }
128}
129
130impl From<AutoVacuum> for Cow<'static, str> {
131    fn from(auto_vacuum: AutoVacuum) -> Self {
132        Cow::Borrowed(auto_vacuum.as_str())
133    }
134}
135
136/// Pragmas whose value must not reach the query log, error text, or
137/// `Debug` output.
138const SECRET: &[&str] = &["key", "rekey", "hexkey", "hexrekey", "cipher_salt"];
139
140/// The pragmas a driver applies to each connection, in application order.
141///
142/// The map starts with an empty slot for each pragma whose position
143/// matters. `IndexMap::insert` keeps an existing key's position, so setting
144/// one fills its slot rather than appending. Pragmas without a slot are
145/// appended and run after every slotted one, in the order first set.
146#[derive(Clone)]
147pub(crate) struct Pragmas {
148    entries: IndexMap<Cow<'static, str>, Option<Cow<'static, str>>>,
149}
150
151impl Default for Pragmas {
152    fn default() -> Self {
153        let slots = [
154            // SQLCipher requires `key` before any other statement, and the
155            // remaining cipher settings before any read of the database.
156            "key",
157            "cipher_plaintext_header_size",
158            "cipher_salt",
159            "kdf_iter",
160            "cipher_kdf_algorithm",
161            "cipher_use_hmac",
162            "cipher_compatibility",
163            "cipher_page_size",
164            "cipher_hmac_algorithm",
165            // Must precede any write to the database file.
166            "page_size",
167            // `locking_mode` before `journal_mode`: an exclusive lock taken
168            // before WAL is first used lets SQLite run WAL without a
169            // shared-memory file
170            // (https://www.sqlite.org/wal.html#use_of_wal_without_shared_memory).
171            // `auto_vacuum` before `journal_mode` too: changing
172            // `journal_mode` first marks the database dirty and the
173            // `auto_vacuum` change is then dropped.
174            "locking_mode",
175            "auto_vacuum",
176            "journal_mode",
177            "foreign_keys",
178            "synchronous",
179        ];
180
181        let mut pragmas = Self {
182            entries: slots
183                .into_iter()
184                .map(|name| (Cow::Borrowed(name), None))
185                .collect(),
186        };
187
188        // Bundled SQLite already defaults this ON; set it explicitly so it
189        // doesn't depend on that build flag.
190        pragmas.set("foreign_keys", "ON");
191        pragmas
192    }
193}
194
195impl Pragmas {
196    /// Sets `name` to `value`, filling its slot or replacing an earlier
197    /// value. Names match exactly.
198    pub(crate) fn set(
199        &mut self,
200        name: impl Into<Cow<'static, str>>,
201        value: impl Into<Cow<'static, str>>,
202    ) {
203        self.entries.insert(name.into(), Some(value.into()));
204    }
205
206    fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
207        self.entries
208            .iter()
209            .filter_map(|(name, value)| Some((name.as_ref(), value.as_deref()?)))
210    }
211
212    /// Applies the pragmas to a freshly opened connection.
213    pub(crate) fn apply(
214        &self,
215        conn: &RusqliteConnection,
216        query_log: &QueryLogConfig,
217    ) -> Result<()> {
218        for (name, value) in self.iter() {
219            exec(conn, name, value, query_log)?;
220        }
221
222        Ok(())
223    }
224}
225
226/// Lists the pragmas that are set, withholding secret values.
227impl fmt::Debug for Pragmas {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        f.debug_map()
230            .entries(self.iter().map(|(name, value)| {
231                let value = if SECRET.contains(&name) {
232                    "[redacted]"
233                } else {
234                    value
235                };
236                (name, value)
237            }))
238            .finish()
239    }
240}
241
242fn exec(
243    conn: &RusqliteConnection,
244    name: &str,
245    value: &str,
246    query_log: &QueryLogConfig,
247) -> Result<()> {
248    let secret = SECRET.contains(&name);
249    let sql = format!("PRAGMA {name} = {value}");
250
251    let logged = if secret {
252        Cow::Owned(format!("PRAGMA {name} = [redacted]"))
253    } else {
254        Cow::Borrowed(sql.as_str())
255    };
256    let log = QueryLog::sql(
257        query_log,
258        "sqlite",
259        &logged,
260        std::iter::empty::<&stmt::Value>(),
261    );
262
263    // `execute_batch` steps past any rows the pragma returns, such as the
264    // new mode `journal_mode` reports.
265    let result = conn
266        .execute_batch(&sql)
267        .map_err(|err| {
268            // SQLite's error message can quote the offending statement, which
269            // contains the secret value.
270            let err = if secret { redact(err) } else { err };
271            toasty_core::Error::driver_operation_failed(err)
272        })
273        .map(|()| ExecResponse::count(0));
274    log.finish(&result);
275
276    result.map(|_| ())
277}
278
279/// Keeps a `rusqlite` error's SQLite result code and drops its message.
280fn redact(err: rusqlite::Error) -> rusqlite::Error {
281    let code = err
282        .sqlite_error()
283        .copied()
284        .unwrap_or_else(|| ffi::Error::new(ffi::SQLITE_ERROR));
285    rusqlite::Error::SqliteFailure(code, None)
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    fn order(pragmas: &Pragmas) -> Vec<&str> {
293        pragmas.iter().map(|(name, _)| name).collect()
294    }
295
296    #[test]
297    fn only_foreign_keys_is_set_by_default() {
298        let pragmas = Pragmas::default();
299        assert_eq!(pragmas.iter().collect::<Vec<_>>(), [("foreign_keys", "ON")]);
300    }
301
302    #[test]
303    fn slotted_pragmas_apply_in_slot_order() {
304        let mut pragmas = Pragmas::default();
305        pragmas.set("journal_mode", "WAL");
306        pragmas.set("auto_vacuum", "FULL");
307        pragmas.set("page_size", "8192");
308        pragmas.set("locking_mode", "NORMAL");
309        pragmas.set("key", "'secret'");
310
311        assert_eq!(
312            order(&pragmas),
313            [
314                "key",
315                "page_size",
316                "locking_mode",
317                "auto_vacuum",
318                "journal_mode",
319                "foreign_keys"
320            ]
321        );
322    }
323
324    #[test]
325    fn unslotted_pragmas_follow_in_the_order_first_set() {
326        let mut pragmas = Pragmas::default();
327        pragmas.set("mmap_size", "0");
328        pragmas.set("journal_mode", "WAL");
329        pragmas.set("cache_size", "-64000");
330        pragmas.set("mmap_size", "1");
331
332        assert_eq!(
333            order(&pragmas),
334            ["journal_mode", "foreign_keys", "mmap_size", "cache_size"]
335        );
336    }
337
338    #[test]
339    fn names_match_exactly() {
340        let mut pragmas = Pragmas::default();
341        pragmas.set("JOURNAL_MODE", "WAL");
342
343        assert_eq!(order(&pragmas), ["foreign_keys", "JOURNAL_MODE"]);
344    }
345}