1use postgres::GenericClient;
2use serde::{Deserialize, Serialize};
3
4pub use crate::index::indexer::{
5 IndexDegradation, IndexDurations, IndexOutcome, IndexRequest, index_files,
6};
7
8use crate::models::{
9 CallRelation, ContentChunk, ImportRelation, IndexedFile, IndexedProject, Symbol,
10};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct CodeFactWriteRequest {
14 pub project_id: String,
15 pub file_path: String,
16 pub symbols: usize,
17 pub imports: usize,
18 pub calls: usize,
19 pub chunks: usize,
20}
21
22#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
23pub struct CodeFactWriteSummary {
24 pub files_written: usize,
25 pub symbols_written: usize,
26 pub imports_written: usize,
27 pub calls_written: usize,
28 pub chunks_written: usize,
29 pub graph_sync_pending: bool,
30 pub vectors_sync_pending: bool,
31}
32
33impl CodeFactWriteSummary {
34 pub fn for_file(symbols: usize, imports: usize, calls: usize, chunks: usize) -> Self {
35 Self {
36 files_written: 1,
37 symbols_written: symbols,
38 imports_written: imports,
39 calls_written: calls,
40 chunks_written: chunks,
41 graph_sync_pending: true,
42 vectors_sync_pending: true,
43 }
44 }
45}
46
47pub fn delete_file_facts(
48 conn: &mut impl GenericClient,
49 project_id: &str,
50 file_path: &str,
51) -> anyhow::Result<()> {
52 conn.execute(
53 "DELETE FROM code_symbols WHERE project_id = $1 AND file_path = $2",
54 &[&project_id, &file_path],
55 )?;
56 conn.execute(
57 "DELETE FROM code_indexed_files WHERE project_id = $1 AND file_path = $2",
58 &[&project_id, &file_path],
59 )?;
60 conn.execute(
61 "DELETE FROM code_content_chunks WHERE project_id = $1 AND file_path = $2",
62 &[&project_id, &file_path],
63 )?;
64 conn.execute(
65 "DELETE FROM code_imports WHERE project_id = $1 AND source_file = $2",
66 &[&project_id, &file_path],
67 )?;
68 conn.execute(
69 "DELETE FROM code_calls WHERE project_id = $1 AND file_path = $2",
70 &[&project_id, &file_path],
71 )?;
72 Ok(())
73}
74
75pub fn upsert_symbols(conn: &mut impl GenericClient, symbols: &[Symbol]) -> anyhow::Result<usize> {
76 for sym in symbols {
77 conn.execute(
78 "INSERT INTO code_symbols (
79 id, project_id, file_path, name, qualified_name,
80 kind, language, byte_start, byte_end,
81 line_start, line_end, signature, docstring,
82 parent_symbol_id, content_hash, summary,
83 created_at, updated_at
84 ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,NOW(),NOW())
85 ON CONFLICT(id) DO UPDATE SET
86 name=excluded.name, qualified_name=excluded.qualified_name,
87 kind=excluded.kind, byte_start=excluded.byte_start,
88 byte_end=excluded.byte_end, line_start=excluded.line_start,
89 line_end=excluded.line_end, signature=excluded.signature,
90 docstring=excluded.docstring, parent_symbol_id=excluded.parent_symbol_id,
91 language=excluded.language, content_hash=excluded.content_hash,
92 summary=CASE WHEN excluded.content_hash != code_symbols.content_hash
93 THEN NULL ELSE code_symbols.summary END,
94 updated_at=NOW()",
95 &[
96 &sym.id,
97 &sym.project_id,
98 &sym.file_path,
99 &sym.name,
100 &sym.qualified_name,
101 &sym.kind,
102 &sym.language,
103 &to_i32(sym.byte_start),
104 &to_i32(sym.byte_end),
105 &to_i32(sym.line_start),
106 &to_i32(sym.line_end),
107 &sym.signature,
108 &sym.docstring,
109 &sym.parent_symbol_id,
110 &sym.content_hash,
111 &sym.summary,
112 ],
113 )?;
114 }
115 Ok(symbols.len())
116}
117
118pub fn upsert_file(conn: &mut impl GenericClient, file: &IndexedFile) -> anyhow::Result<()> {
119 conn.execute(
120 "INSERT INTO code_indexed_files (
121 id, project_id, file_path, language, content_hash,
122 symbol_count, byte_size, graph_synced, vectors_synced,
123 graph_sync_attempted_at, indexed_at
124 ) VALUES ($1,$2,$3,$4,$5,$6,$7,false,false,NULL,NOW())
125 ON CONFLICT(id) DO UPDATE SET
126 content_hash=excluded.content_hash,
127 symbol_count=excluded.symbol_count,
128 byte_size=excluded.byte_size,
129 graph_synced=false,
130 vectors_synced=false,
131 graph_sync_attempted_at=NULL,
132 indexed_at=NOW()",
133 &[
134 &file.id,
135 &file.project_id,
136 &file.file_path,
137 &file.language,
138 &file.content_hash,
139 &to_i32(file.symbol_count),
140 &to_i32(file.byte_size),
141 ],
142 )?;
143 Ok(())
144}
145
146pub fn upsert_content_chunks(
147 conn: &mut impl GenericClient,
148 chunks: &[ContentChunk],
149) -> anyhow::Result<usize> {
150 for chunk in chunks {
151 conn.execute(
152 "INSERT INTO code_content_chunks (
153 id, project_id, file_path, chunk_index,
154 line_start, line_end, content, language, created_at
155 ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NOW())
156 ON CONFLICT(id) DO UPDATE SET
157 content=excluded.content,
158 line_start=excluded.line_start,
159 line_end=excluded.line_end",
160 &[
161 &chunk.id,
162 &chunk.project_id,
163 &chunk.file_path,
164 &to_i32(chunk.chunk_index),
165 &to_i32(chunk.line_start),
166 &to_i32(chunk.line_end),
167 &chunk.content,
168 &chunk.language,
169 ],
170 )?;
171 }
172 Ok(chunks.len())
173}
174
175pub fn upsert_project_stats(
176 conn: &mut impl GenericClient,
177 project: &IndexedProject,
178) -> anyhow::Result<()> {
179 conn.execute(
180 "INSERT INTO code_indexed_projects (
181 id, root_path, total_files, total_symbols,
182 last_indexed_at, index_duration_ms
183 ) VALUES ($1,$2,$3,$4,NOW(),$5)
184 ON CONFLICT(id) DO UPDATE SET
185 root_path=excluded.root_path,
186 total_files=excluded.total_files,
187 total_symbols=excluded.total_symbols,
188 last_indexed_at=excluded.last_indexed_at,
189 index_duration_ms=excluded.index_duration_ms,
190 updated_at=NOW()",
191 &[
192 &project.id,
193 &project.root_path,
194 &to_i32(project.total_files),
195 &to_i32(project.total_symbols),
196 &to_i32(project.index_duration_ms as usize),
197 ],
198 )?;
199 Ok(())
200}
201
202pub fn upsert_imports(
203 conn: &mut impl GenericClient,
204 project_id: &str,
205 file_path: &str,
206 imports: &[ImportRelation],
207) -> anyhow::Result<usize> {
208 conn.execute(
209 "DELETE FROM code_imports WHERE project_id = $1 AND source_file = $2",
210 &[&project_id, &file_path],
211 )?;
212 for imp in imports {
213 conn.execute(
214 "INSERT INTO code_imports (project_id, source_file, target_module)
215 VALUES ($1, $2, $3)
216 ON CONFLICT (project_id, source_file, target_module) DO NOTHING",
217 &[&project_id, &imp.file_path, &imp.module_name],
218 )?;
219 }
220 Ok(imports.len())
221}
222
223pub fn upsert_calls(
224 conn: &mut impl GenericClient,
225 project_id: &str,
226 file_path: &str,
227 calls: &[CallRelation],
228) -> anyhow::Result<usize> {
229 conn.execute(
230 "DELETE FROM code_calls WHERE project_id = $1 AND file_path = $2",
231 &[&project_id, &file_path],
232 )?;
233 for call in calls {
234 conn.execute(
235 "INSERT INTO code_calls
236 (project_id, caller_symbol_id, callee_symbol_id, callee_name, \
237 callee_target_kind, callee_external_module, file_path, line)
238 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
239 ON CONFLICT (
240 project_id, caller_symbol_id, callee_symbol_id, callee_name,
241 callee_target_kind, callee_external_module, file_path, line
242 ) DO NOTHING",
243 &[
244 &project_id,
245 &call.caller_symbol_id,
246 &call.callee_symbol_id.as_deref().unwrap_or(""),
247 &call.callee_name,
248 &call.callee_target_kind.as_str(),
249 &call.callee_external_module.as_deref().unwrap_or(""),
250 &call.file_path,
251 &to_i32(call.line),
252 ],
253 )?;
254 }
255 Ok(calls.len())
256}
257
258fn to_i32(value: usize) -> i32 {
259 value.min(i32::MAX as usize) as i32
260}