kaizen/metrics/
codegraph.rs1use crate::metrics::types::{RepoEdge, RepoSnapshotRecord, SymbolFact};
6use anyhow::{Context, Result};
7use graphqlite::Connection;
8use std::fs;
9use std::path::Path;
10
11pub fn rebuild_sidecar(
12 graph_path: &Path,
13 snapshot: &RepoSnapshotRecord,
14 files: &[crate::metrics::types::FileFact],
15 symbols: &[SymbolFact],
16 edges: &[RepoEdge],
17) -> Result<()> {
18 if graph_path.exists() {
19 let _ = fs::remove_file(graph_path);
20 }
21 let conn = Connection::open(graph_path).context("open codegraph database")?;
22
23 run_cypher(
24 &conn,
25 &format!(
26 "CREATE (s:Snapshot {{id: '{}', workspace: '{}', head_commit: '{}', analyzer_version: '{}', indexed_at_ms: {}}})",
27 esc(&snapshot.id),
28 esc(&snapshot.workspace),
29 esc(snapshot.head_commit.as_deref().unwrap_or("")),
30 esc(&snapshot.analyzer_version),
31 snapshot.indexed_at_ms
32 ),
33 )?;
34
35 for file in files {
36 run_cypher(
37 &conn,
38 &format!(
39 "CREATE (f:File {{id: '{}', path: '{}', language: '{}', complexity: {}, churn30: {}}})",
40 esc(&file.path),
41 esc(&file.path),
42 esc(&file.language),
43 file.complexity_total,
44 file.churn_30d
45 ),
46 )?;
47 run_cypher(
48 &conn,
49 &format!(
50 "MATCH (s:Snapshot {{id: '{}'}}), (f:File {{id: '{}'}}) CREATE (s)-[:HAS_FILE]->(f)",
51 esc(&snapshot.id),
52 esc(&file.path)
53 ),
54 )?;
55 }
56
57 for symbol in symbols {
58 let sym_id = symbol_id(symbol);
59 run_cypher(
60 &conn,
61 &format!(
62 "CREATE (sym:Symbol {{id: '{}', path: '{}', name: '{}', kind: '{}', complexity: {}}})",
63 esc(&sym_id),
64 esc(&symbol.path),
65 esc(&symbol.name),
66 esc(&symbol.kind),
67 symbol.complexity
68 ),
69 )?;
70 run_cypher(
71 &conn,
72 &format!(
73 "MATCH (fb:File {{id: '{}'}}), (sm:Symbol {{id: '{}'}}) CREATE (fb)-[:DECLARES]->(sm)",
74 esc(&symbol.path),
75 esc(&sym_id)
76 ),
77 )?;
78 }
79
80 for edge in edges {
81 if edge.kind == "CALLS" {
82 run_cypher(
83 &conn,
84 &format!(
85 "MATCH (a:Symbol {{id: '{}'}}), (b:Symbol {{id: '{}'}}) CREATE (a)-[:CALLS {{weight: {}}}]->(b)",
86 esc(&edge.from_path),
87 esc(&edge.to_path),
88 edge.weight
89 ),
90 )?;
91 continue;
92 }
93 run_cypher(
94 &conn,
95 &format!(
96 "MATCH (a:File {{id: '{}'}}), (b:File {{id: '{}'}}) CREATE (a)-[:{} {{weight: {}}}]->(b)",
97 esc(&edge.from_path),
98 esc(&edge.to_path),
99 edge.kind,
100 edge.weight
101 ),
102 )?;
103 }
104
105 Ok(())
106}
107
108fn run_cypher(conn: &Connection, query: &str) -> Result<()> {
109 conn.cypher(query)
110 .map(|_| ())
111 .map_err(|e| anyhow::anyhow!("{e}"))
112}
113
114pub fn symbol_id(symbol: &SymbolFact) -> String {
115 format!(
116 "{}#{}:{}-{}",
117 symbol.path, symbol.name, symbol.start_byte, symbol.end_byte
118 )
119}
120
121fn esc(input: &str) -> String {
122 input.replace('\\', "\\\\").replace('\'', "\\'")
123}