Skip to main content

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(feature = "__mutants", 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}