//! Strongly-typed SQLite PRAGMA and configuration helpers. //! //! This module provides safe wrappers around common SQLite PRAGMAs using //! rusqlite's type-safe APIs (`pragma_update`, `pragma_query_value`, `query_row`) //! instead of raw `execute` calls. All functions use structured tracing for //! observability. use rusqlite::Connection; use serde::Serialize; use tracing::info; /// Enables WAL (Write-Ahead Logging) journal mode on the given connection. /// /// Uses `query_row` with `PRAGMA journal_mode=WAL` which both sets and returns /// the actual mode. Returns an error if the database does not confirm WAL mode. pub fn enable_wal(conn: &Connection, db_name: &str) -> Result<(), rusqlite::Error> { let actual_mode: String = conn.query_row("PRAGMA journal_mode=WAL;", [], |row| { row.get::<_, String>(0) })?; info!(database = db_name, mode = %actual_mode, "WAL mode configured"); if actual_mode.to_lowercase() != "wal" { return Err(rusqlite::Error::QueryReturnedNoRows); } Ok(()) } /// Enables foreign key constraint enforcement on the given connection. /// /// Sets `foreign_keys` to ON via `pragma_update`, then verifies the setting /// was applied by reading it back with `pragma_query_value`. pub fn enable_foreign_keys(conn: &Connection, db_name: &str) -> Result<(), rusqlite::Error> { conn.pragma_update(None, "foreign_keys", "ON")?; let enabled: bool = conn.pragma_query_value(None, "foreign_keys", |row| row.get::<_, bool>(0))?; info!( database = db_name, foreign_keys = enabled, "Foreign key enforcement configured" ); if !enabled { return Err(rusqlite::Error::QueryReturnedNoRows); } Ok(()) } /// Sets the schema user_version on the given connection. /// /// Uses `pragma_update` with the type-safe API — no `format!` string, no raw /// `execute`. pub fn set_user_version(conn: &Connection, version: i32) -> Result<(), rusqlite::Error> { conn.pragma_update(None, "user_version", version) } /// Returns the current schema user_version from the given connection. pub fn get_user_version(conn: &Connection) -> Result { conn.pragma_query_value(None, "user_version", |row| row.get::<_, u32>(0)) } /// Runs `PRAGMA integrity_check` and returns `Ok(())` if the database reports "ok". /// /// If the integrity check returns any other value, the function returns an error /// containing the integrity check message. pub fn integrity_check(conn: &Connection, db_name: &str) -> Result<(), rusqlite::Error> { let result: String = conn.query_row("PRAGMA integrity_check;", [], |row| row.get::<_, String>(0))?; if result == "ok" { info!(database = db_name, "Integrity check passed"); Ok(()) } else { Err(rusqlite::Error::SqliteFailure( rusqlite::ffi::Error::new(rusqlite::ffi::SQLITE_CORRUPT), Some(format!("Integrity check failed for {db_name}: {result}")), )) } } /// Returns the current journal mode of the given connection. pub fn get_journal_mode(conn: &Connection) -> Result { conn.pragma_query_value(None, "journal_mode", |row| row.get::<_, String>(0)) } /// A snapshot of database health information collected from various PRAGMAs. #[derive(Debug, Clone, Serialize)] pub struct DatabaseHealthReport { /// Name of the database (e.g. "admin", "content"). pub database: String, /// Current schema version (`user_version` PRAGMA). pub schema_version: u32, /// Active journal mode (e.g. "wal", "delete"). pub journal_mode: String, /// Whether foreign key enforcement is enabled. pub foreign_keys_enabled: bool, /// Whether `PRAGMA integrity_check` returned "ok". pub integrity_ok: bool, } /// Collects a [`DatabaseHealthReport`] by querying all relevant PRAGMAs. /// /// This function queries `user_version`, `journal_mode`, `foreign_keys`, and /// `integrity_check` to build a comprehensive health snapshot. The report is /// logged at `info` level with structured fields. pub fn collect_health_report( conn: &Connection, db_name: &str, ) -> Result { let schema_version = get_user_version(conn)?; let journal_mode = get_journal_mode(conn)?; let foreign_keys_enabled: bool = conn.pragma_query_value(None, "foreign_keys", |row| row.get::<_, bool>(0))?; let integrity_result: String = conn.query_row("PRAGMA integrity_check;", [], |row| row.get::<_, String>(0))?; let integrity_ok = integrity_result == "ok"; let report = DatabaseHealthReport { database: db_name.to_owned(), schema_version, journal_mode, foreign_keys_enabled, integrity_ok, }; info!( database = %report.database, version = report.schema_version, journal_mode = %report.journal_mode, foreign_keys = report.foreign_keys_enabled, integrity = report.integrity_ok, "Database health report collected" ); Ok(report) } #[cfg(test)] mod tests { use super::*; use rusqlite::Connection; fn memory_conn() -> Connection { Connection::open_in_memory().expect("Failed to open in-memory database") } #[test] fn test_enable_wal() { let conn = memory_conn(); // In-memory databases may not support WAL; we just verify no panic. // On-disk databases would return "wal". let _ = enable_wal(&conn, "test"); } #[test] fn test_enable_foreign_keys() { let conn = memory_conn(); enable_foreign_keys(&conn, "test").expect("Failed to enable foreign keys"); } #[test] fn test_user_version_roundtrip() { let conn = memory_conn(); set_user_version(&conn, 42).expect("Failed to set user_version"); let v = get_user_version(&conn).expect("Failed to get user_version"); assert_eq!(v, 42); } #[test] fn test_get_journal_mode() { let conn = memory_conn(); let mode = get_journal_mode(&conn).expect("Failed to get journal_mode"); assert!(!mode.is_empty()); } #[test] fn test_integrity_check() { let conn = memory_conn(); integrity_check(&conn, "test").expect("Integrity check should pass on fresh db"); } #[test] fn test_collect_health_report() { let conn = memory_conn(); let report = collect_health_report(&conn, "test").expect("Failed to collect health report"); assert_eq!(report.database, "test"); assert!(report.integrity_ok); } }