Skip to main content

orbok_db/
catalog.rs

1//! Catalog connection management (RFC-002 §5).
2//!
3//! The catalog is opened with foreign keys ON, WAL journal, NORMAL
4//! synchronous, and in-memory temp store. Writes are serialized through
5//! a single mutex-guarded connection (RFC-002 §5 "one serialized writer
6//! path"); v1 keeps reads on the same connection for simplicity.
7
8use crate::migrations;
9use orbok_core::{OrbokError, OrbokResult};
10use rusqlite::Connection;
11use std::path::{Path, PathBuf};
12use std::sync::{Mutex, MutexGuard};
13
14/// File name of the authoritative catalog database (Appendix A §3).
15pub const CATALOG_FILE_NAME: &str = "orbok-catalog.sqlite3";
16
17/// File name of the localcache-managed payload database (Appendix A §3).
18pub const CACHE_FILE_NAME: &str = "orbok-cache.sqlite3";
19
20/// The authoritative orbok catalog.
21pub struct Catalog {
22    conn: Mutex<Connection>,
23    path: PathBuf,
24}
25
26impl Catalog {
27    /// Open (or create) the catalog at `path`, apply pragmas, and run
28    /// pending migrations. Migration failure aborts startup (RFC-002
29    /// §6.2).
30    pub fn open(path: impl AsRef<Path>) -> OrbokResult<Self> {
31        let path = path.as_ref().to_path_buf();
32        let conn = Connection::open(&path).map_err(db_err)?;
33        Self::from_connection(conn, path)
34    }
35
36    /// Open an in-memory catalog (tests).
37    pub fn open_in_memory() -> OrbokResult<Self> {
38        let conn = Connection::open_in_memory().map_err(db_err)?;
39        Self::from_connection(conn, PathBuf::from(":memory:"))
40    }
41
42    fn from_connection(conn: Connection, path: PathBuf) -> OrbokResult<Self> {
43        conn.pragma_update(None, "foreign_keys", "ON")
44            .map_err(db_err)?;
45        // WAL is unsupported for in-memory databases; ignore that case.
46        let _ = conn.pragma_update(None, "journal_mode", "WAL");
47        conn.pragma_update(None, "synchronous", "NORMAL")
48            .map_err(db_err)?;
49        conn.pragma_update(None, "temp_store", "MEMORY")
50            .map_err(db_err)?;
51
52        let catalog = Self {
53            conn: Mutex::new(conn),
54            path,
55        };
56        migrations::run_pending(&catalog)?;
57        Ok(catalog)
58    }
59
60    /// Acquire the serialized connection. Repositories use this; the
61    /// guard scope is kept short.
62    pub fn lock(&self) -> MutexGuard<'_, Connection> {
63        self.conn
64            .lock()
65            .expect("catalog connection mutex poisoned — a repository panicked mid-write")
66    }
67
68    /// Path of the catalog database file.
69    pub fn path(&self) -> &Path {
70        &self.path
71    }
72
73    /// Current schema version (0 when no migration has been applied).
74    pub fn schema_version(&self) -> OrbokResult<i64> {
75        let conn = self.lock();
76        let version = conn
77            .query_row(
78                "SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
79                [],
80                |row| row.get(0),
81            )
82            .map_err(db_err)?;
83        Ok(version)
84    }
85}
86
87/// Map a rusqlite error to the typed orbok error.
88pub(crate) fn db_err(e: rusqlite::Error) -> OrbokError {
89    OrbokError::Database(e.to_string())
90}