use rusqlite::{Connection, ToSql};
use crate::{
SqliteConfig,
error::{SqliteError, SqliteResult},
};
pub fn apply(conn: &Connection, config: &SqliteConfig) -> SqliteResult<()> {
if let Some(page_size) = config.page_size {
set(conn, "page_size", page_size.as_bytes() as u32)?;
}
set(conn, "secure_delete", "FAST")?;
if let Some(journal_mode) = config.journal_mode {
set(conn, "journal_mode", journal_mode.as_str())?;
}
if let Some(synchronous_mode) = config.synchronous_mode {
set(conn, "synchronous", synchronous_mode.as_str())?;
}
if let Some(temp_store) = config.temp_store {
set(conn, "temp_store", temp_store.as_str())?;
}
if let Some(cache_size) = config.cache_size {
set(conn, "cache_size", -(cache_size.as_kib() as i32))?;
}
if let Some(wal_autocheckpoint) = config.wal_autocheckpoint {
set(conn, "wal_autocheckpoint", wal_autocheckpoint)?;
}
if let Some(mmap_size) = config.mmap_size {
set(conn, "mmap_size", mmap_size.as_bytes() as i64)?;
}
conn.set_prepared_statement_cache_capacity(config.prepared_statement_cache_capacity as usize);
Ok(())
}
pub fn apply_read_only(conn: &Connection, config: &SqliteConfig) -> SqliteResult<()> {
set(conn, "query_only", true)?;
if let Some(temp_store) = config.temp_store {
set(conn, "temp_store", temp_store.as_str())?;
}
if let Some(cache_size) = config.cache_size {
set(conn, "cache_size", -(cache_size.as_kib() as i32))?;
}
if let Some(mmap_size) = config.mmap_size {
set(conn, "mmap_size", mmap_size.as_bytes() as i64)?;
}
conn.set_prepared_statement_cache_capacity(config.prepared_statement_cache_capacity as usize);
Ok(())
}
pub fn shrink_memory(conn: &Connection) -> SqliteResult<()> {
set(conn, "shrink_memory", 0)
}
pub fn shutdown(conn: &Connection) -> SqliteResult<()> {
set(conn, "wal_checkpoint", "TRUNCATE")?;
set(conn, "cache_size", 0)?;
Ok(())
}
fn set<V: ToSql>(conn: &Connection, name: &str, value: V) -> SqliteResult<()> {
conn.pragma_update(None, name, value).map_err(|source| SqliteError::Pragma {
name: name.into(),
source,
})
}
#[cfg(test)]
mod tests {
use std::{env::temp_dir, fs::remove_file, path::PathBuf};
use reifydb_value::byte_size::ByteSize;
use rusqlite::Connection;
use uuid::Uuid;
use super::{apply, apply_read_only};
use crate::SqliteConfig;
fn scratch(name: &str) -> (Connection, PathBuf) {
let path = temp_dir().join(format!("reifydb_pragma_{name}_{}.db", Uuid::new_v4()));
let conn = Connection::open(&path).unwrap();
(conn, path)
}
fn cleanup(conn: Connection, path: PathBuf) {
drop(conn);
let _ = remove_file(&path);
let _ = remove_file(path.with_extension("db-wal"));
let _ = remove_file(path.with_extension("db-shm"));
}
#[test]
fn a_none_pragma_is_never_issued_and_sqlites_own_default_survives() {
let (conn, path) = scratch("none");
let config = SqliteConfig::new(&path).cache_size(ByteSize::from_kib(4321)).cache_size(None);
apply(&conn, &config).unwrap();
let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
assert_eq!(cache_size, -2000, "an unset cache_size must leave SQLite's own default in place");
cleanup(conn, path);
}
#[test]
fn journal_mode_none_leaves_a_fresh_database_on_a_rollback_journal() {
let (unset_conn, unset_path) = scratch("journal_none");
apply(&unset_conn, &SqliteConfig::new(&unset_path).journal_mode(None)).unwrap();
let unset: String = unset_conn.pragma_query_value(None, "journal_mode", |r| r.get(0)).unwrap();
let (default_conn, default_path) = scratch("journal_default");
apply(&default_conn, &SqliteConfig::new(&default_path)).unwrap();
let default: String = default_conn.pragma_query_value(None, "journal_mode", |r| r.get(0)).unwrap();
assert_eq!(unset, "delete", "an unset journal_mode drops a fresh database to a rollback journal");
assert_eq!(default, "wal", "the constructors must keep shipping WAL");
cleanup(unset_conn, unset_path);
cleanup(default_conn, default_path);
}
#[test]
fn an_explicit_zero_still_issues_the_pragma() {
let (conn, path) = scratch("zero");
apply(&conn, &SqliteConfig::new(&path).cache_size(ByteSize::ZERO)).unwrap();
let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
assert_eq!(cache_size, 0, "an explicit zero must be issued, not mistaken for unset");
cleanup(conn, path);
}
#[test]
fn the_read_only_path_decides_each_pragma_independently() {
let (conn, path) = scratch("readonly");
let config = SqliteConfig::new(&path).mmap_size(None).cache_size(ByteSize::from_kib(1500));
apply_read_only(&conn, &config).unwrap();
let mmap_size: i64 = conn.pragma_query_value(None, "mmap_size", |r| r.get(0)).unwrap();
let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
assert_eq!(mmap_size, 0, "an unset mmap_size must leave SQLite's default of 0");
assert_eq!(cache_size, -1500, "a set cache_size must still be issued alongside a skipped one");
cleanup(conn, path);
}
#[test]
fn test_apply_converts_units_for_pragmas() {
let path = temp_dir().join(format!("reifydb_pragma_{}.db", Uuid::new_v4()));
let conn = Connection::open(&path).unwrap();
apply(&conn, &SqliteConfig::new(&path)).unwrap();
let cache_size: i64 = conn.pragma_query_value(None, "cache_size", |r| r.get(0)).unwrap();
let page_size: i64 = conn.pragma_query_value(None, "page_size", |r| r.get(0)).unwrap();
let mmap_size: i64 = conn.pragma_query_value(None, "mmap_size", |r| r.get(0)).unwrap();
let secure_delete: i64 = conn.pragma_query_value(None, "secure_delete", |r| r.get(0)).unwrap();
assert_eq!(cache_size, -2000, "cache_size must be the KiB count negated");
assert_eq!(page_size, 4096, "page_size must be raw bytes");
assert_eq!(mmap_size, 67_108_864, "mmap_size must be raw bytes (64 MiB)");
assert_eq!(secure_delete, 2, "secure_delete must be FAST (2), not ON (1)");
drop(conn);
let _ = remove_file(&path);
let _ = remove_file(path.with_extension("db-wal"));
let _ = remove_file(path.with_extension("db-shm"));
}
}