Skip to main content

bonds_core/manager/
storage.rs

1use super::*;
2use std::fs;
3
4/// Storage/open/read/schema concerns for BondManager.
5impl BondManager {
6    /// Open (or create) the DB at `db_path`. If None, defaults to `$HOME/.bonds/bonds.db`.
7    pub fn new(db_path: Option<PathBuf>) -> Result<Self, BondError> {
8        let db_path = db_path.unwrap_or_else(|| {
9            std::env::var("HOME")
10                .map(PathBuf::from)
11                .unwrap_or_else(|_| PathBuf::from("."))
12                .join(".bonds")
13                .join("bonds.db")
14        });
15
16        if let Some(parent) = db_path.parent() {
17            fs::create_dir_all(parent)?;
18        }
19
20        let conn = Connection::open(db_path)?;
21        Self::from_connection(conn)
22    }
23
24    /// List all bonds (most-recent first).
25    pub fn list_bonds(&self) -> Result<Vec<Bond>, BondError> {
26        let mut stmt = self.conn.prepare(
27            "SELECT id, name, source, target, created_at, metadata FROM bonds ORDER BY created_at DESC",
28        )?;
29        let mut rows = stmt.query([])?;
30
31        let mut out = Vec::new();
32        while let Some(row) = rows.next()? {
33            out.push(self.bond_from_row(row)?);
34        }
35        Ok(out)
36    }
37
38    /// Get a single bond by ID or name. ID can be a unique prefix.
39    pub fn get_bond(&self, identifier: &str) -> Result<Bond, BondError> {
40        // 1) Exact name
41        let mut stmt = self.conn.prepare(
42            "SELECT id, name, source, target, created_at, metadata FROM bonds WHERE name = ?1",
43        )?;
44        let mut rows = stmt.query(params![identifier])?;
45        if let Some(row) = rows.next()? {
46            return self.bond_from_row(row);
47        }
48        drop(rows);
49        drop(stmt);
50
51        // 2) ID prefix
52        let mut stmt = self.conn.prepare(
53            "SELECT id, name, source, target, created_at, metadata FROM bonds WHERE id LIKE ?1 || '%'",
54        )?;
55        let mut rows = stmt.query(params![identifier])?;
56
57        let first = match rows.next()? {
58            Some(row) => self.bond_from_row(row)?,
59            None => return Err(BondError::NotFound(identifier.to_string())),
60        };
61
62        if rows.next()?.is_some() {
63            return Err(BondError::AmbiguousId(identifier.to_string()));
64        }
65
66        Ok(first)
67    }
68
69    /// Parse a Bond from a rusqlite row.
70    fn bond_from_row(&self, row: &rusqlite::Row) -> Result<Bond, BondError> {
71        let id: String = row.get(0)?;
72        let name: Option<String> = row.get(1)?;
73        let source: String = row.get(2)?;
74        let target: String = row.get(3)?;
75        let created_at_str: String = row.get(4)?;
76        let metadata_json: Option<String> = row.get(5)?;
77
78        let created_at = DateTime::parse_from_rfc3339(&created_at_str)
79            .map(|dt| dt.with_timezone(&Utc))
80            .map_err(|e| BondError::InvalidTimestamp(e.to_string()))?;
81
82        let metadata = match metadata_json {
83            Some(s) => Some(serde_json::from_str(&s)?),
84            None => None,
85        };
86
87        Ok(Bond {
88            id,
89            name,
90            source: PathBuf::from(source),
91            target: PathBuf::from(target),
92            created_at,
93            metadata,
94        })
95    }
96
97    /// Runs schema migration. Useful for testing with in-memory DBs.
98    pub(crate) fn from_connection(conn: Connection) -> Result<Self, BondError> {
99        conn.execute_batch(
100            "CREATE TABLE IF NOT EXISTS bonds (
101                id TEXT PRIMARY KEY,
102                name TEXT,
103                source TEXT NOT NULL,
104                target TEXT NOT NULL,
105                created_at TEXT NOT NULL,
106                metadata TEXT
107            );",
108        )?;
109
110        // Safe migrations for older DBs.
111        let _ = conn.execute_batch("ALTER TABLE bonds ADD COLUMN name TEXT;");
112        let _ = conn.execute_batch("ALTER TABLE bonds ADD COLUMN metadata TEXT;");
113
114        Ok(Self {
115            conn,
116            hooks: RwLock::new(Vec::new()),
117        })
118    }
119}