Skip to main content

relay_knowledge/storage/sqlite/
code.rs

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