1pub 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
21pub const DEFAULT_BUSY_TIMEOUT: Duration = Duration::from_secs(5);
23
24pub 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}