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    /// Search for nodes whose name contains the given substring (case-insensitive).
44    /// Single SQL query instead of iterating modules + per-file lookups (N+1).
45    pub fn search_nodes(
46        &self,
47        query: &str,
48        kind_filter: Option<&str>,
49        limit: usize,
50    ) -> Vec<GraphNode> {
51        let pattern = format!("%{}%", query);
52        let sql = match kind_filter {
53            Some(_) => {
54                "SELECT id, hash, kind, name, signature, file_path, line_start, line_end, \
55                 docstring, is_public, type_hints_present, has_docstring, module_id \
56                 FROM nodes WHERE LOWER(name) LIKE LOWER(?1) AND kind = ?2 \
57                 ORDER BY name LIMIT ?3"
58            }
59            None => {
60                "SELECT id, hash, kind, name, signature, file_path, line_start, line_end, \
61                 docstring, is_public, type_hints_present, has_docstring, module_id \
62                 FROM nodes WHERE LOWER(name) LIKE LOWER(?1) \
63                 ORDER BY name LIMIT ?2"
64            }
65        };
66
67        let result = match kind_filter {
68            Some(kind) => {
69                let mut stmt = match self.conn.prepare(sql) {
70                    Ok(s) => s,
71                    Err(_) => return vec![],
72                };
73                stmt.query_map(params![pattern, kind, limit as u32], Self::row_to_node)
74                    .ok()
75                    .map(|rows| rows.filter_map(|r| r.ok()).collect())
76                    .unwrap_or_default()
77            }
78            None => {
79                let mut stmt = match self.conn.prepare(sql) {
80                    Ok(s) => s,
81                    Err(_) => return vec![],
82                };
83                stmt.query_map(params![pattern, limit as u32], Self::row_to_node)
84                    .ok()
85                    .map(|rows| rows.filter_map(|r| r.ok()).collect())
86                    .unwrap_or_default()
87            }
88        };
89        result
90    }
91
92    pub fn insert_node(&self, node: &GraphNode) -> Result<(), GraphError> {
93        self.conn.execute(
94            "INSERT INTO nodes (id, hash, kind, name, signature, file_path, line_start, line_end, docstring, is_public, type_hints_present, has_docstring, module_id)
95             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
96             ON CONFLICT(hash) DO UPDATE SET
97                kind = excluded.kind,
98                name = excluded.name,
99                signature = excluded.signature,
100                file_path = excluded.file_path,
101                line_start = excluded.line_start,
102                line_end = excluded.line_end,
103                docstring = excluded.docstring,
104                is_public = excluded.is_public,
105                type_hints_present = excluded.type_hints_present,
106                has_docstring = excluded.has_docstring,
107                module_id = excluded.module_id,
108                updated_at = datetime('now')",
109            params![
110                node.id,
111                node.hash,
112                node.kind.as_str(),
113                node.name,
114                node.signature,
115                node.file_path,
116                node.line_start,
117                node.line_end,
118                node.docstring,
119                node.is_public as i32,
120                node.type_hints_present as i32,
121                node.has_docstring as i32,
122                if node.module_id == 0 { None } else { Some(node.module_id) },
123            ],
124        )?;
125
126        // Clear old endpoints before re-inserting (UPSERT preserves the row,
127        // so CASCADE no longer cleans these up)
128        self.conn.execute(
129            "DELETE FROM external_endpoints WHERE node_id = ?1",
130            params![node.id],
131        )?;
132        for ep in &node.external_endpoints {
133            self.conn.execute(
134                "INSERT INTO external_endpoints (node_id, kind, method, path, direction) VALUES (?1, ?2, ?3, ?4, ?5)",
135                params![node.id, ep.kind, ep.method, ep.path, ep.direction],
136            )?;
137        }
138
139        // Insert previous hashes (PK constraint handles dedup)
140        for ph in &node.previous_hashes {
141            self.conn.execute(
142                "INSERT OR IGNORE INTO previous_hashes (node_id, hash) VALUES (?1, ?2)",
143                params![node.id, ph],
144            )?;
145        }
146
147        Ok(())
148    }
149
150    pub fn update_node_in_db(&self, node: &GraphNode) -> Result<(), GraphError> {
151        // Store old hash as previous hash
152        if let Some(old) = self.get_node_by_id(node.id) {
153            if old.hash != node.hash {
154                self.conn.execute(
155                    "INSERT OR IGNORE INTO previous_hashes (node_id, hash) VALUES (?1, ?2)",
156                    params![node.id, old.hash],
157                )?;
158                // Keep only last 3
159                self.conn.execute(
160                    "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)",
161                    params![node.id],
162                )?;
163            }
164        }
165
166        self.conn.execute(
167            "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",
168            params![
169                node.hash,
170                node.kind.as_str(),
171                node.name,
172                node.signature,
173                node.file_path,
174                node.line_start,
175                node.line_end,
176                node.docstring,
177                node.is_public as i32,
178                node.type_hints_present as i32,
179                node.has_docstring as i32,
180                if node.module_id == 0 { None } else { Some(node.module_id) },
181                node.id,
182            ],
183        )?;
184
185        // Re-insert endpoints
186        self.conn.execute(
187            "DELETE FROM external_endpoints WHERE node_id = ?1",
188            params![node.id],
189        )?;
190        for ep in &node.external_endpoints {
191            self.conn.execute(
192                "INSERT INTO external_endpoints (node_id, kind, method, path, direction) VALUES (?1, ?2, ?3, ?4, ?5)",
193                params![node.id, ep.kind, ep.method, ep.path, ep.direction],
194            )?;
195        }
196
197        Ok(())
198    }
199}