Skip to main content

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}