rust_query/migrate/
config.rs

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