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 an immutable [crate::Transaction] can always be made immediately.
16/// Making a mutable [crate::Transaction] has to wait until all other mutable [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 ///
26 /// The default is [ForeignKeys::SQLite], but this is likely to change to [ForeignKeys::Rust].
27 pub foreign_keys: ForeignKeys,
28}
29
30/// <https://www.sqlite.org/pragma.html#pragma_synchronous>
31///
32/// Note that the database uses WAL mode, so make sure to read the WAL specific section.
33#[non_exhaustive]
34pub enum Synchronous {
35 /// SQLite will fsync after every transaction.
36 ///
37 /// Transactions are durable, even following a power failure or hard reboot.
38 Full,
39
40 /// SQLite will only do essential fsync to prevent corruption.
41 ///
42 /// The database will not rollback transactions due to application crashes, but it might rollback due to a hardware reset or power loss.
43 /// Use this when performance is more important than durability.
44 Normal,
45}
46
47impl Synchronous {
48 #[cfg_attr(test, mutants::skip)] // hard to test
49 pub(crate) fn as_str(self) -> &'static str {
50 match self {
51 Synchronous::Full => "FULL",
52 Synchronous::Normal => "NORMAL",
53 }
54 }
55}
56
57/// Which method should be used to check foreign-key constraints.
58///
59#[non_exhaustive]
60pub enum ForeignKeys {
61 /// Foreign-key constraints are checked by rust-query only.
62 ///
63 /// Most foreign-key checks are done at compile time and are thus completely free.
64 /// However, some runtime checks are required for deletes.
65 Rust,
66
67 /// Foreign-key constraints are checked by SQLite in addition to the checks done by rust-query.
68 ///
69 /// This is useful when using rust-query with [crate::TransactionWeak::rusqlite_transaction]
70 /// or when other software can write to the database.
71 /// Both can result in "dangling" foreign keys (which point at a non-existent row) if written incorrectly.
72 /// Dangling foreign keys can result in wrong results, but these dangling foreign keys can also turn
73 /// into "false" foreign keys if a new record is inserted that makes the foreign key valid.
74 /// This is a lot worse than a dangling foreign key, because it is generally not possible to detect.
75 ///
76 /// With the [ForeignKeys::SQLite] option, rust-query will prevent creating such false foreign keys
77 /// and panic instead.
78 /// The downside is that indexes are required on all foreign keys to make the checks efficient.
79 SQLite,
80}
81
82impl ForeignKeys {
83 pub(crate) fn as_str(self) -> &'static str {
84 match self {
85 ForeignKeys::Rust => "OFF",
86 ForeignKeys::SQLite => "ON",
87 }
88 }
89}
90
91impl Config {
92 /// Open a database that is stored in a file.
93 /// Creates the database if it does not exist.
94 ///
95 /// Opening the same database multiple times at the same time is fine,
96 /// as long as they migrate to or use the same schema.
97 /// All locking is done by SQLite, so connections can even be made using different client implementations.
98 ///
99 /// IMPORTANT: rust-query uses SQLite in WAL mode. While a connection to the database is open there will
100 /// be an additional file with the same name as the database, but with `-wal` appended.
101 /// This "write ahead log" is automatically removed when the last connection to the database closes cleanly.
102 /// Any `-wal` file should be considered an integral part of the database and as such should be kept together.
103 /// For more details see <https://sqlite.org/howtocorrupt.html>.
104 pub fn open(p: impl AsRef<Path>) -> Self {
105 let manager = r2d2_sqlite::SqliteConnectionManager::file(p);
106 Self::open_internal(manager)
107 }
108
109 /// Creates a new empty database in memory.
110 pub fn open_in_memory() -> Self {
111 let manager = r2d2_sqlite::SqliteConnectionManager::memory();
112 Self::open_internal(manager)
113 }
114
115 fn open_internal(manager: r2d2_sqlite::SqliteConnectionManager) -> Self {
116 Self {
117 manager,
118 init: Box::new(|_| {}),
119 synchronous: Synchronous::Full,
120 foreign_keys: ForeignKeys::SQLite,
121 }
122 }
123
124 /// Append a raw sql statement to be executed if the database was just created.
125 ///
126 /// The statement is executed after creating the empty database and executing all previous statements.
127 ///
128 /// [crate::migration::Migrator::fixup] should be prefered over this method.
129 pub fn init_stmt(mut self, sql: &'static str) -> Self {
130 self.init = Box::new(move |txn| {
131 (self.init)(txn);
132
133 txn.execute_batch(sql)
134 .expect("raw sql statement to populate db failed");
135 });
136 self
137 }
138}