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