1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet, HashMap};
3use std::path::Path;
4use std::sync::Arc;
5use std::time::Duration;
6
7use rusqlite::{params, Connection, OptionalExtension};
8
9use crate::callgraph_store::{
10 initialize_schema, join, set_meta_ready, CallGraphStoreError, Result, PROVENANCE_TREESITTER,
11};
12
13pub fn materialize_manifest_view_database(
15 database_path: &Path,
16 callgraph_blob_database: &Path,
17 manifest: &crate::views::Manifest,
18) -> Result<()> {
19 materialize(database_path, callgraph_blob_database, manifest, None).map(|_| ())
20}
21
22#[derive(Debug, Default, Clone, PartialEq, Eq)]
26pub struct MaterializeStats {
27 pub delete_paths_touched: usize,
28 pub deleted: usize,
29 pub inserted: usize,
30 pub relinked_deleted: usize,
31 pub relinked_inserted: usize,
32 pub dependency_deleted: usize,
33 pub dependency_inserted: usize,
34 pub surface_deleted: usize,
35 pub surface_inserted: usize,
36 pub dependent_files: usize,
37 pub resolved_files: usize,
38 pub resolved_refs: usize,
39 pub resolved_bindings: usize,
40 pub emission_lookup_queries: usize,
41 pub rebuilt_surface_entries: usize,
42 pub decoded_caller_blobs: usize,
43 pub full_resolution: bool,
44 pub unattributed_callers: usize,
45}
46
47impl MaterializeStats {
48 pub fn graph_rows_written(&self) -> usize {
49 self.deleted + self.inserted + self.relinked_deleted + self.relinked_inserted
50 }
51
52 pub fn rows_written(&self) -> usize {
53 self.graph_rows_written()
54 + self.dependency_deleted
55 + self.dependency_inserted
56 + self.surface_deleted
57 + self.surface_inserted
58 }
59}
60
61pub fn apply_manifest_diff(
66 database_path: &Path,
67 base_manifest: &crate::views::Manifest,
68 new_manifest: &crate::views::Manifest,
69 callgraph_blob_database: &Path,
70) -> Result<MaterializeStats> {
71 apply_manifest_diff_profiled(
72 database_path,
73 base_manifest,
74 new_manifest,
75 callgraph_blob_database,
76 )
77 .map(|(stats, _)| stats)
78}
79
80pub(crate) fn apply_manifest_diff_profiled(
81 database_path: &Path,
82 base_manifest: &crate::views::Manifest,
83 new_manifest: &crate::views::Manifest,
84 callgraph_blob_database: &Path,
85) -> Result<(MaterializeStats, profile::PhaseTimings)> {
86 materialize(
87 database_path,
88 callgraph_blob_database,
89 new_manifest,
90 Some(base_manifest),
91 )
92}
93
94pub(crate) mod profile;
95mod resolution_facts;
96
97const MATERIALIZATION_VERSION: &str = "6";
98
99fn fingerprint(manifest: &crate::views::Manifest) -> Result<String> {
100 let bytes = manifest
101 .to_json_bytes()
102 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
103 Ok(blake3::hash(&bytes).to_hex().to_string())
104}
105
106pub(super) fn manifest_callgraph_equivalent(
107 left: &crate::views::Manifest,
108 right: &crate::views::Manifest,
109) -> bool {
110 let mut left = left.entries();
111 let mut right = right.entries();
112 loop {
113 match (left.next(), right.next()) {
114 (None, None) => return true,
115 (Some((left_path, left_entry)), Some((right_path, right_entry)))
116 if left_path == right_path
117 && match (left_entry, right_entry) {
118 (
119 crate::views::ManifestEntry::Regular {
120 mode: left_mode,
121 planes: left_planes,
122 resolution_input: left_resolution_input,
123 },
124 crate::views::ManifestEntry::Regular {
125 mode: right_mode,
126 planes: right_planes,
127 resolution_input: right_resolution_input,
128 },
129 ) => {
130 left_mode == right_mode
131 && left_resolution_input == right_resolution_input
132 && left_planes.callgraph == right_planes.callgraph
133 }
134 _ => left_entry == right_entry,
135 } => {}
136 _ => return false,
137 }
138 }
139}
140
141fn materialize(
142 database_path: &Path,
143 callgraph_blob_database: &Path,
144 manifest: &crate::views::Manifest,
145 mut base: Option<&crate::views::Manifest>,
146) -> Result<(MaterializeStats, profile::PhaseTimings)> {
147 let write_probe = profile::WriteProbe::start(database_path);
148 let mut profile = profile::PhaseTimer::new(if base.is_some() {
149 "incremental"
150 } else {
151 "cold"
152 });
153 let mut connection = if base.is_some() {
154 Connection::open_with_flags(database_path, rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE)?
155 } else {
156 Connection::open(database_path)?
157 };
158 configure_materialization_connection(&connection)?;
159 if base.is_none() {
160 initialize_schema(&connection)?;
161 }
162 let transaction =
163 connection.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
164 if let Some(base_manifest) = base {
165 let recorded: Option<String> = transaction
166 .query_row(
167 "SELECT v FROM meta WHERE k = 'view_manifest_fingerprint'",
168 [],
169 |row| row.get(0),
170 )
171 .optional()?;
172 let version: Option<String> = transaction
173 .query_row(
174 "SELECT v FROM meta WHERE k = 'view_materialization_version'",
175 [],
176 |row| row.get(0),
177 )
178 .optional()?;
179 if recorded.is_none() && version.is_none() {
180 base = None;
183 } else if recorded.as_deref() != Some(fingerprint(base_manifest)?.as_str()) {
184 return Err(CallGraphStoreError::Unavailable(
185 "derived manifest fingerprint mismatch; cold materialization required".into(),
186 ));
187 } else if version.as_deref() != Some(MATERIALIZATION_VERSION) {
188 base = None;
189 } else if base_manifest == manifest {
190 profile.finish("load_bindings_select");
191 return Ok((MaterializeStats::default(), profile.into_timings()));
192 } else if manifest_callgraph_equivalent(base_manifest, manifest) {
193 profile.finish("load_bindings_select");
194 transaction.execute(
195 "INSERT OR REPLACE INTO meta(k, v) VALUES('view_manifest_fingerprint', ?1)",
196 [fingerprint(manifest)?],
197 )?;
198 transaction.commit()?;
199 profile.finish("commit");
200 return Ok((MaterializeStats::default(), profile.into_timings()));
201 }
202 }
203 transaction.execute_batch(
204 "CREATE TABLE IF NOT EXISTS view_bindings (
205 file_path TEXT PRIMARY KEY,
206 payload TEXT NOT NULL
207 );
208 CREATE TABLE IF NOT EXISTS view_file_surfaces (
209 file_path TEXT PRIMARY KEY,
210 payload TEXT NOT NULL
211 )",
212 )?;
213 let changed = base.map(|base| {
214 base.entries()
215 .chain(manifest.entries())
216 .filter(|(path, _)| base.get(path) != manifest.get(path))
217 .map(|(path, _)| path.as_bytes().to_vec())
218 .collect::<BTreeSet<_>>()
219 });
220 let blob_connection = Connection::open(callgraph_blob_database)?;
221 let reader = ManifestViewBlobReader::new(&blob_connection);
222 let mut cached_for_invalidation = None;
223 let mut fact_invalidated = BTreeSet::new();
224 let mut fallback_count = 0;
225 let selected = match (&changed, base) {
226 (Some(changed), Some(base)) if !requires_full_resolution(base, manifest, changed) => {
227 let diff = resolution_facts::diff_inputs(base, manifest, changed, &blob_connection)?;
228 if diff.unknown == 0 && (!diff.changed.is_empty() || diff.inputs_changed) {
229 let cached = load_bindings(&transaction)?;
230 for (caller, binding) in &cached {
231 if !binding.consulted_facts.is_disjoint(&diff.changed) {
232 fact_invalidated.insert(caller.clone());
233 }
234 if diff.inputs_changed && binding.unattributed {
235 fallback_count += 1;
236 }
237 }
238 cached_for_invalidation = Some(cached);
239 }
240 if diff.unknown != 0 || fallback_count != 0 {
241 fallback_count += diff.unknown;
242 log::warn!("views materialization: full resolution (reason=unattributed_reads count={fallback_count})");
243 None
244 } else {
245 let mut seeds = changed
248 .iter()
249 .filter(|path| {
250 !join::view_resolution_config(path) || {
251 let rel =
252 crate::views::RelPath::new((*path).clone()).expect("manifest path");
253 base.get(&rel).is_some() != manifest.get(&rel).is_some()
254 }
255 })
256 .cloned()
257 .collect::<BTreeSet<_>>();
258 seeds.extend(fact_invalidated.iter().map(|path| path.as_bytes().to_vec()));
259 if changed.iter().any(|path| {
260 join::view_resolution_config(path) && {
261 let rel = crate::views::RelPath::new(path.clone()).expect("manifest path");
262 base.get(&rel).is_some() != manifest.get(&rel).is_some()
263 }
264 }) {
265 seeds.insert(join::VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes().to_vec());
266 }
267 Some(dependent_closure(&transaction, &seeds)?)
268 }
269 }
270 _ => None,
271 };
272 let cached = if base.is_none() {
273 BTreeMap::new()
274 } else if let Some(cached) = cached_for_invalidation {
275 cached
276 } else if let Some(selected) = &selected {
277 load_bindings_for_selection(
278 &transaction,
279 selected,
280 changed.as_ref().expect("incremental selection has a diff"),
281 )?
282 } else {
283 load_bindings(&transaction)?
284 };
285 let mut stats = MaterializeStats {
286 full_resolution: selected.is_none(),
287 unattributed_callers: fallback_count,
288 ..MaterializeStats::default()
289 };
290 profile.finish("load_bindings_select");
291 if let Some(changed) = &changed {
292 transaction.execute_batch(
293 "CREATE TEMP TABLE changed_view_paths (
294 path TEXT PRIMARY KEY
295 ) WITHOUT ROWID",
296 )?;
297 {
298 let mut insert =
299 transaction.prepare("INSERT INTO changed_view_paths(path) VALUES(?1)")?;
300 for path in changed {
301 let Ok(path) = std::str::from_utf8(path) else {
302 continue;
304 };
305 stats.delete_paths_touched += insert.execute([path])?;
306 }
307 }
308 stats.deleted += transaction.execute(
312 "DELETE FROM edges WHERE ref_id IN (
313 SELECT refs.ref_id FROM changed_view_paths
314 CROSS JOIN refs INDEXED BY idx_refs_caller_file
315 ON refs.caller_file = changed_view_paths.path
316 )",
317 [],
318 )?;
319 stats.deleted += transaction.execute(
320 "DELETE FROM refs WHERE caller_file IN (SELECT path FROM changed_view_paths)",
321 [],
322 )?;
323 stats.deleted += transaction.execute(
324 "DELETE FROM nodes WHERE file_path IN (SELECT path FROM changed_view_paths)",
325 [],
326 )?;
327 stats.deleted += transaction.execute(
328 "DELETE FROM files WHERE path IN (SELECT path FROM changed_view_paths)",
329 [],
330 )?;
331 stats.dependency_deleted += transaction.execute(
332 "DELETE FROM file_dependencies WHERE file_path IN (SELECT path FROM changed_view_paths)",
333 [],
334 )?;
335 stats.dependency_deleted += transaction.execute(
336 "DELETE FROM view_bindings WHERE file_path IN (SELECT path FROM changed_view_paths)",
337 [],
338 )?;
339 stats.surface_deleted += transaction.execute(
340 "DELETE FROM view_file_surfaces WHERE file_path IN (SELECT path FROM changed_view_paths)",
341 [],
342 )?;
343 } else {
344 for table in ["edges", "refs", "nodes", "files"] {
345 stats.deleted += transaction.execute(&format!("DELETE FROM {table}"), [])?;
346 }
347 }
348 if changed.is_none() {
349 for table in ["file_dependencies", "view_bindings"] {
350 stats.dependency_deleted += transaction.execute(&format!("DELETE FROM {table}"), [])?;
351 }
352 stats.surface_deleted += transaction.execute("DELETE FROM view_file_surfaces", [])?;
353 }
354 profile.finish("delete_rows");
355 let mut parsed = BTreeMap::new();
356 let mut nodes = HashMap::<String, HashMap<String, String>>::new();
357 let mut loaded_paths = BTreeSet::new();
358 for (path, entry) in manifest.entries() {
359 if changed
360 .as_ref()
361 .is_some_and(|paths| !paths.contains(path.as_bytes()))
362 {
363 continue;
364 }
365 let crate::views::ManifestEntry::Regular {
366 planes,
367 resolution_input,
368 ..
369 } = entry
370 else {
371 continue;
372 };
373 let Some(key) = planes.callgraph.as_deref() else {
374 continue;
375 };
376 let blob = reader
377 .read_decoded(key)
378 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?
379 .ok_or_else(|| {
380 CallGraphStoreError::Unavailable(format!("missing manifest callgraph blob {key}"))
381 })?;
382 let Some(parse) = blob.parse() else {
383 continue;
384 };
385 let path = String::from_utf8(path.as_bytes().to_vec()).map_err(|_| {
386 CallGraphStoreError::Unavailable("non-UTF-8 manifest callgraph path".to_string())
387 })?;
388 loaded_paths.insert(path.clone());
389 let write_owned = changed
390 .as_ref()
391 .is_none_or(|paths| paths.contains(path.as_bytes()));
392 if write_owned {
393 stats.inserted += transaction.execute(
394 "INSERT OR REPLACE INTO files
395 (path, content_hash, mtime_ns, size, lang, is_dead_code_root, is_public_api,
396 surface_fingerprint, indexed_at)
397 VALUES (?1, ?2, 0, 0, ?3, 0, 0, '', 0)",
398 params![path, key, parse.language],
399 )?;
400 }
401 let file_nodes = nodes.entry(path.clone()).or_default();
402 for symbol in &parse.symbols {
403 let id = format!("view:{path}:{}:{}", symbol.scoped_name, symbol.ordinal);
404 if write_owned {
405 stats.inserted += transaction.execute(
406 "INSERT OR REPLACE INTO nodes
407 (id, file_path, name, scoped_name, kind, start_line, start_col, end_line,
408 end_col, range_ordinal, signature, exported, is_default_export,
409 is_type_like, is_callgraph_entry_point, provenance)
410 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, 0, ?12, ?14)",
411 params![
412 id,
413 path,
414 symbol.name,
415 symbol.scoped_name,
416 symbol.kind,
417 i64::from(symbol.start_line),
418 i64::from(symbol.start_col),
419 i64::from(symbol.end_line),
420 i64::from(symbol.end_col),
421 i64::from(symbol.ordinal),
422 symbol.signature,
423 i64::from(symbol.exported),
424 i64::from(symbol.is_default_export),
425 PROVENANCE_TREESITTER,
426 ],
427 )?;
428 }
429 file_nodes.insert(symbol.scoped_name.clone(), id.clone());
430 file_nodes.entry(symbol.name.clone()).or_insert(id);
431 }
432 if !resolution_input {
433 parsed.insert(path, EmissionParse::new(blob));
434 }
435 }
436
437 profile.finish("owned_blob_decode_and_insert");
438 let changed_strings = changed.as_ref().map_or_else(BTreeSet::new, |paths| {
439 paths
440 .iter()
441 .filter_map(|path| String::from_utf8(path.clone()).ok())
442 .collect()
443 });
444 let mut membership_changed = base.map_or_else(BTreeSet::new, |base| {
445 base.entries()
446 .chain(manifest.entries())
447 .filter(|(path, _)| {
448 base.get(path).map(crate::views::ManifestEntry::kind)
449 != manifest.get(path).map(crate::views::ManifestEntry::kind)
450 })
451 .filter_map(|(path, _)| String::from_utf8(path.as_bytes().to_vec()).ok())
452 .collect()
453 });
454 if membership_changed
455 .iter()
456 .any(|path| join::view_resolution_config(path.as_bytes()))
457 {
458 membership_changed.insert(join::VIEW_CONFIG_MEMBERSHIP_DOMAIN.into());
459 }
460 let joined = join::join_selected_manifest_reusing_surfaces(
461 manifest,
462 &reader,
463 selected.as_ref(),
464 &cached,
465 &changed_strings,
466 &membership_changed,
467 &fact_invalidated,
468 )
469 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
470 profile.finish("selected_join");
471 stats.unattributed_callers = joined
472 .bindings
473 .values()
474 .filter(|binding| binding.unattributed)
475 .count()
476 .max(stats.unattributed_callers);
477 stats.rebuilt_surface_entries = joined.rebuilt_surface_entries;
478 stats.decoded_caller_blobs = joined.decoded_caller_blobs;
479 stats.resolved_refs = joined.result.rows.len();
480 stats.resolved_bindings = joined.resolved_bindings;
481 stats.resolved_files = joined.resolved_callers.len();
482 stats.dependent_files = joined
483 .resolved_callers
484 .iter()
485 .filter(|path| !changed_strings.contains(*path) && changed.is_some())
486 .count();
487 for (path, binding) in &joined.bindings {
488 if joined.rebuilt_surface_paths.contains(path) {
489 if let Some(payload) = binding
490 .surface_json()
491 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?
492 {
493 stats.surface_inserted += transaction.execute(
494 "INSERT OR REPLACE INTO view_file_surfaces(file_path, payload) VALUES(?1, ?2)",
495 params![path, payload],
496 )?;
497 }
498 }
499 let old = cached.get(path).filter(|_| {
500 changed
501 .as_ref()
502 .is_some_and(|paths| !paths.contains(path.as_bytes()))
503 });
504 if old == Some(binding) {
505 continue;
506 }
507 let empty = BTreeSet::new();
508 let previous = old.map_or(&empty, |old| &old.dependencies);
509 for dependency in previous.difference(&binding.dependencies) {
510 stats.dependency_deleted += transaction.execute(
511 "DELETE FROM file_dependencies WHERE file_path=?1 AND dep_file=?2",
512 params![path, dependency],
513 )?;
514 }
515 for dependency in binding.dependencies.difference(previous) {
516 stats.dependency_inserted += transaction.execute(
517 "INSERT INTO file_dependencies(file_path, dep_file) VALUES(?1, ?2)",
518 params![path, dependency],
519 )?;
520 }
521 stats.dependency_deleted +=
522 transaction.execute("DELETE FROM view_bindings WHERE file_path=?1", [path])?;
523 let payload = serde_json::to_string(binding)
524 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
525 stats.dependency_inserted += transaction.execute(
526 "INSERT INTO view_bindings(file_path, payload) VALUES(?1, ?2)",
527 params![path, payload],
528 )?;
529 }
530 profile.finish("write_bindings");
531 {
534 type ExistingRef = (
535 Option<String>,
536 String,
537 Option<String>,
538 Option<String>,
539 Option<String>,
540 );
541 let mut identical_refs = 0;
542 let mut existing = HashMap::<String, HashMap<String, ExistingRef>>::new();
543 let mut load_refs = transaction.prepare("SELECT ref_id, caller_node, status, target_node, target_file, target_symbol FROM refs WHERE caller_file = ?1")?;
544 let mut delete_edge = transaction.prepare("DELETE FROM edges WHERE ref_id = ?1")?;
545 let mut delete_ref = transaction.prepare("DELETE FROM refs WHERE ref_id = ?1")?;
546 let mut insert_ref = transaction.prepare(
547 "INSERT OR REPLACE INTO refs
548 (ref_id, caller_node, caller_file, kind, short_name, full_ref, module_path,
549 import_kind, local_name, requested_name, namespace_alias, wildcard, line,
550 byte_start, byte_end, status, target_node, target_file, target_symbol, provenance)
551 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14,
552 ?15, ?16, ?17, ?18, ?19, ?20)",
553 )?;
554 let mut insert_edge = transaction.prepare(
555 "INSERT OR REPLACE INTO edges
556 (edge_id, ref_id, source_node, target_node, target_file, target_symbol,
557 kind, line, provenance)
558 VALUES (?1, ?2, ?3, ?4, ?5, ?6, 'call', ?7, ?8)",
559 )?;
560 #[cfg(test)]
561 let mut control_lookup = transaction.prepare("SELECT EXISTS(SELECT 1 FROM refs WHERE ref_id=?1 AND caller_node IS ?2 AND status=?3 AND target_node IS ?4 AND target_file IS ?5 AND target_symbol IS ?6)")?;
562 for row in joined.result.rows {
563 let caller_path = String::from_utf8(row.caller_path).map_err(|_| {
564 CallGraphStoreError::Unavailable("non-UTF-8 manifest caller path".to_string())
565 })?;
566 ensure_manifest_path(
567 &caller_path,
568 manifest,
569 &reader,
570 &mut loaded_paths,
571 &mut parsed,
572 &mut nodes,
573 )?;
574 let target_path = row
575 .target_path
576 .and_then(|path| String::from_utf8(path).ok());
577 if let Some(target) = target_path.as_deref() {
578 ensure_manifest_path(
579 target,
580 manifest,
581 &reader,
582 &mut loaded_paths,
583 &mut parsed,
584 &mut nodes,
585 )?;
586 }
587 let Some(parse) = parsed.get(&caller_path) else {
588 continue;
589 };
590 let Some(reference) = parse.reference(row.ref_ordinal) else {
591 continue;
592 };
593 let caller_node = reference
594 .caller_symbol
595 .as_ref()
596 .and_then(|symbol| nodes.get(&caller_path)?.get(symbol))
597 .cloned();
598 let target_symbol = row.target_symbol;
599 let target_node = target_path
600 .as_ref()
601 .zip(target_symbol.as_ref())
602 .and_then(|(path, symbol)| nodes.get(path)?.get(symbol))
603 .cloned();
604 let ref_id = format!("view:{caller_path}:{}", row.ref_ordinal);
605 let relink = changed
606 .as_ref()
607 .is_some_and(|paths| !paths.contains(caller_path.as_bytes()));
608 let status = if row.status == join::ResolutionStatus::Resolved {
609 "resolved"
610 } else {
611 "unresolved"
612 };
613 if relink {
614 #[cfg(test)]
619 let control_same = if tests::PER_REFERENCE_LOOKUP.with(|enabled| enabled.get()) {
620 stats.emission_lookup_queries += 1;
621 Some(control_lookup.query_row(
622 params![
623 ref_id,
624 caller_node,
625 status,
626 target_node,
627 target_path,
628 target_symbol
629 ],
630 |row| row.get::<_, bool>(0),
631 )?)
632 } else {
633 None
634 };
635 #[cfg(not(test))]
636 let control_same: Option<bool> = None;
637 if control_same.is_none() && !existing.contains_key(&caller_path) {
638 stats.emission_lookup_queries += 1;
639 let rows = load_refs
640 .query_map([&caller_path], |row| {
641 Ok((
642 row.get::<_, String>(0)?,
643 (
644 row.get(1)?,
645 row.get(2)?,
646 row.get(3)?,
647 row.get(4)?,
648 row.get(5)?,
649 ),
650 ))
651 })?
652 .collect::<std::result::Result<HashMap<String, ExistingRef>, _>>()?;
653 existing.insert(caller_path.clone(), rows);
654 }
655 let same = control_same.unwrap_or_else(|| {
656 existing[&caller_path].get(&ref_id).is_some_and(|old| {
657 old.0 == caller_node
658 && old.1 == status
659 && old.2 == target_node
660 && old.3 == target_path
661 && old.4 == target_symbol
662 })
663 });
664 if same {
665 identical_refs += 1;
666 continue;
667 }
668 stats.relinked_deleted += delete_edge.execute([&ref_id])?;
669 stats.relinked_deleted += delete_ref.execute([&ref_id])?;
670 }
671 let inserted = if relink {
672 &mut stats.relinked_inserted
673 } else {
674 &mut stats.inserted
675 };
676 *inserted += insert_ref.execute(params![
677 ref_id,
678 caller_node,
679 caller_path,
680 manifest_ref_kind(row.kind),
681 reference.short_name,
682 reference.full_ref,
683 reference.module_path,
684 reference.import_kind,
685 reference.local_name,
686 reference.requested_name,
687 reference.namespace_alias,
688 i64::from(reference.wildcard),
689 i64::from(reference.line),
690 reference.byte_start as i64,
691 reference.byte_end as i64,
692 if row.status == join::ResolutionStatus::Resolved {
693 "resolved"
694 } else {
695 "unresolved"
696 },
697 target_node,
698 target_path,
699 target_symbol,
700 PROVENANCE_TREESITTER,
701 ])?;
702 if row.kind == join::BlobRefKind::Call {
703 if let (Some(source_node), Some(target_file), Some(target_symbol)) =
704 (caller_node, target_path, target_symbol)
705 {
706 *inserted += insert_edge.execute(params![
707 format!("edge:{ref_id}"),
708 ref_id,
709 source_node,
710 target_node,
711 target_file,
712 target_symbol,
713 i64::from(reference.line),
714 PROVENANCE_TREESITTER,
715 ])?;
716 }
717 }
718 }
719 if std::env::var_os("AFT_VIEW_PROFILE").is_some() {
720 eprintln!(
721 "view_profile emission identical_refs_skipped={identical_refs} stats={stats:?}"
722 );
723 }
724 }
725 profile.finish("emit_refs_edges");
726 set_meta_ready(&transaction, true)?;
727 transaction.execute(
728 "INSERT OR REPLACE INTO meta(k, v) VALUES('view_manifest_fingerprint', ?1)",
729 [fingerprint(manifest)?],
730 )?;
731 transaction.execute(
732 "INSERT OR REPLACE INTO meta(k, v) VALUES('view_materialization_version', ?1)",
733 [MATERIALIZATION_VERSION],
734 )?;
735 transaction.commit()?;
736 profile.finish("commit");
737 if let Some(probe) = write_probe {
738 probe.finish(&connection, database_path);
739 }
740 drop((reader, parsed, nodes, cached));
743 drop(joined.bindings);
744 profile.finish("cleanup_memory");
745 drop((blob_connection, connection));
746 profile.finish("cleanup_connections");
747 Ok((stats, profile.into_timings()))
748}
749
750struct ManifestViewBlobReader<'a> {
751 connection: &'a Connection,
752 decoded: RefCell<HashMap<String, Arc<join::CallgraphBlob>>>,
753}
754
755impl<'a> ManifestViewBlobReader<'a> {
756 fn new(connection: &'a Connection) -> Self {
757 Self {
758 connection,
759 decoded: RefCell::new(HashMap::new()),
760 }
761 }
762
763 fn read_payload(
764 &self,
765 full_key: &str,
766 ) -> std::result::Result<Option<Vec<u8>>, join::ManifestJoinError> {
767 let Some(key) = decode_manifest_full_key(full_key) else {
768 return Ok(None);
769 };
770 self.connection
771 .query_row(
772 "SELECT payload FROM blob_payloads WHERE full_key = ?1",
773 [key],
774 |row| row.get(0),
775 )
776 .optional()
777 .map_err(|error| join::ManifestJoinError::InvalidBlob(error.to_string()))
778 }
779
780 fn read_decoded(
781 &self,
782 full_key: &str,
783 ) -> std::result::Result<Option<Arc<join::CallgraphBlob>>, join::ManifestJoinError> {
784 if let Some(blob) = self.decoded.borrow().get(full_key) {
785 return Ok(Some(blob.clone()));
786 }
787 let Some(payload) = self.read_payload(full_key)? else {
788 return Ok(None);
789 };
790 let blob = Arc::new(join::CallgraphBlob::from_bytes(&payload)?);
791 self.decoded
792 .borrow_mut()
793 .insert(full_key.to_string(), blob.clone());
794 Ok(Some(blob))
795 }
796}
797
798impl join::ManifestBlobReader for ManifestViewBlobReader<'_> {
799 fn read_callgraph_blob(
800 &self,
801 full_key: &str,
802 ) -> std::result::Result<Option<Vec<u8>>, join::ManifestJoinError> {
803 self.read_payload(full_key)
804 }
805
806 fn read_callgraph_blob_decoded(
807 &self,
808 full_key: &str,
809 ) -> std::result::Result<Option<Arc<join::CallgraphBlob>>, join::ManifestJoinError> {
810 self.read_decoded(full_key)
811 }
812}
813
814fn decode_manifest_full_key(value: &str) -> Option<Vec<u8>> {
815 if value.len() != 64 {
816 return None;
817 }
818 (0..value.len())
819 .step_by(2)
820 .map(|index| u8::from_str_radix(&value[index..index + 2], 16).ok())
821 .collect()
822}
823
824const fn manifest_ref_kind(kind: join::BlobRefKind) -> &'static str {
825 match kind {
826 join::BlobRefKind::Call => "call",
827 join::BlobRefKind::ValueRef => "value_ref",
828 join::BlobRefKind::Import => "import",
829 join::BlobRefKind::Module => "module",
830 join::BlobRefKind::Reexport => "reexport",
831 join::BlobRefKind::ExportAlias => "export_alias",
832 }
833}
834
835fn configure_materialization_connection(connection: &Connection) -> Result<()> {
836 connection.busy_timeout(Duration::from_secs(5))?;
837 connection.pragma_update(None, "journal_mode", "WAL")?;
838 connection.pragma_update(None, "synchronous", "FULL")?;
841 connection.pragma_update(None, "wal_autocheckpoint", 0)?;
844 connection.pragma_update(None, "mmap_size", 268_435_456)?;
853 connection.pragma_update(None, "temp_store", "MEMORY")?;
854 Ok(())
855}
856
857#[cfg(test)]
858mod tests;
859
860fn load_bindings(
861 connection: &Connection,
862) -> Result<BTreeMap<String, join::ViewBindingDependencies>> {
863 let mut statement = connection.prepare("SELECT file_path, payload FROM view_bindings")?;
864 let rows = statement.query_map([], |row| {
865 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
866 })?;
867 let mut bindings = rows
868 .map(|row| {
869 let (path, payload) = row?;
870 let binding: join::ViewBindingDependencies = serde_json::from_str(&payload)
871 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
872 Ok((path, binding))
873 })
874 .collect::<Result<BTreeMap<_, _>>>()?;
875 for (path, surface) in load_surfaces(connection)? {
876 if let Some(binding) = bindings.get_mut(&path) {
877 binding.copy_surface_from(&surface);
878 }
879 }
880 Ok(bindings)
881}
882
883fn load_surfaces(
884 connection: &Connection,
885) -> Result<BTreeMap<String, join::ViewBindingDependencies>> {
886 let mut statement = connection.prepare("SELECT file_path, payload FROM view_file_surfaces")?;
887 let rows = statement.query_map([], |row| {
888 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
889 })?;
890 rows.map(|row| {
891 let (path, surface) = row?;
892 let binding = join::ViewBindingDependencies::from_surface_json(&surface)
893 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
894 Ok((path, binding))
895 })
896 .collect()
897}
898
899fn load_bindings_for_selection(
900 connection: &Connection,
901 selected: &BTreeSet<String>,
902 changed: &BTreeSet<Vec<u8>>,
903) -> Result<BTreeMap<String, join::ViewBindingDependencies>> {
904 connection.execute_batch(
905 "CREATE TEMP TABLE selected_view_paths (
906 path TEXT PRIMARY KEY
907 ) WITHOUT ROWID",
908 )?;
909 {
910 let mut insert = connection.prepare("INSERT INTO selected_view_paths(path) VALUES(?1)")?;
911 for path in selected {
912 if !changed.contains(path.as_bytes()) {
913 insert.execute([path])?;
914 }
915 }
916 }
917
918 let mut bindings = load_surfaces(connection)?;
919 for path in changed {
920 if let Ok(path) = std::str::from_utf8(path) {
921 bindings.remove(path);
922 }
923 }
924 {
925 let mut statement = connection.prepare(
926 "SELECT bindings.file_path, bindings.payload
927 FROM selected_view_paths
928 CROSS JOIN view_bindings AS bindings INDEXED BY sqlite_autoindex_view_bindings_1
929 ON bindings.file_path = selected_view_paths.path",
930 )?;
931 let rows = statement.query_map([], |row| {
932 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
933 })?;
934 for row in rows {
935 let (path, payload) = row?;
936 let mut binding: join::ViewBindingDependencies = serde_json::from_str(&payload)
937 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?;
938 if let Some(surface) = bindings.get(&path) {
939 binding.copy_surface_from(surface);
940 }
941 bindings.insert(path, binding);
942 }
943 }
944 Ok(bindings)
945}
946
947fn dependent_closure(
948 connection: &Connection,
949 changed: &BTreeSet<Vec<u8>>,
950) -> Result<BTreeSet<String>> {
951 connection.execute_batch(
952 "CREATE TEMP TABLE dependency_seed_paths (
953 path TEXT PRIMARY KEY
954 ) WITHOUT ROWID",
955 )?;
956 {
957 let mut insert =
958 connection.prepare("INSERT OR IGNORE INTO dependency_seed_paths(path) VALUES(?1)")?;
959 for path in changed {
960 if let Ok(path) = std::str::from_utf8(path) {
961 insert.execute([path])?;
962 }
963 }
964 if !changed.is_empty() {
965 insert.execute([join::VIEW_RUST_MODULE_DOMAIN])?;
969 }
970 }
971 let mut statement = connection.prepare(
972 "WITH RECURSIVE selected(path) AS (
973 SELECT path FROM dependency_seed_paths
974 UNION
975 SELECT dependencies.file_path
976 FROM selected
977 JOIN file_dependencies AS dependencies INDEXED BY idx_file_dependencies_dep_file
978 ON dependencies.dep_file = selected.path
979 )
980 SELECT path FROM selected",
981 )?;
982 let rows = statement.query_map([], |row| row.get::<_, String>(0))?;
983 rows.collect::<std::result::Result<BTreeSet<_>, _>>()
984 .map_err(Into::into)
985}
986
987fn requires_full_resolution(
988 base: &crate::views::Manifest,
989 next: &crate::views::Manifest,
990 changed: &BTreeSet<Vec<u8>>,
991) -> bool {
992 changed.iter().any(|path| {
993 base.entries()
994 .chain(next.entries())
995 .any(|(candidate, entry)| {
996 candidate.as_bytes() == path
997 && matches!(
998 entry,
999 crate::views::ManifestEntry::Synthetic { .. }
1000 | crate::views::ManifestEntry::Symlink { .. }
1001 | crate::views::ManifestEntry::Gitlink { .. }
1002 )
1003 })
1004 })
1005}
1006
1007struct EmissionParse {
1008 blob: Arc<join::CallgraphBlob>,
1009 refs_by_ordinal: HashMap<u32, usize>,
1010}
1011
1012impl EmissionParse {
1013 fn new(blob: Arc<join::CallgraphBlob>) -> Self {
1014 let refs_by_ordinal = blob
1015 .parse()
1016 .into_iter()
1017 .flat_map(|parse| parse.refs.iter().enumerate())
1018 .fold(HashMap::new(), |mut by_ordinal, (position, reference)| {
1019 by_ordinal.entry(reference.ordinal).or_insert(position);
1022 by_ordinal
1023 });
1024 Self {
1025 blob,
1026 refs_by_ordinal,
1027 }
1028 }
1029
1030 fn reference(&self, ordinal: u32) -> Option<&join::BlobRef> {
1031 self.blob
1032 .parse()?
1033 .refs
1034 .get(*self.refs_by_ordinal.get(&ordinal)?)
1035 }
1036}
1037
1038fn ensure_manifest_path(
1042 path: &str,
1043 manifest: &crate::views::Manifest,
1044 blobs: &ManifestViewBlobReader<'_>,
1045 loaded: &mut BTreeSet<String>,
1046 parsed: &mut BTreeMap<String, EmissionParse>,
1047 nodes: &mut HashMap<String, HashMap<String, String>>,
1048) -> Result<()> {
1049 if !loaded.insert(path.to_string()) {
1050 return Ok(());
1051 }
1052 let Ok(rel) = crate::views::RelPath::new(path.as_bytes().to_vec()) else {
1053 return Ok(());
1054 };
1055 let Some(crate::views::ManifestEntry::Regular {
1056 planes,
1057 resolution_input,
1058 ..
1059 }) = manifest.get(&rel)
1060 else {
1061 return Ok(());
1062 };
1063 let Some(key) = &planes.callgraph else {
1064 return Ok(());
1065 };
1066 let blob = blobs
1067 .read_decoded(key)
1068 .map_err(|error| CallGraphStoreError::Unavailable(error.to_string()))?
1069 .ok_or_else(|| {
1070 CallGraphStoreError::Unavailable(format!("missing manifest callgraph blob {key}"))
1071 })?;
1072 let Some(parse) = blob.parse() else {
1073 return Ok(());
1074 };
1075 let file_nodes = nodes.entry(path.to_string()).or_default();
1076 for symbol in &parse.symbols {
1077 let id = format!("view:{path}:{}:{}", symbol.scoped_name, symbol.ordinal);
1078 file_nodes.insert(symbol.scoped_name.clone(), id.clone());
1079 file_nodes.entry(symbol.name.clone()).or_insert(id);
1080 }
1081 if !resolution_input {
1082 parsed.insert(path.to_string(), EmissionParse::new(blob));
1083 }
1084 Ok(())
1085}