Skip to main content

relay_knowledge/storage/sqlite/
code.rs

1use rusqlite::{Connection, params};
2
3#[path = "code_query.rs"]
4mod code_query;
5
6#[path = "code_impact.rs"]
7mod code_impact;
8
9#[path = "code_report.rs"]
10mod code_report;
11
12#[path = "code_schema.rs"]
13mod code_schema;
14
15#[path = "code_status.rs"]
16mod code_status;
17
18#[path = "code_batch.rs"]
19mod code_batch;
20
21#[cfg(test)]
22#[path = "code_tests.rs"]
23mod code_tests;
24
25#[cfg(test)]
26#[path = "code_batch_finalize_tests.rs"]
27mod code_batch_finalize_tests;
28
29#[cfg(test)]
30#[path = "code_query_accuracy_tests.rs"]
31mod code_query_accuracy_tests;
32
33#[cfg(test)]
34#[path = "code_query_import_target_tests.rs"]
35mod code_query_import_target_tests;
36
37#[cfg(test)]
38#[path = "code_metadata_tests.rs"]
39mod code_metadata_tests;
40
41use crate::{
42    domain::{
43        CodeFileFingerprint, CodeImpactRequest, CodeIndexBatch, CodeIndexCheckpoint,
44        CodeIndexProgressSummary, CodeIndexSession, CodeIndexSnapshot, CodeIndexSummary,
45        CodeRepositoryRegistration, CodeRepositoryReport, CodeRepositoryStatus,
46        CodeRepositoryTotals, CodeRetrievalHit, CodeRetrievalRequest,
47    },
48    storage::{CodeImpactChanges, CodeRepositoryStore, StorageError, StorageFuture},
49};
50
51use super::SqliteGraphStore;
52use code_status::{canonical_filter_values, canonical_path_filters, parse_json_list};
53
54pub(super) fn initialize_code_schema(connection: &Connection) -> Result<(), StorageError> {
55    code_schema::initialize_code_schema(connection)
56}
57
58impl CodeRepositoryStore for SqliteGraphStore {
59    fn upsert_code_repository(
60        &self,
61        registration: CodeRepositoryRegistration,
62    ) -> StorageFuture<'_, CodeRepositoryStatus> {
63        self.run(move |connection| code_status::upsert_repository(connection, registration))
64    }
65
66    fn code_repository_status(
67        &self,
68        repository: String,
69    ) -> StorageFuture<'_, Option<CodeRepositoryStatus>> {
70        self.run(move |connection| code_status::repository_status(connection, &repository))
71    }
72
73    fn code_repository_scope_status(
74        &self,
75        repository: String,
76        resolved_commit_sha: String,
77        path_filters: Vec<String>,
78        language_filters: Vec<String>,
79    ) -> StorageFuture<'_, Option<CodeRepositoryStatus>> {
80        self.run(move |connection| {
81            code_status::repository_scope_status(
82                connection,
83                &repository,
84                &resolved_commit_sha,
85                &path_filters,
86                &language_filters,
87            )
88        })
89    }
90
91    fn code_file_fingerprints(
92        &self,
93        repository_id: String,
94    ) -> StorageFuture<'_, Vec<CodeFileFingerprint>> {
95        self.run(move |connection| file_fingerprints(connection, &repository_id))
96    }
97
98    fn code_file_fingerprints_for_scope(
99        &self,
100        source_scope: String,
101    ) -> StorageFuture<'_, Vec<CodeFileFingerprint>> {
102        self.run(move |connection| file_fingerprints_for_scope(connection, &source_scope))
103    }
104
105    fn apply_code_index_snapshot(
106        &self,
107        snapshot: CodeIndexSnapshot,
108    ) -> StorageFuture<'_, CodeIndexSummary> {
109        self.run(move |connection| apply_snapshot(connection, snapshot))
110    }
111
112    fn begin_code_index_session(
113        &self,
114        session: CodeIndexSession,
115    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
116        self.run(move |connection| code_batch::begin_session(connection, session))
117    }
118
119    fn apply_code_index_batch(
120        &self,
121        batch: CodeIndexBatch,
122    ) -> StorageFuture<'_, CodeIndexCheckpoint> {
123        self.run(move |connection| code_batch::apply_batch(connection, batch))
124    }
125
126    fn finalize_code_index_session(
127        &self,
128        session: CodeIndexSession,
129    ) -> StorageFuture<'_, CodeIndexSummary> {
130        self.run(move |connection| code_batch::finalize_session(connection, session))
131    }
132
133    fn search_code(
134        &self,
135        request: CodeRetrievalRequest,
136    ) -> StorageFuture<'_, Vec<CodeRetrievalHit>> {
137        self.run(move |connection| code_query::search_code(connection, request))
138    }
139
140    fn analyze_code_impact(
141        &self,
142        request: CodeImpactRequest,
143        changes: CodeImpactChanges,
144    ) -> StorageFuture<'_, Vec<CodeRetrievalHit>> {
145        self.run(move |connection| code_impact::analyze_impact(connection, request, changes))
146    }
147
148    fn code_repository_totals(&self) -> StorageFuture<'_, CodeRepositoryTotals> {
149        self.run(code_report::repository_totals)
150    }
151
152    fn code_repository_report(
153        &self,
154        repository: String,
155    ) -> StorageFuture<'_, CodeRepositoryReport> {
156        self.run(move |connection| code_report::repository_report(connection, &repository))
157    }
158}
159
160fn file_fingerprints(
161    connection: &mut Connection,
162    repository_id: &str,
163) -> Result<Vec<CodeFileFingerprint>, StorageError> {
164    let mut statement = connection.prepare(
165        "
166        SELECT path, blob_hash
167        FROM code_repository_files
168        WHERE repository_id = ?1
169          AND source_scope = (
170              SELECT last_indexed_scope_id FROM code_repositories WHERE repository_id = ?1
171          )
172        ORDER BY path ASC
173        ",
174    )?;
175    let rows = statement.query_map(params![repository_id], |row| {
176        Ok(CodeFileFingerprint {
177            path: row.get(0)?,
178            blob_hash: row.get(1)?,
179        })
180    })?;
181
182    rows.collect::<Result<Vec<_>, _>>()
183        .map_err(StorageError::from)
184}
185
186fn file_fingerprints_for_scope(
187    connection: &mut Connection,
188    source_scope: &str,
189) -> Result<Vec<CodeFileFingerprint>, StorageError> {
190    let mut statement = connection.prepare(
191        "
192        SELECT path, blob_hash
193        FROM code_repository_files
194        WHERE source_scope = ?1
195        ORDER BY path ASC
196        ",
197    )?;
198    let rows = statement.query_map(params![source_scope], |row| {
199        Ok(CodeFileFingerprint {
200            path: row.get(0)?,
201            blob_hash: row.get(1)?,
202        })
203    })?;
204
205    rows.collect::<Result<Vec<_>, _>>()
206        .map_err(StorageError::from)
207}
208
209fn apply_snapshot(
210    connection: &mut Connection,
211    snapshot: CodeIndexSnapshot,
212) -> Result<CodeIndexSummary, StorageError> {
213    let transaction = connection.transaction()?;
214    if snapshot.full_replace {
215        delete_scope_index(&transaction, &snapshot.source_scope)?;
216    } else {
217        clone_active_scope_for_incremental(&transaction, &snapshot)?;
218        for path in &snapshot.deleted_paths {
219            delete_path_index(&transaction, &snapshot.source_scope, path)?;
220        }
221        for file in &snapshot.files {
222            delete_path_index(&transaction, &snapshot.source_scope, &file.path)?;
223        }
224    }
225
226    for file in &snapshot.files {
227        transaction.execute(
228            "
229            INSERT INTO code_repository_files (
230                repository_id, source_scope, file_id, path, language_id, blob_hash, byte_len,
231                line_count, parse_status, degraded_reason
232            )
233            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
234            ",
235            params![
236                file.repository_id,
237                file.source_scope,
238                file.file_id,
239                file.path,
240                file.language_id,
241                file.blob_hash,
242                file.byte_len,
243                file.line_count,
244                file.parse_status.as_str(),
245                file.degraded_reason,
246            ],
247        )?;
248    }
249    for symbol in &snapshot.symbols {
250        transaction.execute(
251            "
252            INSERT INTO code_repository_symbols (
253                repository_id, source_scope, symbol_snapshot_id, canonical_symbol_id,
254                file_id, path, language_id, name,
255                qualified_name, kind, signature, doc_comment, byte_start, byte_end,
256                line_start, line_end
257            )
258            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)
259            ",
260            params![
261                symbol.repository_id,
262                symbol.source_scope,
263                symbol.symbol_snapshot_id,
264                symbol.canonical_symbol_id,
265                symbol.file_id,
266                symbol.path,
267                symbol.language_id,
268                symbol.name,
269                symbol.qualified_name,
270                symbol.kind,
271                symbol.signature,
272                symbol.doc_comment,
273                symbol.byte_range.start,
274                symbol.byte_range.end,
275                symbol.line_range.start,
276                symbol.line_range.end,
277            ],
278        )?;
279        insert_search_document(
280            &transaction,
281            &symbol.source_scope,
282            "symbol",
283            &symbol.symbol_snapshot_id,
284            &symbol.path,
285            &symbol.language_id,
286            [
287                symbol.name.as_str(),
288                symbol.qualified_name.as_str(),
289                symbol.kind.as_str(),
290                symbol.signature.as_str(),
291                symbol.doc_comment.as_deref().unwrap_or_default(),
292                symbol.path.as_str(),
293            ],
294        )?;
295    }
296    for reference in &snapshot.references {
297        transaction.execute(
298            "
299            INSERT INTO code_repository_references (
300                repository_id, source_scope, reference_id, file_id, path, name, kind,
301                target_symbol_snapshot_id, target_hint, resolution_state,
302                confidence_basis_points, confidence_tier,
303                byte_start, byte_end, line_start, line_end
304            )
305            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)
306            ",
307            params![
308                reference.repository_id,
309                reference.source_scope,
310                reference.reference_id,
311                reference.file_id,
312                reference.path,
313                reference.name,
314                reference.kind,
315                reference.target_symbol_snapshot_id,
316                reference.target_hint,
317                reference.resolution_state,
318                reference.confidence_basis_points,
319                reference.confidence_tier,
320                reference.byte_range.start,
321                reference.byte_range.end,
322                reference.line_range.start,
323                reference.line_range.end,
324            ],
325        )?;
326        insert_search_document(
327            &transaction,
328            &reference.source_scope,
329            "reference",
330            &reference.reference_id,
331            &reference.path,
332            "",
333            [
334                reference.name.as_str(),
335                reference.kind.as_str(),
336                reference.target_hint.as_deref().unwrap_or_default(),
337                reference.path.as_str(),
338            ],
339        )?;
340    }
341    insert_imports_calls_chunks_diagnostics(&transaction, &snapshot)?;
342    update_repository_after_snapshot(&transaction, &snapshot)?;
343    transaction.commit()?;
344
345    let status =
346        code_status::repository_status(connection, &snapshot.repository_id)?.ok_or_else(|| {
347            StorageError::InvalidInput("code repository status is missing after index".to_owned())
348        })?;
349
350    Ok(CodeIndexSummary {
351        repository_id: snapshot.repository_id,
352        source_scope: snapshot.source_scope,
353        resolved_commit_sha: snapshot.resolved_commit_sha,
354        tree_hash: snapshot.tree_hash,
355        indexed_file_count: status.indexed_file_count,
356        changed_path_count: snapshot.changed_path_count,
357        skipped_unchanged_count: snapshot.skipped_unchanged_count,
358        deleted_path_count: snapshot.deleted_paths.len(),
359        symbol_count: status.symbol_count,
360        reference_count: status.reference_count,
361        chunk_count: status.chunk_count,
362        degraded_file_count: snapshot.diagnostics.len(),
363        progress: CodeIndexProgressSummary {
364            git_file_count: if snapshot.full_replace {
365                status.indexed_file_count
366            } else {
367                snapshot.changed_path_count
368            },
369            blob_read_count: snapshot.files.len(),
370            parsed_file_count: snapshot.files.len(),
371            sqlite_write_count: snapshot
372                .files
373                .len()
374                .saturating_add(snapshot.symbols.len())
375                .saturating_add(snapshot.references.len())
376                .saturating_add(snapshot.imports.len())
377                .saturating_add(snapshot.calls.len())
378                .saturating_add(snapshot.chunks.len())
379                .saturating_add(snapshot.diagnostics.len()),
380            skipped_file_count: snapshot.skipped_unchanged_count,
381            degraded_file_count: snapshot.diagnostics.len(),
382            batch_count: 1,
383            checkpoint_file_count: snapshot.files.len(),
384            resource_budget: crate::domain::CodeIndexResourceBudget::default(),
385        },
386    })
387}
388
389fn clone_active_scope_for_incremental(
390    transaction: &rusqlite::Transaction<'_>,
391    snapshot: &CodeIndexSnapshot,
392) -> Result<(), StorageError> {
393    let path_filters_json = serde_json::to_string(&snapshot.path_filters)
394        .map_err(|error| StorageError::InvalidInput(error.to_string()))?;
395    let language_filters_json = serde_json::to_string(&snapshot.language_filters)
396        .map_err(|error| StorageError::InvalidInput(error.to_string()))?;
397    let requested_path_filters = canonical_path_filters(&snapshot.path_filters);
398    let requested_language_filters = canonical_filter_values(&snapshot.language_filters);
399    let mut statement = transaction.prepare(
400        "
401        SELECT source_scope, path_filters_json, language_filters_json
402        FROM code_repository_scopes
403        WHERE repository_id = ?1
404          AND resolved_commit_sha = ?4
405        ORDER BY
406          CASE WHEN path_filters_json = ?2 AND language_filters_json = ?3 THEN 0 ELSE 1 END,
407          rowid DESC
408        ",
409    )?;
410    let base_commit = snapshot
411        .base_resolved_commit_sha
412        .as_deref()
413        .ok_or_else(|| {
414            StorageError::InvalidInput(format!(
415                "code repository '{}' incremental snapshot is missing its resolved base commit",
416                snapshot.repository_id
417            ))
418        })?;
419    let rows = statement.query_map(
420        params![
421            snapshot.repository_id,
422            path_filters_json,
423            language_filters_json,
424            base_commit
425        ],
426        |row| {
427            Ok((
428                row.get::<_, String>(0)?,
429                parse_json_list(row.get::<_, String>(1)?)?,
430                parse_json_list(row.get::<_, String>(2)?)?,
431            ))
432        },
433    )?;
434    let mut previous_scope = None;
435    for row in rows {
436        let (source_scope, stored_path_filters, stored_language_filters) = row?;
437        if canonical_path_filters(&stored_path_filters) == requested_path_filters
438            && canonical_filter_values(&stored_language_filters) == requested_language_filters
439        {
440            previous_scope = Some(source_scope);
441            break;
442        }
443    }
444    let previous_scope = previous_scope.ok_or_else(|| {
445        StorageError::InvalidInput(format!(
446            "code repository '{}' has no matching indexed scope for incremental filters at the current base commit",
447            snapshot.repository_id
448        ))
449    })?;
450    if previous_scope == snapshot.source_scope {
451        return Ok(());
452    }
453    delete_scope_index(transaction, &snapshot.source_scope)?;
454    clone_code_table(
455        transaction,
456        "code_repository_files",
457        "repository_id, source_scope, file_id, path, language_id, blob_hash, byte_len, line_count, parse_status, degraded_reason",
458        &previous_scope,
459        &snapshot.source_scope,
460    )?;
461    clone_code_table(
462        transaction,
463        "code_repository_symbols",
464        "repository_id, source_scope, symbol_snapshot_id, canonical_symbol_id, file_id, path, language_id, name, qualified_name, kind, signature, doc_comment, byte_start, byte_end, line_start, line_end",
465        &previous_scope,
466        &snapshot.source_scope,
467    )?;
468    clone_code_table(
469        transaction,
470        "code_repository_references",
471        "repository_id, source_scope, reference_id, file_id, path, name, kind, target_symbol_snapshot_id, target_hint, resolution_state, confidence_basis_points, confidence_tier, byte_start, byte_end, line_start, line_end",
472        &previous_scope,
473        &snapshot.source_scope,
474    )?;
475    clone_code_table(
476        transaction,
477        "code_repository_imports",
478        "repository_id, source_scope, import_id, file_id, path, module, target_hint, resolution_state, confidence_basis_points, confidence_tier, line_start, line_end",
479        &previous_scope,
480        &snapshot.source_scope,
481    )?;
482    clone_code_table(
483        transaction,
484        "code_repository_calls",
485        "repository_id, source_scope, call_id, file_id, path, caller_symbol_snapshot_id, caller_name, callee_symbol_snapshot_id, callee_name, target_hint, resolution_state, confidence_basis_points, confidence_tier, line_start, line_end",
486        &previous_scope,
487        &snapshot.source_scope,
488    )?;
489    clone_code_table(
490        transaction,
491        "code_repository_chunks",
492        "repository_id, source_scope, chunk_id, file_id, path, language_id, content, byte_start, byte_end, line_start, line_end, symbol_snapshot_id",
493        &previous_scope,
494        &snapshot.source_scope,
495    )?;
496    clone_code_table(
497        transaction,
498        "code_repository_file_diagnostics",
499        "repository_id, source_scope, path, parse_status, message",
500        &previous_scope,
501        &snapshot.source_scope,
502    )?;
503    clone_code_table(
504        transaction,
505        "code_repository_search",
506        "source_scope, document_kind, record_id, path, language_id, content",
507        &previous_scope,
508        &snapshot.source_scope,
509    )?;
510
511    Ok(())
512}
513
514pub(super) struct SearchDocumentInserter<'transaction> {
515    statement: rusqlite::Statement<'transaction>,
516}
517
518impl<'transaction> SearchDocumentInserter<'transaction> {
519    pub(super) fn new(
520        transaction: &'transaction rusqlite::Transaction<'_>,
521    ) -> Result<Self, StorageError> {
522        let statement = transaction.prepare(
523            "
524            INSERT INTO code_repository_search (
525                source_scope, document_kind, record_id, path, language_id, content
526            )
527            VALUES (?1, ?2, ?3, ?4, ?5, ?6)
528            ",
529        )?;
530
531        Ok(Self { statement })
532    }
533
534    pub(super) fn insert<'a>(
535        &mut self,
536        source_scope: &str,
537        document_kind: &str,
538        record_id: &str,
539        path: &str,
540        language_id: &str,
541        fields: impl IntoIterator<Item = &'a str>,
542    ) -> Result<(), StorageError> {
543        insert_search_document_with_statement(
544            &mut self.statement,
545            source_scope,
546            document_kind,
547            record_id,
548            path,
549            language_id,
550            fields,
551        )
552    }
553}
554
555fn insert_search_document<'a>(
556    transaction: &rusqlite::Transaction<'_>,
557    source_scope: &str,
558    document_kind: &str,
559    record_id: &str,
560    path: &str,
561    language_id: &str,
562    fields: impl IntoIterator<Item = &'a str>,
563) -> Result<(), StorageError> {
564    let mut inserter = SearchDocumentInserter::new(transaction)?;
565    inserter.insert(
566        source_scope,
567        document_kind,
568        record_id,
569        path,
570        language_id,
571        fields,
572    )
573}
574
575fn insert_search_document_with_statement<'a>(
576    statement: &mut rusqlite::Statement<'_>,
577    source_scope: &str,
578    document_kind: &str,
579    record_id: &str,
580    path: &str,
581    language_id: &str,
582    fields: impl IntoIterator<Item = &'a str>,
583) -> Result<(), StorageError> {
584    let fields = fields
585        .into_iter()
586        .filter(|field| !field.trim().is_empty())
587        .collect::<Vec<_>>();
588    let mut content = fields.join(" ");
589    if document_kind == "symbol" {
590        let terms =
591            identifier_search_terms(&fields.iter().take(2).copied().collect::<Vec<_>>().join(" "));
592        if !terms.is_empty() {
593            content.push(' ');
594            content.push_str(&terms.join(" "));
595        }
596    }
597    statement.execute(params![
598        source_scope,
599        document_kind,
600        record_id,
601        path,
602        language_id,
603        content
604    ])?;
605
606    Ok(())
607}
608
609fn identifier_search_terms(content: &str) -> Vec<String> {
610    let mut terms = Vec::new();
611    for token in
612        content.split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
613    {
614        if token.is_empty() {
615            continue;
616        }
617        terms.extend(
618            token
619                .split('_')
620                .filter(|part| !part.is_empty())
621                .map(str::to_ascii_lowercase),
622        );
623        terms.extend(camel_case_terms(token));
624    }
625    terms.sort();
626    terms.dedup();
627
628    terms
629}
630
631fn camel_case_terms(token: &str) -> Vec<String> {
632    let mut terms = Vec::new();
633    let mut start = 0;
634    let mut previous: Option<char> = None;
635    let chars = token.char_indices().collect::<Vec<_>>();
636    for (index, (byte_index, character)) in chars.iter().enumerate() {
637        let next = chars.get(index + 1).map(|(_, next)| *next);
638        let starts_upper_word = character.is_ascii_uppercase()
639            && previous.is_some_and(|previous| {
640                previous.is_ascii_lowercase()
641                    || previous.is_ascii_digit()
642                    || next.is_some_and(|next| next.is_ascii_lowercase())
643            });
644        if *byte_index > start && starts_upper_word {
645            terms.push(token[start..*byte_index].to_ascii_lowercase());
646            start = *byte_index;
647        }
648        previous = Some(*character);
649    }
650    if start < token.len() {
651        terms.push(token[start..].to_ascii_lowercase());
652    }
653
654    terms
655}
656
657fn clone_code_table(
658    transaction: &rusqlite::Transaction<'_>,
659    table: &'static str,
660    columns: &'static str,
661    previous_scope: &str,
662    next_scope: &str,
663) -> Result<(), StorageError> {
664    let selected_columns = columns.replacen("source_scope", "?2", 1);
665    transaction.execute(
666        &format!(
667            "INSERT INTO {table} ({columns}) SELECT {selected_columns} FROM {table} WHERE source_scope = ?1"
668        ),
669        params![previous_scope, next_scope],
670    )?;
671
672    Ok(())
673}
674
675fn insert_imports_calls_chunks_diagnostics(
676    transaction: &rusqlite::Transaction<'_>,
677    snapshot: &CodeIndexSnapshot,
678) -> Result<(), StorageError> {
679    for import in &snapshot.imports {
680        transaction.execute(
681            "
682            INSERT INTO code_repository_imports (
683                repository_id, source_scope, import_id, file_id, path, module, target_hint,
684                resolution_state, confidence_basis_points, confidence_tier, line_start, line_end
685            )
686            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
687            ",
688            params![
689                import.repository_id,
690                import.source_scope,
691                import.import_id,
692                import.file_id,
693                import.path,
694                import.module,
695                import.target_hint,
696                import.resolution_state,
697                import.confidence_basis_points,
698                import.confidence_tier,
699                import.line_range.start,
700                import.line_range.end,
701            ],
702        )?;
703        insert_search_document(
704            transaction,
705            &import.source_scope,
706            "import",
707            &import.import_id,
708            &import.path,
709            "",
710            [
711                import.module.as_str(),
712                import.target_hint.as_deref().unwrap_or_default(),
713                import.path.as_str(),
714            ],
715        )?;
716    }
717    for call in &snapshot.calls {
718        transaction.execute(
719            "
720            INSERT INTO code_repository_calls (
721                repository_id, source_scope, call_id, file_id, path, caller_symbol_snapshot_id,
722                caller_name, callee_symbol_snapshot_id, callee_name, target_hint,
723                resolution_state, confidence_basis_points, confidence_tier, line_start, line_end
724            )
725            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
726            ",
727            params![
728                call.repository_id,
729                call.source_scope,
730                call.call_id,
731                call.file_id,
732                call.path,
733                call.caller_symbol_snapshot_id,
734                call.caller_name,
735                call.callee_symbol_snapshot_id,
736                call.callee_name,
737                call.target_hint,
738                call.resolution_state,
739                call.confidence_basis_points,
740                call.confidence_tier,
741                call.line_range.start,
742                call.line_range.end,
743            ],
744        )?;
745        insert_search_document(
746            transaction,
747            &call.source_scope,
748            "call",
749            &call.call_id,
750            &call.path,
751            "",
752            [
753                call.caller_name.as_deref().unwrap_or_default(),
754                call.callee_name.as_str(),
755                call.target_hint.as_deref().unwrap_or_default(),
756                call.path.as_str(),
757            ],
758        )?;
759    }
760    for chunk in &snapshot.chunks {
761        transaction.execute(
762            "
763            INSERT INTO code_repository_chunks (
764                repository_id, source_scope, chunk_id, file_id, path, language_id, content,
765                byte_start, byte_end, line_start, line_end, symbol_snapshot_id
766            )
767            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
768            ",
769            params![
770                chunk.repository_id,
771                chunk.source_scope,
772                chunk.chunk_id,
773                chunk.file_id,
774                chunk.path,
775                chunk.language_id,
776                chunk.content,
777                chunk.byte_range.start,
778                chunk.byte_range.end,
779                chunk.line_range.start,
780                chunk.line_range.end,
781                chunk.symbol_snapshot_id,
782            ],
783        )?;
784        insert_search_document(
785            transaction,
786            &chunk.source_scope,
787            "chunk",
788            &chunk.chunk_id,
789            &chunk.path,
790            &chunk.language_id,
791            [
792                chunk.content.as_str(),
793                chunk.symbol_snapshot_id.as_deref().unwrap_or_default(),
794                chunk.path.as_str(),
795            ],
796        )?;
797    }
798    for diagnostic in &snapshot.diagnostics {
799        transaction.execute(
800            "
801            INSERT OR REPLACE INTO code_repository_file_diagnostics
802                (repository_id, source_scope, path, parse_status, message)
803            VALUES (?1, ?2, ?3, ?4, ?5)
804            ",
805            params![
806                diagnostic.repository_id,
807                diagnostic.source_scope,
808                diagnostic.path,
809                diagnostic.parse_status.as_str(),
810                diagnostic.message,
811            ],
812        )?;
813    }
814    for tombstone in &snapshot.tombstones {
815        transaction.execute(
816            "
817            INSERT OR REPLACE INTO code_repository_path_tombstones
818                (repository_id, source_scope, old_path, new_path, base_ref, head_ref)
819            VALUES (?1, ?2, ?3, ?4, ?5, ?6)
820            ",
821            params![
822                tombstone.repository_id,
823                tombstone.source_scope,
824                tombstone.old_path,
825                tombstone.new_path,
826                tombstone.base_ref,
827                tombstone.head_ref,
828            ],
829        )?;
830    }
831
832    Ok(())
833}
834
835fn update_repository_after_snapshot(
836    transaction: &rusqlite::Transaction<'_>,
837    snapshot: &CodeIndexSnapshot,
838) -> Result<(), StorageError> {
839    let file_count = count_code_rows(transaction, "code_repository_files", &snapshot.source_scope)?;
840    let symbol_count = count_code_rows(
841        transaction,
842        "code_repository_symbols",
843        &snapshot.source_scope,
844    )?;
845    let reference_count = count_code_rows(
846        transaction,
847        "code_repository_references",
848        &snapshot.source_scope,
849    )?;
850    let chunk_count = count_code_rows(
851        transaction,
852        "code_repository_chunks",
853        &snapshot.source_scope,
854    )?;
855    let degraded_file_count = count_code_rows(
856        transaction,
857        "code_repository_file_diagnostics",
858        &snapshot.source_scope,
859    )?;
860    let degraded_reason = (degraded_file_count > 0)
861        .then(|| format!("{degraded_file_count} file(s) degraded during code indexing"));
862    let path_filters_json = serde_json::to_string(&snapshot.path_filters)
863        .map_err(|error| StorageError::InvalidInput(error.to_string()))?;
864    let language_filters_json = serde_json::to_string(&snapshot.language_filters)
865        .map_err(|error| StorageError::InvalidInput(error.to_string()))?;
866    transaction.execute(
867        "
868        INSERT INTO code_repository_scopes (
869            source_scope, repository_id, resolved_commit_sha, tree_hash,
870            path_filters_json, language_filters_json, indexed_file_count,
871            symbol_count, reference_count, chunk_count, stale, degraded_reason
872        )
873        VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 0, ?11)
874        ON CONFLICT(source_scope) DO UPDATE SET
875            repository_id = excluded.repository_id,
876            resolved_commit_sha = excluded.resolved_commit_sha,
877            tree_hash = excluded.tree_hash,
878            path_filters_json = excluded.path_filters_json,
879            language_filters_json = excluded.language_filters_json,
880            indexed_file_count = excluded.indexed_file_count,
881            symbol_count = excluded.symbol_count,
882            reference_count = excluded.reference_count,
883            chunk_count = excluded.chunk_count,
884            stale = 0,
885            degraded_reason = excluded.degraded_reason
886        ",
887        params![
888            snapshot.source_scope,
889            snapshot.repository_id,
890            snapshot.resolved_commit_sha,
891            snapshot.tree_hash,
892            path_filters_json,
893            language_filters_json,
894            file_count,
895            symbol_count,
896            reference_count,
897            chunk_count,
898            degraded_reason,
899        ],
900    )?;
901    transaction.execute(
902        "
903        UPDATE code_repositories
904        SET last_indexed_scope_id = ?2,
905            last_indexed_commit = ?3,
906            tree_hash = ?4,
907            state = 'fresh',
908            indexed_file_count = ?5,
909            symbol_count = ?6,
910            reference_count = ?7,
911            chunk_count = ?8,
912            stale = 0,
913            degraded_reason = ?9
914        WHERE repository_id = ?1
915        ",
916        params![
917            snapshot.repository_id,
918            snapshot.source_scope,
919            snapshot.resolved_commit_sha,
920            snapshot.tree_hash,
921            file_count,
922            symbol_count,
923            reference_count,
924            chunk_count,
925            degraded_reason,
926        ],
927    )?;
928
929    Ok(())
930}
931
932fn delete_scope_index(
933    transaction: &rusqlite::Transaction<'_>,
934    source_scope: &str,
935) -> Result<(), StorageError> {
936    for table in [
937        "code_repository_path_tombstones",
938        "code_repository_file_diagnostics",
939        "code_repository_chunks",
940        "code_repository_calls",
941        "code_repository_imports",
942        "code_repository_references",
943        "code_repository_symbols",
944        "code_repository_files",
945        "code_repository_search",
946    ] {
947        transaction.execute(
948            &format!("DELETE FROM {table} WHERE source_scope = ?1"),
949            params![source_scope],
950        )?;
951    }
952
953    Ok(())
954}
955
956fn delete_path_index(
957    transaction: &rusqlite::Transaction<'_>,
958    source_scope: &str,
959    path: &str,
960) -> Result<(), StorageError> {
961    for table in [
962        "code_repository_file_diagnostics",
963        "code_repository_chunks",
964        "code_repository_calls",
965        "code_repository_imports",
966        "code_repository_references",
967        "code_repository_symbols",
968        "code_repository_files",
969        "code_repository_search",
970    ] {
971        transaction.execute(
972            &format!("DELETE FROM {table} WHERE source_scope = ?1 AND path = ?2"),
973            params![source_scope, path],
974        )?;
975    }
976
977    Ok(())
978}
979
980fn count_code_rows(
981    transaction: &rusqlite::Transaction<'_>,
982    table: &'static str,
983    source_scope: &str,
984) -> Result<usize, StorageError> {
985    transaction
986        .query_row(
987            &format!("SELECT COUNT(*) FROM {table} WHERE source_scope = ?1"),
988            params![source_scope],
989            |row| row.get(0),
990        )
991        .map_err(StorageError::from)
992}