Skip to main content

koan_core/db/
connection.rs

1use std::path::Path;
2
3use rusqlite::Connection;
4use thiserror::Error;
5
6use super::schema;
7use crate::config;
8
9#[derive(Debug, Error)]
10pub enum DbError {
11    #[error("sqlite error: {0}")]
12    Sqlite(#[from] rusqlite::Error),
13    #[error("io error: {0}")]
14    Io(#[from] std::io::Error),
15}
16
17/// Wrapper around a SQLite connection with koan's schema applied.
18pub struct Database {
19    pub conn: Connection,
20}
21
22impl Database {
23    /// Open (or create) a database at the given path.
24    pub fn open(path: &Path) -> Result<Self, DbError> {
25        if let Some(parent) = path.parent() {
26            std::fs::create_dir_all(parent)?;
27        }
28
29        let conn = Connection::open(path)?;
30
31        #[cfg(unix)]
32        {
33            use std::os::unix::fs::PermissionsExt;
34            let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
35        }
36
37        // WAL mode for concurrent reads + single writer.
38        conn.pragma_update(None, "journal_mode", "wal")?;
39        conn.pragma_update(None, "foreign_keys", "on")?;
40        conn.pragma_update(None, "busy_timeout", 5000)?;
41        // Slightly faster at the cost of durability on power loss (acceptable for a media DB).
42        conn.pragma_update(None, "synchronous", "normal")?;
43
44        // Attempt a passive WAL checkpoint on open. This is non-blocking — it
45        // moves WAL pages back to the main DB file only if no readers/writers
46        // are active, preventing unbounded WAL growth across sessions.
47        let _ = conn.execute_batch("PRAGMA wal_checkpoint(PASSIVE)");
48
49        schema::create_tables(&conn)?;
50
51        Ok(Self { conn })
52    }
53
54    /// Open the default database at the standard data directory.
55    pub fn open_default() -> Result<Self, DbError> {
56        Self::open(&config::db_path())
57    }
58}