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}