rust_query/migrate/
config.rs

1use std::path::Path;
2
3#[cfg(doc)]
4use crate::migrate::{Database, Migrator};
5
6/// [Config] is used to open a database from a file or in memory.
7///
8/// This is the first step in the [Config] -> [Migrator] -> [Database] chain to
9/// get a [Database] instance.
10///
11/// # Sqlite config
12///
13/// Sqlite is configured to be in [WAL mode](https://www.sqlite.org/wal.html).
14/// The effect of this mode is that there can be any number of readers with one concurrent writer.
15/// What is nice about this is that a `&`[crate::Transaction] can always be made immediately.
16/// Making a `&mut`[crate::Transaction] has to wait until all other `&mut`[crate::Transaction]s are finished.
17pub struct Config {
18    pub(super) manager: r2d2_sqlite::SqliteConnectionManager,
19    pub(super) init: Box<dyn FnOnce(&rusqlite::Transaction)>,
20    /// Configure how often SQLite will synchronize the database to disk.
21    ///
22    /// The default is [Synchronous::Full].
23    pub synchronous: Synchronous,
24    /// Configure how foreign keys should be checked.
25    pub foreign_keys: ForeignKeys,
26}
27
28/// <https://www.sqlite.org/pragma.html#pragma_synchronous>
29///
30/// Note that the database uses WAL mode, so make sure to read the WAL specific section.
31#[non_exhaustive]
32pub enum Synchronous {
33    /// SQLite will fsync after every transaction.
34    ///
35    /// Transactions are durable, even following a power failure or hard reboot.
36    Full,
37
38    /// SQLite will only do essential fsync to prevent corruption.
39    ///
40    /// The database will not rollback transactions due to application crashes, but it might rollback due to a hardware reset or power loss.
41    /// Use this when performance is more important than durability.
42    Normal,
43}
44
45impl Synchronous {
46    #[cfg_attr(test, mutants::skip)] // hard to test
47    pub(crate) fn as_str(self) -> &'static str {
48        match self {
49            Synchronous::Full => "FULL",
50            Synchronous::Normal => "NORMAL",
51        }
52    }
53}
54
55/// Which method should be used to check foreign-key constraints.
56///
57/// The default is [ForeignKeys::SQLite], but this is likely to change to [ForeignKeys::Rust].
58#[non_exhaustive]
59pub enum ForeignKeys {
60    /// Foreign-key constraints are checked by rust-query only.
61    ///
62    /// Most foreign-key checks are done at compile time and are thus completely free.
63    /// However, some runtime checks are required for deletes.
64    Rust,
65
66    /// Foreign-key constraints are checked by SQLite in addition to the checks done by rust-query.
67    ///
68    /// This is useful when using rust-query with [crate::TransactionWeak::rusqlite_transaction]
69    /// or when other software can write to the database.
70    /// Both can result in "dangling" foreign keys (which point at a non-existent row) if written incorrectly.
71    /// Dangling foreign keys can result in wrong results, but these dangling foreign keys can also turn
72    /// into "false" foreign keys if a new record is inserted that makes the foreign key valid.
73    /// This is a lot worse than a dangling foreign key, because it is generally not possible to detect.
74    ///
75    /// With the [ForeignKeys::SQLite] option, rust-query will prevent creating such false foreign keys
76    /// and panic instead.
77    /// The downside is that indexes are required on all foreign keys to make the checks efficient.
78    SQLite,
79}
80
81impl ForeignKeys {
82    pub(crate) fn as_str(self) -> &'static str {
83        match self {
84            ForeignKeys::Rust => "OFF",
85            ForeignKeys::SQLite => "ON",
86        }
87    }
88}
89
90impl Config {
91    /// Open a database that is stored in a file.
92    /// Creates the database if it does not exist.
93    ///
94    /// Opening the same database multiple times at the same time is fine,
95    /// as long as they migrate to or use the same schema.
96    /// All locking is done by sqlite, so connections can even be made using different client implementations.
97    pub fn open(p: impl AsRef<Path>) -> Self {
98        let manager = r2d2_sqlite::SqliteConnectionManager::file(p);
99        Self::open_internal(manager)
100    }
101
102    /// Creates a new empty database in memory.
103    pub fn open_in_memory() -> Self {
104        let manager = r2d2_sqlite::SqliteConnectionManager::memory();
105        Self::open_internal(manager)
106    }
107
108    fn open_internal(manager: r2d2_sqlite::SqliteConnectionManager) -> Self {
109        Self {
110            manager,
111            init: Box::new(|_| {}),
112            synchronous: Synchronous::Full,
113            foreign_keys: ForeignKeys::SQLite,
114        }
115    }
116
117    /// Append a raw sql statement to be executed if the database was just created.
118    ///
119    /// The statement is executed after creating the empty database and executing all previous statements.
120    pub fn init_stmt(mut self, sql: &'static str) -> Self {
121        self.init = Box::new(move |txn| {
122            (self.init)(txn);
123
124            txn.execute_batch(sql)
125                .expect("raw sql statement to populate db failed");
126        });
127        self
128    }
129}