196 lines
6.5 KiB
Rust
196 lines
6.5 KiB
Rust
//! 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<u32, rusqlite::Error> {
|
|
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<String, rusqlite::Error> {
|
|
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<DatabaseHealthReport, rusqlite::Error> {
|
|
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);
|
|
}
|
|
}
|