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").map_err(db_err)?;
44        // WAL is unsupported for in-memory databases; ignore that case.
45        let _ = conn.pragma_update(None, "journal_mode", "WAL");
46        conn.pragma_update(None, "synchronous", "NORMAL").map_err(db_err)?;
47        conn.pragma_update(None, "temp_store", "MEMORY").map_err(db_err)?;
48
49        let catalog = Self {
50            conn: Mutex::new(conn),
51            path,
52        };
53        migrations::run_pending(&catalog)?;
54        Ok(catalog)
55    }
56
57    /// Acquire the serialized connection. Repositories use this; the
58    /// guard scope is kept short.
59    pub fn lock(&self) -> MutexGuard<'_, Connection> {
60        self.conn
61            .lock()
62            .expect("catalog connection mutex poisoned — a repository panicked mid-write")
63    }
64
65    /// Path of the catalog database file.
66    pub fn path(&self) -> &Path {
67        &self.path
68    }
69
70    /// Current schema version (0 when no migration has been applied).
71    pub fn schema_version(&self) -> OrbokResult<i64> {
72        let conn = self.lock();
73        let version = conn
74            .query_row(
75                "SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
76                [],
77                |row| row.get(0),
78            )
79            .map_err(db_err)?;
80        Ok(version)
81    }
82}
83
84/// Map a rusqlite error to the typed orbok error.
85pub(crate) fn db_err(e: rusqlite::Error) -> OrbokError {
86    OrbokError::Database(e.to_string())
87}