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