Skip to main content

relay_knowledge/storage/sqlite/
code.rs

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