1use std::collections::{BTreeMap, BTreeSet, HashMap};
2use std::path::Path;
3use std::time::Duration;
4
5use rusqlite::{params, Connection, OptionalExtension};
6
7use crate::callgraph_store::{
8 initialize_schema, join, set_meta_ready, CallGraphStoreError, Result, PROVENANCE_TREESITTER,
9};
10
11pub fn materialize_manifest_view_database(
13 database_path: &Path,
14 callgraph_blob_database: &Path,
15 manifest: &crate::views::Manifest,
16) -> Result<()> {
17 materialize(database_path, callgraph_blob_database, manifest, None).map(|_| ())
18}
19
20#[derive(Debug, Default, Clone, PartialEq, Eq)]
24pub struct MaterializeStats {
25 pub deleted: usize,
26 pub inserted: usize,
27 pub relinked_deleted: usize,
28 pub relinked_inserted: usize,
29 pub dependency_deleted: usize,
30 pub dependency_inserted: usize,
31 pub dependent_files: usize,
32 pub resolved_files: usize,
33 pub resolved_refs: usize,
34 pub resolved_bindings: usize,
35 pub rebuilt_surface_entries: usize,
36 pub decoded_caller_blobs: usize,
37 pub full_resolution: bool,
38 pub unattributed_callers: usize,
39}
40
41impl MaterializeStats {
42 pub fn graph_rows_written(&self) -> usize {
43 self.deleted + self.inserted + self.relinked_deleted + self.relinked_inserted
44 }
45
46 pub fn rows_written(&self) -> usize {
47 self.graph_rows_written() + self.dependency_deleted + self.dependency_inserted
48 }
49}
50
51pub fn apply_manifest_diff(
56 database_path: &Path,
57 base_manifest: &crate::views::Manifest,
58 new_manifest: &crate::views::Manifest,
59 callgraph_blob_database: &Path,
60) -> Result<MaterializeStats> {
61 apply_manifest_diff_profiled(
62 database_path,
63 base_manifest,
64 new_manifest,
65 callgraph_blob_database,
66 )
67 .map(|(stats, _)| stats)
68}
69
70pub(crate) fn apply_manifest_diff_profiled(
71 database_path: &Path,
72 base_manifest: &crate::views::Manifest,
73 new_manifest: &crate::views::Manifest,
74 callgraph_blob_database: &Path,
75) -> Result<(MaterializeStats, profile::PhaseTimings)> {
76 materialize(
77 database_path,
78 callgraph_blob_database,
79 new_manifest,
80 Some(base_manifest),
81 )
82}
83
84pub(crate) mod profile;
85mod resolution_facts;
86
87const MATERIALIZATION_VERSION: &str = "5";
88
89fn fingerprint(manifest: &crate::views::Manifest) -> Result<String> {
90 let bytes = manifest
91 .to_json_bytes()
92 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
93 Ok(blake3::hash(&bytes).to_hex().to_string())
94}
95
96fn manifest_callgraph_equivalent(
97 left: &crate::views::Manifest,
98 right: &crate::views::Manifest,
99) -> bool {
100 let mut left = left.entries();
101 let mut right = right.entries();
102 loop {
103 match (left.next(), right.next()) {
104 (None, None) => return true,
105 (Some((left_path, left_entry)), Some((right_path, right_entry)))
106 if left_path == right_path
107 && match (left_entry, right_entry) {
108 (
109 crate::views::ManifestEntry::Regular {
110 mode: left_mode,
111 planes: left_planes,
112 resolution_input: left_resolution_input,
113 },
114 crate::views::ManifestEntry::Regular {
115 mode: right_mode,
116 planes: right_planes,
117 resolution_input: right_resolution_input,
118 },
119 ) => {
120 left_mode == right_mode
121 && left_resolution_input == right_resolution_input
122 && left_planes.callgraph == right_planes.callgraph
123 }
124 _ => left_entry == right_entry,
125 } => {}
126 _ => return false,
127 }
128 }
129}
130
131fn materialize(
132 database_path: &Path,
133 callgraph_blob_database: &Path,
134 manifest: &crate::views::Manifest,
135 mut base: Option<&crate::views::Manifest>,
136) -> Result<(MaterializeStats, profile::PhaseTimings)> {
137 let mut profile = profile::PhaseTimer::new(if base.is_some() {
138 "incremental"
139 } else {
140 "cold"
141 });
142 let mut connection = if base.is_some() {
143 Connection::open_with_flags(database_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?
144 } else {
145 Connection::open(database_path)?
146 };
147 configure_materialization_connection(&connection)?;
148 if base.is_none() {
149 initialize_schema(&connection)?;
150 }
151 let transaction =
152 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
153 if let Some(base_manifest) = base {
154 let recorded: Option<String> = transaction
155 .query_row(
156 "SELECT v FROM meta WHERE k = 'view_manifest_fingerprint'",
157 [],
158 |row| row.get(0),
159 )
160 .optional()?;
161 let version: Option<String> = transaction
162 .query_row(
163 "SELECT v FROM meta WHERE k = 'view_materialization_version'",
164 [],
165 |row| row.get(0),
166 )
167 .optional()?;
168 if recorded.is_none() && version.is_none() {
169 base = None;
172 } else if recorded.as_deref() != Some(fingerprint(base_manifest)?.as_str()) {
173 return Err(CallGraphStoreError::Unavailable(
174 "derived manifest fingerprint mismatch; cold materialization required".into(),
175 ));
176 } else if version.as_deref() != Some(MATERIALIZATION_VERSION) {
177 base = None;
178 } else if base_manifest == manifest {
179 profile.finish("load_bindings_select");
180 return Ok((MaterializeStats::default(), profile.into_timings()));
181 } else if manifest_callgraph_equivalent(base_manifest, manifest) {
182 profile.finish("load_bindings_select");
183 transaction.execute(
184 "INSERT OR REPLACE INTO meta(k, v) VALUES('view_manifest_fingerprint', ?1)",
185 [fingerprint(manifest)?],
186 )?;
187 transaction.commit()?;
188 profile.finish("commit");
189 return Ok((MaterializeStats::default(), profile.into_timings()));
190 }
191 }
192 transaction.execute_batch("CREATE TABLE IF NOT EXISTS view_bindings (file_path TEXT PRIMARY KEY, payload TEXT NOT NULL)")?;
193 let changed = base.map(|base| {
194 base.entries()
195 .chain(manifest.entries())
196 .filter(|(path, _)| base.get(path) != manifest.get(path))
197 .map(|(path, _)| path.as_bytes().to_vec())
198 .collect::<BTreeSet<_>>()
199 });
200 let cached = if base.is_some() {
201 load_bindings(&transaction)?
202 } else {
203 BTreeMap::new()
204 };
205 let blob_connection = Connection::open(callgraph_blob_database)?;
206 let mut fact_invalidated = BTreeSet::new();
207 let mut fallback_count = 0;
208 let selected = match (&changed, base) {
209 (Some(changed), Some(base)) if !requires_full_resolution(base, manifest, changed) => {
210 let diff = resolution_facts::diff_inputs(base, manifest, changed, &blob_connection)?;
211 for (caller, binding) in &cached {
212 if !binding.consulted_facts.is_disjoint(&diff.changed) {
213 fact_invalidated.insert(caller.clone());
214 }
215 if diff.inputs_changed && binding.unattributed {
216 fallback_count += 1;
217 }
218 }
219 if diff.unknown != 0 || fallback_count != 0 {
220 fallback_count += diff.unknown;
221 log::warn!("views materialization: full resolution (reason=unattributed_reads count={fallback_count})");
222 None
223 } else {
224 let mut seeds = changed
227 .iter()
228 .filter(|path| {
229 !join::view_resolution_config(path) || {
230 let rel =
231 crate::views::RelPath::new((*path).clone()).expect("manifest path");
232 base.get(&rel).is_some() != manifest.get(&rel).is_some()
233 }
234 })
235 .cloned()
236 .collect::<BTreeSet<_>>();
237 seeds.extend(fact_invalidated.iter().map(|path| path.as_bytes().to_vec()));
238 if changed.iter().any(|path| {
239 join::view_resolution_config(path) && {
240 let rel = crate::views::RelPath::new(path.clone()).expect("manifest path");
241 base.get(&rel).is_some() != manifest.get(&rel).is_some()
242 }
243 }) {
244 seeds.insert(join::VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes().to_vec());
245 }
246 Some(dependent_closure(&transaction, &seeds)?)
247 }
248 }
249 _ => None,
250 };
251 let mut stats = MaterializeStats {
252 full_resolution: selected.is_none(),
253 unattributed_callers: fallback_count,
254 ..MaterializeStats::default()
255 };
256 profile.finish("load_bindings_select");
257 if let Some(changed) = &changed {
258 for path in changed {
259 let Ok(path) = std::str::from_utf8(path) else {
260 continue;
262 };
263 stats.deleted += transaction.execute("DELETE FROM edges WHERE ref_id IN (SELECT ref_id FROM refs WHERE caller_file = ?1)", [path])?;
266 stats.deleted +=
267 transaction.execute("DELETE FROM refs WHERE caller_file = ?1", [path])?;
268 stats.deleted +=
269 transaction.execute("DELETE FROM nodes WHERE file_path = ?1", [path])?;
270 stats.deleted += transaction.execute("DELETE FROM files WHERE path = ?1", [path])?;
271 stats.dependency_deleted += transaction
272 .execute("DELETE FROM file_dependencies WHERE file_path = ?1", [path])?;
273 stats.dependency_deleted +=
274 transaction.execute("DELETE FROM view_bindings WHERE file_path = ?1", [path])?;
275 }
276 } else {
277 for table in ["edges", "refs", "nodes", "files"] {
278 stats.deleted += transaction.execute(&format!("DELETE FROM {table}"), [])?;
279 }
280 }
281 if changed.is_none() {
282 for table in ["file_dependencies", "view_bindings"] {
283 stats.dependency_deleted += transaction.execute(&format!("DELETE FROM {table}"), [])?;
284 }
285 }
286 profile.finish("delete_rows");
287 let mut parsed = BTreeMap::new();
288 let mut nodes = HashMap::new();
289 let mut loaded_paths = BTreeSet::new();
290 for (path, entry) in manifest.entries() {
291 if changed
292 .as_ref()
293 .is_some_and(|paths| !paths.contains(path.as_bytes()))
294 {
295 continue;
296 }
297 let crate::views::ManifestEntry::Regular {
298 planes,
299 resolution_input,
300 ..
301 } = entry
302 else {
303 continue;
304 };
305 let Some(key) = planes.callgraph.as_deref() else {
306 continue;
307 };
308 let key_bytes = decode_manifest_full_key(key).ok_or_else(|| {
309 CallGraphStoreError::Unavailable(format!("invalid manifest callgraph key {key}"))
310 })?;
311 let payload = blob_connection
312 .query_row(
313 "SELECT payload FROM blob_payloads WHERE full_key = ?1",
314 [key_bytes],
315 |row| row.get::<_, Vec<u8>>(0),
316 )
317 .optional()?
318 .ok_or_else(|| {
319 CallGraphStoreError::Unavailable(format!("missing manifest callgraph blob {key}"))
320 })?;
321 let blob = join::CallgraphBlob::from_bytes(&payload)
322 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
323 let Some(parse) = blob.parse() else {
324 continue;
325 };
326 let path = String::from_utf8(path.as_bytes().to_vec()).map_err(|_| {
327 CallGraphStoreError::Unavailable("non-UTF-8 manifest callgraph path".to_string())
328 })?;
329 loaded_paths.insert(path.clone());
330 let write_owned = changed
331 .as_ref()
332 .is_none_or(|paths| paths.contains(path.as_bytes()));
333 if write_owned {
334 stats.inserted += transaction.execute(
335 "INSERT OR REPLACE INTO files
336 (path, content_hash, mtime_ns, size, lang, is_dead_code_root, is_public_api,
337 surface_fingerprint, indexed_at)
338 VALUES (?1, ?2, 0, 0, ?3, 0, 0, '', 0)",
339 params![path, key, parse.language],
340 )?;
341 }
342 for symbol in &parse.symbols {
343 let id = format!("view:{path}:{}:{}", symbol.scoped_name, symbol.ordinal);
344 if write_owned {
345 stats.inserted += transaction.execute(
346 "INSERT OR REPLACE INTO nodes
347 (id, file_path, name, scoped_name, kind, start_line, start_col, end_line,
348 end_col, range_ordinal, signature, exported, is_default_export,
349 is_type_like, is_callgraph_entry_point, provenance)
350 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, 0, ?12, ?14)",
351 params![
352 id,
353 path,
354 symbol.name,
355 symbol.scoped_name,
356 symbol.kind,
357 i64::from(symbol.start_line),
358 i64::from(symbol.start_col),
359 i64::from(symbol.end_line),
360 i64::from(symbol.end_col),
361 i64::from(symbol.ordinal),
362 symbol.signature,
363 i64::from(symbol.exported),
364 i64::from(symbol.is_default_export),
365 PROVENANCE_TREESITTER,
366 ],
367 )?;
368 }
369 nodes.insert((path.clone(), symbol.scoped_name.clone()), id.clone());
370 nodes
371 .entry((path.clone(), symbol.name.clone()))
372 .or_insert(id);
373 }
374 if !resolution_input {
375 parsed.insert(
376 path,
377 parse
378 .refs
379 .iter()
380 .fold(BTreeMap::new(), |mut by_ordinal, reference| {
381 by_ordinal
384 .entry(reference.ordinal)
385 .or_insert_with(|| reference.clone());
386 by_ordinal
387 }),
388 );
389 }
390 }
391
392 profile.finish("owned_blob_decode_and_insert");
393 let reader = ManifestViewBlobReader {
394 connection: &blob_connection,
395 };
396 let changed_strings = changed.as_ref().map_or_else(BTreeSet::new, |paths| {
397 paths
398 .iter()
399 .filter_map(|path| String::from_utf8(path.clone()).ok())
400 .collect()
401 });
402 let mut membership_changed = base.map_or_else(BTreeSet::new, |base| {
403 base.entries()
404 .chain(manifest.entries())
405 .filter(|(path, _)| {
406 base.get(path).map(crate::views::ManifestEntry::kind)
407 != manifest.get(path).map(crate::views::ManifestEntry::kind)
408 })
409 .filter_map(|(path, _)| String::from_utf8(path.as_bytes().to_vec()).ok())
410 .collect()
411 });
412 if membership_changed
413 .iter()
414 .any(|path| join::view_resolution_config(path.as_bytes()))
415 {
416 membership_changed.insert(join::VIEW_CONFIG_MEMBERSHIP_DOMAIN.into());
417 }
418 let joined = join::join_selected_manifest_reusing_surfaces(
419 manifest,
420 &reader,
421 selected.as_ref(),
422 &cached,
423 &changed_strings,
424 &membership_changed,
425 &fact_invalidated,
426 )
427 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
428 profile.finish("selected_join");
429 stats.unattributed_callers = joined
430 .bindings
431 .values()
432 .filter(|binding| binding.unattributed)
433 .count()
434 .max(stats.unattributed_callers);
435 stats.rebuilt_surface_entries = joined.rebuilt_surface_entries;
436 stats.decoded_caller_blobs = joined.decoded_caller_blobs;
437 stats.resolved_refs = joined.result.rows.len();
438 stats.resolved_bindings = joined.resolved_bindings;
439 stats.resolved_files = joined.resolved_callers.len();
440 stats.dependent_files = joined
441 .resolved_callers
442 .iter()
443 .filter(|path| !changed_strings.contains(*path) && changed.is_some())
444 .count();
445 for (path, binding) in &joined.bindings {
446 let old = cached.get(path).filter(|_| {
447 changed
448 .as_ref()
449 .is_some_and(|paths| !paths.contains(path.as_bytes()))
450 });
451 if old == Some(binding) {
452 continue;
453 }
454 let empty = BTreeSet::new();
455 let previous = old.map_or(&empty, |old| &old.dependencies);
456 for dependency in previous.difference(&binding.dependencies) {
457 stats.dependency_deleted += transaction.execute(
458 "DELETE FROM file_dependencies WHERE file_path=?1 AND dep_file=?2",
459 params![path, dependency],
460 )?;
461 }
462 for dependency in binding.dependencies.difference(previous) {
463 stats.dependency_inserted += transaction.execute(
464 "INSERT INTO file_dependencies(file_path, dep_file) VALUES(?1, ?2)",
465 params![path, dependency],
466 )?;
467 }
468 stats.dependency_deleted +=
469 transaction.execute("DELETE FROM view_bindings WHERE file_path=?1", [path])?;
470 let payload = serde_json::to_string(binding)
471 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
472 stats.dependency_inserted += transaction.execute(
473 "INSERT INTO view_bindings(file_path, payload) VALUES(?1, ?2)",
474 params![path, payload],
475 )?;
476 }
477 profile.finish("write_bindings");
478 {
481 let mut same_ref = transaction.prepare("SELECT EXISTS(SELECT 1 FROM refs WHERE ref_id = ?1 AND caller_node IS ?2
482 AND status = ?3 AND target_node IS ?4 AND target_file IS ?5 AND target_symbol IS ?6)")?;
483 let mut delete_edge = transaction.prepare("DELETE FROM edges WHERE ref_id = ?1")?;
484 let mut delete_ref = transaction.prepare("DELETE FROM refs WHERE ref_id = ?1")?;
485 let mut insert_ref = transaction.prepare(
486 "INSERT OR REPLACE INTO refs
487 (ref_id, caller_node, caller_file, kind, short_name, full_ref, module_path,
488 import_kind, local_name, requested_name, namespace_alias, wildcard, line,
489 byte_start, byte_end, status, target_node, target_file, target_symbol, provenance)
490 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14,
491 ?15, ?16, ?17, ?18, ?19, ?20)",
492 )?;
493 let mut insert_edge = transaction.prepare(
494 "INSERT OR REPLACE INTO edges
495 (edge_id, ref_id, source_node, target_node, target_file, target_symbol,
496 kind, line, provenance)
497 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'call', ?7, ?8)",
498 )?;
499 for row in joined.result.rows {
500 let caller_path = String::from_utf8(row.caller_path.clone()).map_err(|_| {
501 CallGraphStoreError::Unavailable("non-UTF-8 manifest caller path".to_string())
502 })?;
503 ensure_manifest_path(
504 &caller_path,
505 manifest,
506 &blob_connection,
507 &mut loaded_paths,
508 &mut parsed,
509 &mut nodes,
510 )?;
511 if let Some(target) = row
512 .target_path
513 .as_ref()
514 .and_then(|path| std::str::from_utf8(path).ok())
515 {
516 ensure_manifest_path(
517 target,
518 manifest,
519 &blob_connection,
520 &mut loaded_paths,
521 &mut parsed,
522 &mut nodes,
523 )?;
524 }
525 let Some(parse) = parsed.get(&caller_path) else {
526 continue;
527 };
528 let Some(reference) = parse.get(&row.ref_ordinal) else {
529 continue;
530 };
531 let caller_node = reference
532 .caller_symbol
533 .as_ref()
534 .and_then(|symbol| nodes.get(&(caller_path.clone(), symbol.clone())))
535 .cloned();
536 let target_path = row
537 .target_path
538 .as_ref()
539 .and_then(|path| String::from_utf8(path.clone()).ok());
540 let target_symbol = row.target_symbol.clone();
541 let target_node = target_path
542 .as_ref()
543 .zip(target_symbol.as_ref())
544 .and_then(|(path, symbol)| nodes.get(&(path.clone(), symbol.clone())))
545 .cloned();
546 let ref_id = format!("view:{caller_path}:{}", row.ref_ordinal);
547 let relink = changed
548 .as_ref()
549 .is_some_and(|paths| !paths.contains(caller_path.as_bytes()));
550 let status = if row.status == join::ResolutionStatus::Resolved {
551 "resolved"
552 } else {
553 "unresolved"
554 };
555 if relink {
556 let same: bool = same_ref.query_row(
561 params![
562 ref_id,
563 caller_node,
564 status,
565 target_node,
566 target_path,
567 target_symbol
568 ],
569 |row| row.get(0),
570 )?;
571 if same {
572 continue;
573 }
574 stats.relinked_deleted += delete_edge.execute([&ref_id])?;
575 stats.relinked_deleted += delete_ref.execute([&ref_id])?;
576 }
577 let inserted = if relink {
578 &mut stats.relinked_inserted
579 } else {
580 &mut stats.inserted
581 };
582 *inserted += insert_ref.execute(params![
583 ref_id,
584 caller_node,
585 caller_path,
586 manifest_ref_kind(row.kind),
587 reference.short_name,
588 reference.full_ref,
589 reference.module_path,
590 reference.import_kind,
591 reference.local_name,
592 reference.requested_name,
593 reference.namespace_alias,
594 i64::from(reference.wildcard),
595 i64::from(reference.line),
596 reference.byte_start as i64,
597 reference.byte_end as i64,
598 if row.status == join::ResolutionStatus::Resolved {
599 "resolved"
600 } else {
601 "unresolved"
602 },
603 target_node,
604 target_path,
605 target_symbol,
606 PROVENANCE_TREESITTER,
607 ])?;
608 if row.kind == join::BlobRefKind::Call {
609 if let (Some(source_node), Some(target_file), Some(target_symbol)) =
610 (caller_node, target_path, target_symbol)
611 {
612 *inserted += insert_edge.execute(params![
613 format!("edge:{ref_id}"),
614 ref_id,
615 source_node,
616 target_node,
617 target_file,
618 target_symbol,
619 i64::from(reference.line),
620 PROVENANCE_TREESITTER,
621 ])?;
622 }
623 }
624 }
625 }
626 profile.finish("emit_refs_edges");
627 set_meta_ready(&transaction, true)?;
628 transaction.execute(
629 "INSERT OR REPLACE INTO meta(k, v) VALUES('view_manifest_fingerprint', ?1)",
630 [fingerprint(manifest)?],
631 )?;
632 transaction.execute(
633 "INSERT OR REPLACE INTO meta(k, v) VALUES('view_materialization_version', ?1)",
634 [MATERIALIZATION_VERSION],
635 )?;
636 transaction.commit()?;
637 profile.finish("commit");
638 Ok((stats, profile.into_timings()))
639}
640
641struct ManifestViewBlobReader<'a> {
642 connection: &'a Connection,
643}
644
645impl join::ManifestBlobReader for ManifestViewBlobReader<'_> {
646 fn read_callgraph_blob(
647 &self,
648 full_key: &str,
649 ) -> std::result::Result<Option<Vec<u8>>, join::ManifestJoinError> {
650 let Some(key) = decode_manifest_full_key(full_key) else {
651 return Ok(None);
652 };
653 self.connection
654 .query_row(
655 "SELECT payload FROM blob_payloads WHERE full_key = ?1",
656 [key],
657 |row| row.get(0),
658 )
659 .optional()
660 .map_err(|error| join::ManifestJoinError::InvalidBlob(error.to_string()))
661 }
662}
663
664fn decode_manifest_full_key(value: &str) -> Option<Vec<u8>> {
665 if value.len() != 64 {
666 return None;
667 }
668 (0..value.len())
669 .step_by(2)
670 .map(|index| u8::from_str_radix(&value[index..index + 2], 16).ok())
671 .collect()
672}
673
674const fn manifest_ref_kind(kind: join::BlobRefKind) -> &'static str {
675 match kind {
676 join::BlobRefKind::Call => "call",
677 join::BlobRefKind::ValueRef => "value_ref",
678 join::BlobRefKind::Import => "import",
679 join::BlobRefKind::Module => "module",
680 join::BlobRefKind::Reexport => "reexport",
681 join::BlobRefKind::ExportAlias => "export_alias",
682 }
683}
684
685fn configure_materialization_connection(connection: &Connection) -> Result<()> {
686 connection.busy_timeout(Duration::from_secs(5))?;
687 connection.pragma_update(None, "journal_mode", "WAL")?;
688 connection.pragma_update(None, "synchronous", "FULL")?;
691 connection.pragma_update(None, "wal_autocheckpoint", 0)?;
694 Ok(())
695}
696
697#[cfg(test)]
698mod tests;
699
700fn load_bindings(
701 connection: &Connection,
702) -> Result<BTreeMap<String, join::ViewBindingDependencies>> {
703 let mut statement = connection.prepare("SELECT file_path, payload FROM view_bindings")?;
704 let rows = statement.query_map([], |row| {
705 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
706 })?;
707 rows.map(|row| {
708 let (path, payload) = row?;
709 let binding = serde_json::from_str(&payload)
710 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
711 Ok((path, binding))
712 })
713 .collect()
714}
715
716fn dependent_closure(
717 connection: &Connection,
718 changed: &BTreeSet<Vec<u8>>,
719) -> Result<BTreeSet<String>> {
720 let mut selected = changed
721 .iter()
722 .filter_map(|path| String::from_utf8(path.clone()).ok())
723 .collect::<BTreeSet<_>>();
724 if !changed.is_empty() {
725 selected.insert(join::VIEW_RUST_MODULE_DOMAIN.to_string());
729 }
730 let mut pending = selected.iter().cloned().collect::<Vec<_>>();
731 let mut dependents =
732 connection.prepare("SELECT file_path FROM file_dependencies WHERE dep_file = ?1")?;
733 while let Some(path) = pending.pop() {
734 for caller in dependents.query_map([path], |row| row.get::<_, String>(0))? {
735 let caller = caller?;
736 if selected.insert(caller.clone()) {
737 pending.push(caller);
738 }
739 }
740 }
741 Ok(selected)
742}
743
744fn requires_full_resolution(
745 base: &crate::views::Manifest,
746 next: &crate::views::Manifest,
747 changed: &BTreeSet<Vec<u8>>,
748) -> bool {
749 changed.iter().any(|path| {
750 base.entries()
751 .chain(next.entries())
752 .any(|(candidate, entry)| {
753 candidate.as_bytes() == path
754 && matches!(
755 entry,
756 crate::views::ManifestEntry::Synthetic { .. }
757 | crate::views::ManifestEntry::Symlink { .. }
758 | crate::views::ManifestEntry::Gitlink { .. }
759 )
760 })
761 })
762}
763
764fn ensure_manifest_path(
768 path: &str,
769 manifest: &crate::views::Manifest,
770 blobs: &Connection,
771 loaded: &mut BTreeSet<String>,
772 parsed: &mut BTreeMap<String, BTreeMap<u32, join::BlobRef>>,
773 nodes: &mut HashMap<(String, String), String>,
774) -> Result<()> {
775 if !loaded.insert(path.to_string()) {
776 return Ok(());
777 }
778 let Ok(rel) = crate::views::RelPath::new(path.as_bytes().to_vec()) else {
779 return Ok(());
780 };
781 let Some(crate::views::ManifestEntry::Regular {
782 planes,
783 resolution_input,
784 ..
785 }) = manifest.get(&rel)
786 else {
787 return Ok(());
788 };
789 let Some(key) = &planes.callgraph else {
790 return Ok(());
791 };
792 let key_bytes = decode_manifest_full_key(key).ok_or_else(|| {
793 CallGraphStoreError::Unavailable(format!("invalid manifest callgraph key {key}"))
794 })?;
795 let payload = blobs
796 .query_row(
797 "SELECT payload FROM blob_payloads WHERE full_key=?1",
798 [key_bytes],
799 |row| row.get::<_, Vec<u8>>(0),
800 )
801 .optional()?
802 .ok_or_else(|| {
803 CallGraphStoreError::Unavailable(format!("missing manifest callgraph blob {key}"))
804 })?;
805 let blob = join::CallgraphBlob::from_bytes(&payload)
806 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
807 let Some(parse) = blob.parse() else {
808 return Ok(());
809 };
810 for symbol in &parse.symbols {
811 let id = format!("view:{path}:{}:{}", symbol.scoped_name, symbol.ordinal);
812 nodes.insert((path.to_string(), symbol.scoped_name.clone()), id.clone());
813 nodes
814 .entry((path.to_string(), symbol.name.clone()))
815 .or_insert(id);
816 }
817 if !resolution_input {
818 parsed.insert(
819 path.to_string(),
820 parse
821 .refs
822 .iter()
823 .fold(BTreeMap::new(), |mut by_ordinal, reference| {
824 by_ordinal
825 .entry(reference.ordinal)
826 .or_insert_with(|| reference.clone());
827 by_ordinal
828 }),
829 );
830 }
831 Ok(())
832}