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;
29pub mod template;
30
31pub use deps::AddDependencyResult;
32pub use search::{AttachmentMatch, SearchResult};
33
34use anyhow::Result;
35use rusqlite::Connection;
36use std::path::Path;
37use std::sync::{Arc, Mutex};
38
39mod embedded {
40    use refinery::embed_migrations;
41    embed_migrations!("migrations");
42}
43
44/// Database handle wrapping a SQLite connection.
45#[derive(Clone)]
46pub struct Database {
47    conn: Arc<Mutex<Connection>>,
48}
49
50impl Database {
51    /// Open or create the database at the given path.
52    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
53        let conn = Connection::open(path)?;
54
55        // Enable WAL mode for concurrent access
56        conn.execute_batch(
57            "PRAGMA journal_mode=WAL;
58             PRAGMA foreign_keys=ON;
59             PRAGMA busy_timeout=5000;",
60        )?;
61
62        let db = Self {
63            conn: Arc::new(Mutex::new(conn)),
64        };
65
66        db.run_migrations()?;
67
68        Ok(db)
69    }
70
71    /// Open an in-memory database (for testing).
72    #[allow(dead_code)]
73    pub fn open_in_memory() -> Result<Self> {
74        let conn = Connection::open_in_memory()?;
75
76        conn.execute_batch("PRAGMA foreign_keys=ON;")?;
77
78        let db = Self {
79            conn: Arc::new(Mutex::new(conn)),
80        };
81
82        db.run_migrations()?;
83
84        Ok(db)
85    }
86
87    /// Run database migrations.
88    fn run_migrations(&self) -> Result<()> {
89        // Recover from poisoned mutex to prevent cascading failures
90        let mut conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
91        embedded::migrations::runner().run(&mut *conn)?;
92        Ok(())
93    }
94
95    /// Execute a function with exclusive access to the connection.
96    ///
97    /// Recovers from poisoned mutex to prevent cascading failures if another
98    /// thread panicked while holding the lock.
99    pub fn with_conn<F, T>(&self, f: F) -> Result<T>
100    where
101        F: FnOnce(&Connection) -> Result<T>,
102    {
103        let conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
104        f(&conn)
105    }
106
107    /// Execute a function with mutable access to the connection (for transactions).
108    ///
109    /// Recovers from poisoned mutex to prevent cascading failures if another
110    /// thread panicked while holding the lock.
111    pub fn with_conn_mut<F, T>(&self, f: F) -> Result<T>
112    where
113        F: FnOnce(&mut Connection) -> Result<T>,
114    {
115        let mut conn = self.conn.lock().unwrap_or_else(|e| e.into_inner());
116        f(&mut conn)
117    }
118}
119
120/// Get the current timestamp in milliseconds.
121pub fn now_ms() -> i64 {
122    chrono::Utc::now().timestamp_millis()
123}