Skip to main content

tga_core/db/
mod.rs

1//! SQLite database access layer.
2//!
3//! All databases opened by this crate are configured with:
4//! - `journal_mode = WAL` — concurrent reads during write-heavy collection
5//! - `synchronous = NORMAL` — durability with reasonable performance
6//! - `foreign_keys = ON` — enforce FK constraints
7//!
8//! WAL mode is **mandatory** per project conventions and is set on every
9//! [`Database::open`] call.
10
11use std::path::Path;
12
13use rusqlite::Connection;
14use tracing::{debug, info};
15
16use crate::config::expand_path;
17use crate::errors::{Result, TgaError};
18
19pub mod migrations;
20
21/// Wrapper around a [`rusqlite::Connection`] with project-standard pragmas
22/// applied and migrations run.
23pub struct Database {
24    conn: Connection,
25}
26
27impl Database {
28    /// Open or create a SQLite database at `path`, apply pragmas, and run
29    /// any pending migrations.
30    ///
31    /// Tilde-expansion is applied to `path`.
32    ///
33    /// # Errors
34    ///
35    /// - [`TgaError::DbError`] for SQLite-level failures.
36    /// - [`TgaError::MigrationError`] if a migration fails.
37    pub fn open(path: &Path) -> Result<Database> {
38        let resolved = expand_path(path);
39        debug!(path = %resolved.display(), "opening database");
40        let conn = Connection::open(&resolved)?;
41        Self::apply_pragmas(&conn)?;
42        let mut db = Database { conn };
43        migrations::run(&mut db.conn)?;
44        info!(path = %resolved.display(), "database ready");
45        Ok(db)
46    }
47
48    /// Open an in-memory database. Primarily intended for tests.
49    ///
50    /// # Errors
51    ///
52    /// See [`Database::open`].
53    pub fn open_in_memory() -> Result<Database> {
54        let conn = Connection::open_in_memory()?;
55        Self::apply_pragmas(&conn)?;
56        let mut db = Database { conn };
57        migrations::run(&mut db.conn)?;
58        Ok(db)
59    }
60
61    /// Apply the canonical pragma set: WAL journal, normal sync, FK enforcement.
62    fn apply_pragmas(conn: &Connection) -> Result<()> {
63        // `journal_mode` is a query-style pragma; use query_row to honor it.
64        let mode: String = conn
65            .query_row("PRAGMA journal_mode=WAL", [], |row| row.get(0))
66            .map_err(TgaError::from)?;
67        debug!(journal_mode = %mode, "applied WAL pragma");
68        conn.execute_batch("PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON;")?;
69        Ok(())
70    }
71
72    /// Borrow the underlying connection (read-only).
73    pub fn connection(&self) -> &Connection {
74        &self.conn
75    }
76
77    /// Borrow the underlying connection mutably.
78    pub fn connection_mut(&mut self) -> &mut Connection {
79        &mut self.conn
80    }
81
82    /// Return the active journal mode (e.g. `"wal"` or `"memory"`).
83    ///
84    /// # Errors
85    ///
86    /// Returns [`TgaError::DbError`] if the pragma query fails.
87    pub fn journal_mode(&self) -> Result<String> {
88        let mode: String = self
89            .conn
90            .query_row("PRAGMA journal_mode", [], |row| row.get(0))
91            .map_err(TgaError::from)?;
92        Ok(mode)
93    }
94
95    /// Return the highest applied migration version.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`TgaError::DbError`] if the query fails.
100    pub fn schema_version(&self) -> Result<i64> {
101        let v: i64 = self
102            .conn
103            .query_row(
104                "SELECT COALESCE(MAX(version), 0) FROM schema_migrations",
105                [],
106                |row| row.get(0),
107            )
108            .map_err(TgaError::from)?;
109        Ok(v)
110    }
111}