Skip to main content

keel_core/
sqlite_helpers.rs

1use rusqlite::params;
2
3use crate::sqlite::SqliteGraphStore;
4use crate::store::GraphStore;
5use crate::types::{GraphError, GraphNode};
6
7impl SqliteGraphStore {
8    /// Load circuit breaker state from the database.
9    pub fn load_circuit_breaker(&self) -> Result<Vec<(String, String, u32, bool)>, GraphError> {
10        let mut stmt = self.conn.prepare(
11            "SELECT error_code, hash, consecutive_failures, downgraded FROM circuit_breaker",
12        )?;
13        let rows = stmt
14            .query_map([], |row| {
15                Ok((
16                    row.get::<_, String>(0)?,
17                    row.get::<_, String>(1)?,
18                    row.get::<_, u32>(2)?,
19                    row.get::<_, i32>(3)? != 0,
20                ))
21            })?
22            .filter_map(|r| r.ok())
23            .collect();
24        Ok(rows)
25    }
26
27    /// Save circuit breaker state, replacing all existing rows.
28    pub fn save_circuit_breaker(
29        &self,
30        state: &[(String, String, u32, bool)],
31    ) -> Result<(), GraphError> {
32        self.conn.execute("DELETE FROM circuit_breaker", [])?;
33        let mut stmt = self.conn.prepare(
34            "INSERT INTO circuit_breaker (error_code, hash, consecutive_failures, downgraded) \
35             VALUES (?1, ?2, ?3, ?4)",
36        )?;
37        for (code, hash, consecutive, downgraded) in state {
38            stmt.execute(params![code, hash, consecutive, *downgraded as i32])?;
39        }
40        Ok(())
41    }
42
43    pub fn insert_node(&self, node: &GraphNode) -> Result<(), GraphError> {
44        self.conn.execute(
45            "INSERT INTO nodes (id, hash, kind, name, signature, file_path, line_start, line_end, docstring, is_public, type_hints_present, has_docstring, module_id)
46             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
47             ON CONFLICT(hash) DO UPDATE SET
48                kind = excluded.kind,
49                name = excluded.name,
50                signature = excluded.signature,
51                file_path = excluded.file_path,
52                line_start = excluded.line_start,
53                line_end = excluded.line_end,
54                docstring = excluded.docstring,
55                is_public = excluded.is_public,
56                type_hints_present = excluded.type_hints_present,
57                has_docstring = excluded.has_docstring,
58                module_id = excluded.module_id,
59                updated_at = datetime('now')",
60            params![
61                node.id,
62                node.hash,
63                node.kind.as_str(),
64                node.name,
65                node.signature,
66                node.file_path,
67                node.line_start,
68                node.line_end,
69                node.docstring,
70                node.is_public as i32,
71                node.type_hints_present as i32,
72                node.has_docstring as i32,
73                if node.module_id == 0 { None } else { Some(node.module_id) },
74            ],
75        )?;
76
77        // Clear old endpoints before re-inserting (UPSERT preserves the row,
78        // so CASCADE no longer cleans these up)
79        self.conn.execute(
80            "DELETE FROM external_endpoints WHERE node_id = ?1",
81            params![node.id],
82        )?;
83        for ep in &node.external_endpoints {
84            self.conn.execute(
85                "INSERT INTO external_endpoints (node_id, kind, method, path, direction) VALUES (?1, ?2, ?3, ?4, ?5)",
86                params![node.id, ep.kind, ep.method, ep.path, ep.direction],
87            )?;
88        }
89
90        // Insert previous hashes (PK constraint handles dedup)
91        for ph in &node.previous_hashes {
92            self.conn.execute(
93                "INSERT OR IGNORE INTO previous_hashes (node_id, hash) VALUES (?1, ?2)",
94                params![node.id, ph],
95            )?;
96        }
97
98        Ok(())
99    }
100
101    pub fn update_node_in_db(&self, node: &GraphNode) -> Result<(), GraphError> {
102        // Store old hash as previous hash
103        if let Some(old) = self.get_node_by_id(node.id) {
104            if old.hash != node.hash {
105                self.conn.execute(
106                    "INSERT OR IGNORE INTO previous_hashes (node_id, hash) VALUES (?1, ?2)",
107                    params![node.id, old.hash],
108                )?;
109                // Keep only last 3
110                self.conn.execute(
111                    "DELETE FROM previous_hashes WHERE node_id = ?1 AND hash NOT IN (SELECT hash FROM previous_hashes WHERE node_id = ?1 ORDER BY created_at DESC LIMIT 3)",
112                    params![node.id],
113                )?;
114            }
115        }
116
117        self.conn.execute(
118            "UPDATE nodes SET hash = ?1, kind = ?2, name = ?3, signature = ?4, file_path = ?5, line_start = ?6, line_end = ?7, docstring = ?8, is_public = ?9, type_hints_present = ?10, has_docstring = ?11, module_id = ?12, updated_at = datetime('now') WHERE id = ?13",
119            params![
120                node.hash,
121                node.kind.as_str(),
122                node.name,
123                node.signature,
124                node.file_path,
125                node.line_start,
126                node.line_end,
127                node.docstring,
128                node.is_public as i32,
129                node.type_hints_present as i32,
130                node.has_docstring as i32,
131                if node.module_id == 0 { None } else { Some(node.module_id) },
132                node.id,
133            ],
134        )?;
135
136        // Re-insert endpoints
137        self.conn
138            .execute("DELETE FROM external_endpoints WHERE node_id = ?1", params![node.id])?;
139        for ep in &node.external_endpoints {
140            self.conn.execute(
141                "INSERT INTO external_endpoints (node_id, kind, method, path, direction) VALUES (?1, ?2, ?3, ?4, ?5)",
142                params![node.id, ep.kind, ep.method, ep.path, ep.direction],
143            )?;
144        }
145
146        Ok(())
147    }
148}