Skip to main content

relay_knowledge/storage/sqlite/
code.rs

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