1use rusqlite::{Connection, ToSql};
5
6use crate::{
7 SqliteConfig,
8 error::{SqliteError, SqliteResult},
9};
10
11pub fn apply(conn: &Connection, config: &SqliteConfig) -> SqliteResult<()> {
12 if let Some(page_size) = config.page_size {
13 set(conn, "page_size", page_size.as_bytes() as u32)?;
14 }
15 set(conn, "secure_delete", "FAST")?;
16 if let Some(journal_mode) = config.journal_mode {
17 set(conn, "journal_mode", journal_mode.as_str())?;
18 }
19 if let Some(synchronous_mode) = config.synchronous_mode {
20 set(conn, "synchronous", synchronous_mode.as_str())?;
21 }
22 if let Some(temp_store) = config.temp_store {
23 set(conn, "temp_store", temp_store.as_str())?;
24 }
25 if let Some(cache_size) = config.cache_size {
26 set(conn, "cache_size", -(cache_size.as_kib() as i32))?;
27 }
28 if let Some(wal_autocheckpoint) = config.wal_autocheckpoint {
29 set(conn, "wal_autocheckpoint", wal_autocheckpoint)?;
30 }
31 if let Some(mmap_size) = config.mmap_size {
32 set(conn, "mmap_size", mmap_size.as_bytes() as i64)?;
33 }
34 conn.set_prepared_statement_cache_capacity(config.prepared_statement_cache_capacity as usize);
35 Ok(())
36}
37
38pub fn apply_read_only(conn: &Connection, config: &SqliteConfig) -> SqliteResult<()> {
39 set(conn, "query_only", true)?;
40 if let Some(temp_store) = config.temp_store {
41 set(conn, "temp_store", temp_store.as_str())?;
42 }
43 if let Some(cache_size) = config.cache_size {
44 set(conn, "cache_size", -(cache_size.as_kib() as i32))?;
45 }
46 if let Some(mmap_size) = config.mmap_size {
47 set(conn, "mmap_size", mmap_size.as_bytes() as i64)?;
48 }
49 conn.set_prepared_statement_cache_capacity(config.prepared_statement_cache_capacity as usize);
50 Ok(())
51}
52
53pub fn shrink_memory(conn: &Connection) -> SqliteResult<()> {
54 set(conn, "shrink_memory", 0)
55}
56
57pub fn shutdown(conn: &Connection) -> SqliteResult<()> {
58 set(conn, "wal_checkpoint", "TRUNCATE")?;
59 set(conn, "cache_size", 0)?;
60 Ok(())
61}
62
63fn set<V: ToSql>(conn: &Connection, name: &str, value: V) -> SqliteResult<()> {
64 conn.pragma_update(None, name, value).map_err(|source| SqliteError::Pragma {
65 name: name.into(),
66 source,
67 })
68}
69
70#[cfg(test)]
71mod tests {
72 use std::{env::temp_dir, fs::remove_file, path::PathBuf};
73
74 use reifydb_value::byte_size::ByteSize;
75 use rusqlite::Connection;
76 use uuid::Uuid;
77
78 use super::{apply, apply_read_only};
79 use crate::SqliteConfig;
80
81 fn scratch(name: &str) -> (Connection, PathBuf) {
82 let path = temp_dir().join(format!("reifydb_pragma_{name}_{}.db", Uuid::new_v4()));
84 let conn = Connection::open(&path).unwrap();
85 (conn, path)
86 }
87
88 fn cleanup(conn: Connection, path: PathBuf) {
89 drop(conn);
90 let _ = remove_file(&path);
91 let _ = remove_file(path.with_extension("db-wal"));
92 let _ = remove_file(path.with_extension("db-shm"));
93 }
94
95 #[test]
96 fn a_none_pragma_is_never_issued_and_sqlites_own_default_survives() {
97 let (conn, path) = scratch("none");
101 let config = SqliteConfig::new(&path).cache_size(ByteSize::from_kib(4321)).cache_size(None);
102
103 apply(&conn, &config).unwrap();
104
105 let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
106 assert_eq!(cache_size, -2000, "an unset cache_size must leave SQLite's own default in place");
107
108 cleanup(conn, path);
109 }
110
111 #[test]
112 fn journal_mode_none_leaves_a_fresh_database_on_a_rollback_journal() {
113 let (unset_conn, unset_path) = scratch("journal_none");
117 apply(&unset_conn, &SqliteConfig::new(&unset_path).journal_mode(None)).unwrap();
118 let unset: String = unset_conn.pragma_query_value(None, "journal_mode", |r| r.get(0)).unwrap();
119
120 let (default_conn, default_path) = scratch("journal_default");
121 apply(&default_conn, &SqliteConfig::new(&default_path)).unwrap();
122 let default: String = default_conn.pragma_query_value(None, "journal_mode", |r| r.get(0)).unwrap();
123
124 assert_eq!(unset, "delete", "an unset journal_mode drops a fresh database to a rollback journal");
125 assert_eq!(default, "wal", "the constructors must keep shipping WAL");
126
127 cleanup(unset_conn, unset_path);
128 cleanup(default_conn, default_path);
129 }
130
131 #[test]
132 fn an_explicit_zero_still_issues_the_pragma() {
133 let (conn, path) = scratch("zero");
137
138 apply(&conn, &SqliteConfig::new(&path).cache_size(ByteSize::ZERO)).unwrap();
139
140 let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
141 assert_eq!(cache_size, 0, "an explicit zero must be issued, not mistaken for unset");
142
143 cleanup(conn, path);
144 }
145
146 #[test]
147 fn the_read_only_path_decides_each_pragma_independently() {
148 let (conn, path) = scratch("readonly");
151 let config = SqliteConfig::new(&path).mmap_size(None).cache_size(ByteSize::from_kib(1500));
152
153 apply_read_only(&conn, &config).unwrap();
154
155 let mmap_size: i64 = conn.pragma_query_value(None, "mmap_size", |r| r.get(0)).unwrap();
156 let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
157
158 assert_eq!(mmap_size, 0, "an unset mmap_size must leave SQLite's default of 0");
159 assert_eq!(cache_size, -1500, "a set cache_size must still be issued alongside a skipped one");
160
161 cleanup(conn, path);
162 }
163
164 #[test]
165 fn test_apply_converts_units_for_pragmas() {
166 let path = temp_dir().join(format!("reifydb_pragma_{}.db", Uuid::new_v4()));
167 let conn = Connection::open(&path).unwrap();
168
169 apply(&conn, &SqliteConfig::new(&path)).unwrap();
171
172 let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
173 let page_size: i64 = conn.pragma_query_value(None, "page_size", |r| r.get(0)).unwrap();
174 let mmap_size: i64 = conn.pragma_query_value(None, "mmap_size", |r| r.get(0)).unwrap();
175 let secure_delete: i64 = conn.pragma_query_value(None, "secure_delete", |r| r.get(0)).unwrap();
176
177 assert_eq!(cache_size, -2000, "cache_size must be the KiB count negated");
178 assert_eq!(page_size, 4096, "page_size must be raw bytes");
179 assert_eq!(mmap_size, 67_108_864, "mmap_size must be raw bytes (64 MiB)");
180 assert_eq!(secure_delete, 2, "secure_delete must be FAST (2), not ON (1)");
183
184 drop(conn);
185 let _ = remove_file(&path);
186 let _ = remove_file(path.with_extension("db-wal"));
187 let _ = remove_file(path.with_extension("db-shm"));
188 }
189}