task_graph_mcp/db/mod.rs
1//! Database layer for the Task Graph MCP Server.
2//!
3//! # Crash Safety
4//!
5//! This module uses mutex recovery to prevent cascading failures. If a thread
6//! panics while holding the database mutex, subsequent operations will recover
7//! the connection rather than panicking on poison errors.
8//!
9//! ## Known TODO items for improved robustness:
10//!
11//! - `src/db/tasks.rs:612,659` - `current_owner.unwrap()` could panic on inconsistent state
12//! - `src/tools/tasks.rs:650` - `serde_json::to_value().unwrap()` could panic
13//! - `src/tools/tracking.rs:34,215,338` - various unwraps on date/option handling
14//! - `src/tools/attachments.rs:247,250` - `content.unwrap()` assumes content is Some
15//! - `src/db/migrations.rs:323,556` - `expect()` on migration path validation
16
17pub mod agents;
18pub mod attachments;
19pub mod dashboard;
20pub mod deps;
21pub mod export;
22pub mod import;
23pub mod locks;
24pub mod schema;
25pub mod search;
26pub mod state_transitions;
27pub mod stats;
28pub mod tasks;
29
30pub use deps::AddDependencyResult;
31pub use search::{AttachmentMatch, SearchResult};
32
33use anyhow::Result;
34use rusqlite::Connection;
35use std::path::Path;
36use std::sync::{Arc, Mutex};
37
38mod embedded {
39 use refinery::embed_migrations;
40 embed_migrations!("migrations");
41}
42
43/// Database handle wrapping a SQLite connection.
44#[derive(Clone)]
45pub struct Database {
46 conn: Arc<Mutex<Connection>>,
47}
48
49impl Database {
50 /// Open or create the database at the given path.
51 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
52 let conn = Connection::open(path)?;
53
54 // Enable WAL mode for concurrent access
55 conn.execute_batch(
56 "PRAGMA journal_mode=WAL;
57 PRAGMA foreign_keys=ON;
58 PRAGMA busy_timeout=5000;",
59 )?;
60
61 let db = Self {
62 conn: Arc::new(Mutex::new(conn)),
63 };
64
65 db.run_migrations()?;
66
67 Ok(db)
68 }
69
70 /// Open an in-memory database (for testing).
71 #[allow(dead_code)]
72 pub fn open_in_memory() -> Result<Self> {
73 let conn = Connection::open_in_memory()?;
74
75 conn.execute_batch("PRAGMA foreign_keys=ON;")?;
76
77 let db = Self {
78 conn: Arc::new(Mutex::new(conn)),
79 };
80
81 db.run_migrations()?;
82
83 Ok(db)
84 }
85
86 /// Run database migrations.
87 fn run_migrations(&self) -> Result<()> {
88 // Recover from poisoned mutex to prevent cascading failures
89 let mut conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
90 embedded::migrations::runner().run(&mut *conn)?;
91 Ok(())
92 }
93
94 /// Execute a function with exclusive access to the connection.
95 ///
96 /// Recovers from poisoned mutex to prevent cascading failures if another
97 /// thread panicked while holding the lock.
98 pub fn with_conn<F, T>(&self, f: F) -> Result<T>
99 where
100 F: FnOnce(&Connection) -> Result<T>,
101 {
102 let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
103 f(&conn)
104 }
105
106 /// Execute a function with mutable access to the connection (for transactions).
107 ///
108 /// Recovers from poisoned mutex to prevent cascading failures if another
109 /// thread panicked while holding the lock.
110 pub fn with_conn_mut<F, T>(&self, f: F) -> Result<T>
111 where
112 F: FnOnce(&mut Connection) -> Result<T>,
113 {
114 let mut conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
115 f(&mut conn)
116 }
117}
118
119/// Get the current timestamp in milliseconds.
120pub fn now_ms() -> i64 {
121 chrono::Utc::now().timestamp_millis()
122}