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