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