Skip to main content

lean_ctx/core/property_graph/
edge.rs

1//! Edge types and CRUD operations for graph edges.
2
3use rusqlite::{Connection, params};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum EdgeKind {
7    Imports,
8    Calls,
9    Defines,
10    Exports,
11    TypeRef,
12    TestedBy,
13    ChangedIn,
14    BuiltIn,
15    MentionedIn,
16    Affects,
17    Breaks,
18    /// Implicit module/package/re-export relationship (from graph_index)
19    Module,
20    /// Git co-change correlation (files frequently changed together)
21    Cochange,
22    /// Implementation → interface/trait it implements (semantic backend)
23    Implements,
24    /// Subtype → supertype it extends (semantic backend, type hierarchy)
25    Extends,
26    /// File → file whose symbols it uses (semantic backend `references`,
27    /// verified on demand by `ctx_impact`)
28    References,
29    /// Sibling/orphan rescue edge (fallback connectivity)
30    Sibling,
31}
32
33impl EdgeKind {
34    pub fn as_str(&self) -> &'static str {
35        match self {
36            Self::Imports => "imports",
37            Self::Calls => "calls",
38            Self::Defines => "defines",
39            Self::Exports => "exports",
40            Self::TypeRef => "type_ref",
41            Self::TestedBy => "tested_by",
42            Self::ChangedIn => "changed_in",
43            Self::BuiltIn => "built_in",
44            Self::MentionedIn => "mentioned_in",
45            Self::Affects => "affects",
46            Self::Breaks => "breaks",
47            Self::Module => "module",
48            Self::Cochange => "cochange",
49            Self::Sibling => "sibling",
50            Self::Implements => "implements",
51            Self::Extends => "extends",
52            Self::References => "references",
53        }
54    }
55
56    pub fn parse(s: &str) -> Self {
57        match s {
58            "calls" => Self::Calls,
59            "defines" => Self::Defines,
60            "exports" => Self::Exports,
61            "type_ref" => Self::TypeRef,
62            "tested_by" => Self::TestedBy,
63            "changed_in" => Self::ChangedIn,
64            "built_in" => Self::BuiltIn,
65            "mentioned_in" => Self::MentionedIn,
66            "affects" => Self::Affects,
67            "breaks" => Self::Breaks,
68            "module" => Self::Module,
69            "cochange" => Self::Cochange,
70            "sibling" => Self::Sibling,
71            "implements" => Self::Implements,
72            "extends" => Self::Extends,
73            "references" => Self::References,
74            _ => Self::Imports,
75        }
76    }
77}
78
79#[derive(Debug, Clone)]
80pub struct Edge {
81    pub id: Option<i64>,
82    pub source_id: i64,
83    pub target_id: i64,
84    pub kind: EdgeKind,
85    pub metadata: Option<String>,
86}
87
88impl Edge {
89    pub fn new(source_id: i64, target_id: i64, kind: EdgeKind) -> Self {
90        Self {
91            id: None,
92            source_id,
93            target_id,
94            kind,
95            metadata: None,
96        }
97    }
98
99    pub fn with_metadata(mut self, meta: &str) -> Self {
100        self.metadata = Some(meta.to_string());
101        self
102    }
103}
104
105pub(super) fn upsert(conn: &Connection, edge: &Edge) -> anyhow::Result<()> {
106    conn.execute(
107        "INSERT INTO edges (source_id, target_id, kind, metadata)
108         VALUES (?1, ?2, ?3, ?4)
109         ON CONFLICT(source_id, target_id, kind) DO UPDATE SET
110            metadata = excluded.metadata",
111        params![
112            edge.source_id,
113            edge.target_id,
114            edge.kind.as_str(),
115            edge.metadata,
116        ],
117    )?;
118    Ok(())
119}
120
121pub(super) fn from_node(conn: &Connection, node_id: i64) -> anyhow::Result<Vec<Edge>> {
122    let mut stmt = conn.prepare(
123        "SELECT id, source_id, target_id, kind, metadata
124         FROM edges WHERE source_id = ?1",
125    )?;
126    let edges = stmt
127        .query_map(params![node_id], |row| {
128            Ok(Edge {
129                id: Some(row.get(0)?),
130                source_id: row.get(1)?,
131                target_id: row.get(2)?,
132                kind: EdgeKind::parse(&row.get::<_, String>(3)?),
133                metadata: row.get(4)?,
134            })
135        })?
136        .filter_map(std::result::Result::ok)
137        .collect();
138    Ok(edges)
139}
140
141pub(super) fn to_node(conn: &Connection, node_id: i64) -> anyhow::Result<Vec<Edge>> {
142    let mut stmt = conn.prepare(
143        "SELECT id, source_id, target_id, kind, metadata
144         FROM edges WHERE target_id = ?1",
145    )?;
146    let edges = stmt
147        .query_map(params![node_id], |row| {
148            Ok(Edge {
149                id: Some(row.get(0)?),
150                source_id: row.get(1)?,
151                target_id: row.get(2)?,
152                kind: EdgeKind::parse(&row.get::<_, String>(3)?),
153                metadata: row.get(4)?,
154            })
155        })?
156        .filter_map(std::result::Result::ok)
157        .collect();
158    Ok(edges)
159}
160
161pub(super) fn count(conn: &Connection) -> anyhow::Result<usize> {
162    let c: i64 = conn.query_row("SELECT COUNT(*) FROM edges", [], |row| row.get(0))?;
163    Ok(c as usize)
164}
165
166pub(super) fn metadata_of(
167    conn: &Connection,
168    source_id: i64,
169    target_id: i64,
170    kind: &EdgeKind,
171) -> anyhow::Result<Option<String>> {
172    use rusqlite::OptionalExtension;
173    // `None` for both "no such edge" and "edge without metadata".
174    Ok(conn
175        .query_row(
176            "SELECT metadata FROM edges WHERE source_id = ?1 AND target_id = ?2 AND kind = ?3",
177            params![source_id, target_id, kind.as_str()],
178            |r| r.get::<_, Option<String>>(0),
179        )
180        .optional()?
181        .flatten())
182}
183
184pub(super) fn file_edges_of_kind(
185    conn: &Connection,
186    kind: &EdgeKind,
187) -> anyhow::Result<Vec<(String, String, Option<String>)>> {
188    let mut stmt = conn.prepare(
189        "SELECT ps.path, pt.path, e.metadata
190         FROM edges e
191         JOIN nodes ns ON ns.id = e.source_id
192         JOIN nodes nt ON nt.id = e.target_id
193         JOIN paths ps ON ps.id = ns.file_id
194         JOIN paths pt ON pt.id = nt.file_id
195         WHERE e.kind = ?1 AND ns.kind = 'file' AND nt.kind = 'file'
196         ORDER BY ps.path, pt.path",
197    )?;
198    let rows = stmt
199        .query_map(params![kind.as_str()], |r| {
200            Ok((r.get(0)?, r.get(1)?, r.get(2)?))
201        })?
202        .collect::<Result<Vec<_>, _>>()?;
203    Ok(rows)
204}
205
206pub(super) fn remove_file_edge(
207    conn: &Connection,
208    source: &str,
209    target: &str,
210    kind: &EdgeKind,
211) -> anyhow::Result<()> {
212    conn.execute(
213        "DELETE FROM edges WHERE kind = ?3
214           AND source_id IN (SELECT n.id FROM nodes n JOIN paths p ON p.id = n.file_id
215                             WHERE n.kind = 'file' AND p.path = ?1)
216           AND target_id IN (SELECT n.id FROM nodes n JOIN paths p ON p.id = n.file_id
217                             WHERE n.kind = 'file' AND p.path = ?2)",
218        params![source, target, kind.as_str()],
219    )?;
220    Ok(())
221}