Skip to main content

gobby_code/index/
api.rs

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}