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}