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    /// Insert a node into the database, or update it on hash conflict (upsert).
93    pub fn insert_node(&self, node: &GraphNode) -> Result<(), GraphError> {
94        self.conn.execute(
95            "INSERT INTO nodes (id, hash, kind, name, signature, file_path, line_start, line_end, docstring, is_public, type_hints_present, has_docstring, module_id)
96             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
97             ON CONFLICT(hash) DO UPDATE SET
98                kind = excluded.kind,
99                name = excluded.name,
100                signature = excluded.signature,
101                file_path = excluded.file_path,
102                line_start = excluded.line_start,
103                line_end = excluded.line_end,
104                docstring = excluded.docstring,
105                is_public = excluded.is_public,
106                type_hints_present = excluded.type_hints_present,
107                has_docstring = excluded.has_docstring,
108                module_id = excluded.module_id,
109                updated_at = datetime('now')",
110            params![
111                node.id,
112                node.hash,
113                node.kind.as_str(),
114                node.name,
115                node.signature,
116                node.file_path,
117                node.line_start,
118                node.line_end,
119                node.docstring,
120                node.is_public as i32,
121                node.type_hints_present as i32,
122                node.has_docstring as i32,
123                if node.module_id == 0 { None } else { Some(node.module_id) },
124            ],
125        )?;
126
127        // Clear old endpoints before re-inserting (UPSERT preserves the row,
128        // so CASCADE no longer cleans these up)
129        self.conn.execute(
130            "DELETE FROM external_endpoints WHERE node_id = ?1",
131            params![node.id],
132        )?;
133        for ep in &node.external_endpoints {
134            self.conn.execute(
135                "INSERT INTO external_endpoints (node_id, kind, method, path, direction) VALUES (?1, ?2, ?3, ?4, ?5)",
136                params![node.id, ep.kind, ep.method, ep.path, ep.direction],
137            )?;
138        }
139
140        // Insert previous hashes (PK constraint handles dedup)
141        for ph in &node.previous_hashes {
142            self.conn.execute(
143                "INSERT OR IGNORE INTO previous_hashes (node_id, hash) VALUES (?1, ?2)",
144                params![node.id, ph],
145            )?;
146        }
147
148        Ok(())
149    }
150
151    /// Update an existing node by ID, preserving the old hash in previous_hashes.
152    pub fn update_node_in_db(&self, node: &GraphNode) -> Result<(), GraphError> {
153        // Store old hash as previous hash
154        if let Some(old) = self.get_node_by_id(node.id) {
155            if old.hash != node.hash {
156                self.conn.execute(
157                    "INSERT OR IGNORE INTO previous_hashes (node_id, hash) VALUES (?1, ?2)",
158                    params![node.id, old.hash],
159                )?;
160                // Keep only last 3
161                self.conn.execute(
162                    "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)",
163                    params![node.id],
164                )?;
165            }
166        }
167
168        self.conn.execute(
169            "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",
170            params![
171                node.hash,
172                node.kind.as_str(),
173                node.name,
174                node.signature,
175                node.file_path,
176                node.line_start,
177                node.line_end,
178                node.docstring,
179                node.is_public as i32,
180                node.type_hints_present as i32,
181                node.has_docstring as i32,
182                if node.module_id == 0 { None } else { Some(node.module_id) },
183                node.id,
184            ],
185        )?;
186
187        // Re-insert endpoints
188        self.conn.execute(
189            "DELETE FROM external_endpoints WHERE node_id = ?1",
190            params![node.id],
191        )?;
192        for ep in &node.external_endpoints {
193            self.conn.execute(
194                "INSERT INTO external_endpoints (node_id, kind, method, path, direction) VALUES (?1, ?2, ?3, ?4, ?5)",
195                params![node.id, ep.kind, ep.method, ep.path, ep.direction],
196            )?;
197        }
198
199        Ok(())
200    }
201}