rust_query/migrate/config.rs
1use std::{
2 path::{Path, PathBuf},
3 sync::atomic::AtomicUsize,
4};
5
6use rusqlite::config::DbConfig;
7
8#[cfg(doc)]
9use crate::migrate::{Database, Migrator};
10
11/// [Config] is used to open a database from a file or in memory.
12///
13/// This is the first step in the [Config] -> [Migrator] -> [Database] chain to
14/// get a [Database] instance.
15///
16/// # Sqlite config
17///
18/// Sqlite is configured to be in [WAL mode](https://www.sqlite.org/wal.html).
19/// The effect of this mode is that there can be any number of readers with one concurrent writer.
20/// What is nice about this is that an immutable [crate::Transaction] can always be made immediately.
21/// Making a mutable [crate::Transaction] has to wait until all other mutable [crate::Transaction]s are finished.
22pub struct Config {
23 pub(super) source: PathBuf,
24 /// Configure how often SQLite will synchronize the database to disk.
25 ///
26 /// The default is [Synchronous::Full].
27 pub synchronous: Synchronous,
28 /// Configure how foreign keys should be checked.
29 ///
30 /// The default is [ForeignKeys::SQLite], but this is likely to change to [ForeignKeys::Rust].
31 pub foreign_keys: ForeignKeys,
32}
33
34/// <https://www.sqlite.org/pragma.html#pragma_synchronous>
35///
36/// Note that the database uses WAL mode, so make sure to read the WAL specific section.
37#[non_exhaustive]
38pub enum Synchronous {
39 /// SQLite will fsync after every transaction.
40 ///
41 /// Transactions are durable, even following a power failure or hard reboot.
42 Full,
43
44 /// SQLite will only do essential fsync to prevent corruption.
45 ///
46 /// The database will not rollback transactions due to application crashes, but it might rollback due to a hardware reset or power loss.
47 /// Use this when performance is more important than durability.
48 Normal,
49}
50
51impl Synchronous {
52 #[cfg_attr(test, mutants::skip)] // hard to test
53 pub(crate) fn as_str(&self) -> &'static str {
54 match self {
55 Synchronous::Full => "FULL",
56 Synchronous::Normal => "NORMAL",
57 }
58 }
59}
60
61/// Which method should be used to check foreign-key constraints.
62///
63#[non_exhaustive]
64pub enum ForeignKeys {
65 /// Foreign-key constraints are checked by rust-query only.
66 ///
67 /// Most foreign-key checks are done at compile time and are thus completely free.
68 /// However, some runtime checks are required for deletes.
69 Rust,
70
71 /// Foreign-key constraints are checked by SQLite in addition to the checks done by rust-query.
72 ///
73 /// This is useful when using rust-query with [crate::TransactionWeak::rusqlite_transaction]
74 /// or when other software can write to the database.
75 /// Both can result in "dangling" foreign keys (which point at a non-existent row) if written incorrectly.
76 /// Dangling foreign keys can result in wrong results, but these dangling foreign keys can also turn
77 /// into "false" foreign keys if a new record is inserted that makes the foreign key valid.
78 /// This is a lot worse than a dangling foreign key, because it is generally not possible to detect.
79 ///
80 /// With the [ForeignKeys::SQLite] option, rust-query will prevent creating such false foreign keys
81 /// and panic instead.
82 /// The downside is that indexes are required on all foreign keys to make the checks efficient.
83 SQLite,
84}
85
86impl ForeignKeys {
87 pub(crate) fn as_str(&self) -> &'static str {
88 match self {
89 ForeignKeys::Rust => "OFF",
90 ForeignKeys::SQLite => "ON",
91 }
92 }
93}
94
95impl Config {
96 /// Open a database that is stored in a file.
97 /// Creates the database if it does not exist.
98 ///
99 /// Opening the same database multiple times at the same time is fine,
100 /// as long as they migrate to or use the same schema.
101 /// All locking is done by SQLite, so connections can even be made using different client implementations.
102 ///
103 /// IMPORTANT: rust-query uses SQLite in WAL mode. While a connection to the database is open there will
104 /// be an additional file with the same name as the database, but with `-wal` appended.
105 /// This "write ahead log" is automatically removed when the last connection to the database closes cleanly.
106 /// Any `-wal` file should be considered an integral part of the database and as such should be kept together.
107 /// For more details see <https://sqlite.org/howtocorrupt.html>.
108 pub fn open(p: impl AsRef<Path>) -> Self {
109 Self::open_internal(p.as_ref().to_path_buf())
110 }
111
112 /// Creates a new empty database in memory.
113 pub fn open_in_memory() -> Self {
114 static IDX: AtomicUsize = AtomicUsize::new(0);
115 let idx = IDX.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
116 let uri = format!("file:{idx}?mode=memory&cache=shared");
117 Self::open_internal(PathBuf::from(uri))
118 }
119
120 fn open_internal(source: PathBuf) -> Self {
121 Self {
122 source,
123 synchronous: Synchronous::Full,
124 foreign_keys: ForeignKeys::SQLite,
125 }
126 }
127
128 /// [Self::connect] should always be used through [crate::pool::Pool::pop]!
129 /// The pool keeps at least one connection alive to make sure that in memory databases are not dropped.
130 pub(crate) fn connect(&self) -> rusqlite::Result<rusqlite::Connection> {
131 let inner = rusqlite::Connection::open(&self.source)?;
132
133 inner.pragma_update(None, "journal_mode", "WAL")?;
134 inner.pragma_update(None, "synchronous", self.synchronous.as_str())?;
135 inner.pragma_update(None, "foreign_keys", self.foreign_keys.as_str())?;
136 inner.set_db_config(DbConfig::SQLITE_DBCONFIG_DQS_DDL, false)?;
137 inner.set_db_config(DbConfig::SQLITE_DBCONFIG_DQS_DML, false)?;
138 inner.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?;
139
140 #[cfg(feature = "bundled")]
141 inner.create_scalar_function(
142 "floor",
143 1,
144 rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
145 |ctx| {
146 assert_eq!(ctx.len(), 1, "called with unexpected number of arguments");
147 let res = ctx.get::<Option<f64>>(0)?.map(|x| x.floor());
148 Ok(res)
149 },
150 )?;
151
152 #[cfg(feature = "bundled")]
153 inner.create_scalar_function(
154 "ceil",
155 1,
156 rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
157 |ctx| {
158 assert_eq!(ctx.len(), 1, "called with unexpected number of arguments");
159 let res = ctx.get::<Option<f64>>(0)?.map(|x| x.ceil());
160 Ok(res)
161 },
162 )?;
163
164 #[cfg(feature = "jiff-02")]
165 inner.create_scalar_function(
166 "timestamp_add_nanosecond",
167 2,
168 rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
169 |ctx| {
170 use crate::value::DbTyp;
171 assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
172 if matches!(ctx.get_raw(0), rusqlite::types::ValueRef::Null)
173 || matches!(ctx.get_raw(1), rusqlite::types::ValueRef::Null)
174 {
175 return Ok(None);
176 }
177
178 let timestamp = jiff::Timestamp::from_sql(ctx.get_raw(0))?;
179 let seconds = ctx.get::<i64>(1)?;
180 let new = timestamp + jiff::SignedDuration::from_nanos(seconds);
181 let rusqlite::types::Value::Text(res) = jiff::Timestamp::out_to_value(new) else {
182 unreachable!("func always returns some string")
183 };
184 Ok(Some(res))
185 },
186 )?;
187
188 #[cfg(feature = "jiff-02")]
189 inner.create_scalar_function(
190 "timestamp_subsec_nanosecond",
191 1,
192 rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
193 |ctx| {
194 use crate::value::DbTyp;
195 assert_eq!(ctx.len(), 1, "called with unexpected number of arguments");
196 if matches!(ctx.get_raw(0), rusqlite::types::ValueRef::Null) {
197 return Ok(None);
198 }
199
200 let timestamp = jiff::Timestamp::from_sql(ctx.get_raw(0))?;
201 Ok(Some(timestamp.subsec_nanosecond()))
202 },
203 )?;
204
205 #[cfg(feature = "jiff-02")]
206 inner.create_scalar_function(
207 "timestamp_to_second",
208 1,
209 rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
210 |ctx| {
211 use crate::value::DbTyp;
212 assert_eq!(ctx.len(), 1, "called with unexpected number of arguments");
213 if matches!(ctx.get_raw(0), rusqlite::types::ValueRef::Null) {
214 return Ok(None);
215 }
216
217 let timestamp = jiff::Timestamp::from_sql(ctx.get_raw(0))?;
218 Ok(Some(timestamp.as_second()))
219 },
220 )?;
221
222 #[cfg(feature = "jiff-02")]
223 inner.create_scalar_function(
224 "timestamp_to_date",
225 2,
226 rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC,
227 |ctx| {
228 use jiff::fmt::temporal;
229
230 use crate::value::DbTyp;
231 assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
232 if matches!(ctx.get_raw(0), rusqlite::types::ValueRef::Null)
233 || matches!(ctx.get_raw(1), rusqlite::types::ValueRef::Null)
234 {
235 return Ok(None);
236 }
237
238 static PARSER: temporal::DateTimeParser = temporal::DateTimeParser::new();
239
240 let timestamp = jiff::Timestamp::from_sql(ctx.get_raw(0))?;
241 let timezone = PARSER
242 .parse_time_zone(ctx.get_raw(1).as_str()?)
243 .expect("time zone was serialized with jiff");
244 let date = timezone.to_datetime(timestamp).date();
245 let rusqlite::types::Value::Text(res) = jiff::civil::Date::out_to_value(date)
246 else {
247 unreachable!("func always returns some string")
248 };
249 Ok(Some(res))
250 },
251 )?;
252
253 Ok(inner)
254 }
255}