Skip to main content

tessera_codegraph/
engine.rs

1//! Graph-engine abstraction. The default backend is SQLite (the one used
2//! everywhere in the codebase). A feature-gated `cozo` backend mirrors the
3//! call edges into an embedded Datalog database so impact queries can be
4//! expressed as a recursive Datalog rule.
5//!
6//! The trait is intentionally small: it only covers the call-graph traversal
7//! that benefits from a different algebra. Symbol lookup, references, and
8//! outlines all stay on SQLite — they're already fast there.
9
10use std::path::Path;
11
12use anyhow::Result;
13use rusqlite::Connection;
14
15use crate::types::GraphEngineKind;
16
17pub trait GraphEngine {
18    /// Return caller symbol IDs that transitively reach `target` within
19    /// `depth` hops (depth 1 = direct callers).
20    fn transitive_callers(&self, target: &str, depth: usize) -> Result<Vec<(i64, usize)>>;
21
22    fn kind(&self) -> GraphEngineKind;
23}
24
25pub struct SqliteEngine<'a> {
26    pub conn: &'a Connection,
27}
28
29impl<'a> SqliteEngine<'a> {
30    pub fn new(conn: &'a Connection) -> Self {
31        Self { conn }
32    }
33}
34
35impl<'a> GraphEngine for SqliteEngine<'a> {
36    fn transitive_callers(&self, target: &str, depth: usize) -> Result<Vec<(i64, usize)>> {
37        use std::collections::{HashSet, VecDeque};
38        let mut out: Vec<(i64, usize)> = Vec::new();
39        let mut seen: HashSet<i64> = HashSet::new();
40        let mut queue: VecDeque<(String, usize)> = VecDeque::new();
41        queue.push_back((target.to_string(), 1));
42
43        let mut stmt = self.conn.prepare(
44            "
45            SELECT DISTINCT s.id, s.name, s.qualified_name
46            FROM edges e
47            JOIN symbols s ON s.id = e.from_symbol_id
48            WHERE e.to_symbol_name = ?1 OR e.to_symbol_name LIKE ?2
49            LIMIT 500
50            ",
51        )?;
52
53        while let Some((current, current_depth)) = queue.pop_front() {
54            if current_depth > depth {
55                continue;
56            }
57            let like = format!("%.{}", current);
58            let rows = stmt
59                .query_map(rusqlite::params![current, like], |row| {
60                    let id: i64 = row.get(0)?;
61                    let name: String = row.get(1)?;
62                    let qname: String = row.get(2)?;
63                    Ok((id, name, qname))
64                })?
65                .collect::<rusqlite::Result<Vec<_>>>()?;
66
67            for (id, name, qname) in rows {
68                if !seen.insert(id) {
69                    continue;
70                }
71                out.push((id, current_depth));
72                if current_depth < depth {
73                    queue.push_back((name, current_depth + 1));
74                    if qname != current {
75                        queue.push_back((qname, current_depth + 1));
76                    }
77                }
78            }
79        }
80        Ok(out)
81    }
82
83    fn kind(&self) -> GraphEngineKind {
84        GraphEngineKind::Sqlite
85    }
86}
87
88/// Open the requested engine. The `cozo` variant returns an error unless the
89/// `cozo` Cargo feature is enabled. We deliberately do **not** silently fall
90/// back to SQLite — if the user asked for Cozo, they should see the gap.
91pub fn select_engine<'a>(
92    kind: GraphEngineKind,
93    conn: &'a Connection,
94    _db_path: &Path,
95) -> Result<Box<dyn GraphEngine + 'a>> {
96    match kind {
97        GraphEngineKind::Sqlite => Ok(Box::new(SqliteEngine::new(conn))),
98        GraphEngineKind::Cozo => cozo_engine(conn, _db_path),
99    }
100}
101
102fn cozo_engine<'a>(_conn: &'a Connection, _db_path: &Path) -> Result<Box<dyn GraphEngine + 'a>> {
103    Err(anyhow::anyhow!(
104        "Cozo backend is a v0.3 deliverable — the GraphEngine trait is in place \
105         and the call-edge relation is documented, but the embedded Cozo dependency \
106         is deliberately not wired up in v0.2. Use the default SQLite backend."
107    ))
108}