Skip to main content

bones_core/db/
mod.rs

1//! `SQLite` projection database utilities.
2//!
3//! Runtime defaults are intentionally conservative:
4//! - `journal_mode = WAL` to allow concurrent readers while writers append
5//! - `busy_timeout = 5s` to reduce transient lock failures under contention
6//! - `foreign_keys = ON` to protect relational integrity in projection tables
7
8pub mod fts;
9pub mod incremental;
10pub mod migrations;
11pub mod project;
12pub mod query;
13pub mod rebuild;
14pub mod schema;
15
16use anyhow::{Context, Result};
17use rusqlite::Connection;
18use std::{path::Path, time::Duration};
19use tracing::debug;
20
21/// Busy timeout used for projection DB connections.
22pub const DEFAULT_BUSY_TIMEOUT: Duration = Duration::from_secs(5);
23
24/// Open (or create) the projection `SQLite` database, apply runtime pragmas,
25/// and migrate schema to the latest version.
26///
27/// # Errors
28///
29/// Returns an error if opening/configuring/migrating the database fails.
30pub fn open_projection(path: &Path) -> Result<Connection> {
31    if let Some(parent) = path.parent() {
32        std::fs::create_dir_all(parent)
33            .with_context(|| format!("create projection db directory {}", parent.display()))?;
34    }
35
36    if let Err(err) = bones_sqlite_vec::register_auto_extension() {
37        debug!(%err, "sqlite-vec auto-extension unavailable");
38    }
39
40    let mut conn = Connection::open(path)
41        .with_context(|| format!("open projection database {}", path.display()))?;
42
43    configure_connection(&conn).context("configure sqlite pragmas")?;
44    migrations::migrate(&mut conn).context("apply projection migrations")?;
45
46    Ok(conn)
47}
48
49fn configure_connection(conn: &Connection) -> anyhow::Result<()> {
50    conn.pragma_update(None, "foreign_keys", "ON")
51        .context("PRAGMA foreign_keys = ON")?;
52    conn.pragma_update(None, "synchronous", "NORMAL")
53        .context("PRAGMA synchronous = NORMAL")?;
54    let _journal_mode: String = conn
55        .query_row("PRAGMA journal_mode = WAL", [], |row| row.get(0))
56        .context("PRAGMA journal_mode = WAL")?;
57    conn.busy_timeout(DEFAULT_BUSY_TIMEOUT)
58        .context("busy_timeout")?;
59    Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::{DEFAULT_BUSY_TIMEOUT, open_projection};
65    use crate::db::migrations;
66    use tempfile::TempDir;
67
68    fn temp_db_path() -> (TempDir, std::path::PathBuf) {
69        let dir = tempfile::tempdir().expect("create temp dir");
70        let path = dir.path().join("bones-projection.sqlite3");
71        (dir, path)
72    }
73
74    #[test]
75    fn open_projection_sets_wal_busy_timeout_and_fk() {
76        let (_dir, path) = temp_db_path();
77        let conn = open_projection(&path).expect("open projection db");
78
79        let journal_mode: String = conn
80            .pragma_query_value(None, "journal_mode", |row| row.get(0))
81            .expect("query journal_mode");
82        assert_eq!(journal_mode.to_ascii_lowercase(), "wal");
83
84        let busy_timeout_ms: u64 = conn
85            .pragma_query_value(None, "busy_timeout", |row| row.get(0))
86            .expect("query busy_timeout");
87        assert_eq!(
88            u128::from(busy_timeout_ms),
89            DEFAULT_BUSY_TIMEOUT.as_millis()
90        );
91
92        let foreign_keys: i64 = conn
93            .pragma_query_value(None, "foreign_keys", |row| row.get(0))
94            .expect("query foreign_keys");
95        assert_eq!(foreign_keys, 1);
96    }
97
98    #[test]
99    fn open_projection_runs_migrations() {
100        let (_dir, path) = temp_db_path();
101        let conn = open_projection(&path).expect("open projection db");
102
103        let version = migrations::current_schema_version(&conn).expect("schema version query");
104        assert_eq!(version, migrations::LATEST_SCHEMA_VERSION);
105
106        let projection_version: i64 = conn
107            .query_row(
108                "SELECT schema_version FROM projection_meta WHERE id = 1",
109                [],
110                |row| row.get(0),
111            )
112            .expect("projection_meta schema version");
113        assert_eq!(
114            projection_version,
115            i64::from(migrations::LATEST_SCHEMA_VERSION)
116        );
117    }
118}