1use std::collections::{BTreeMap, BTreeSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::time::Instant;
5
6use rusqlite::{Connection, OptionalExtension};
7
8use crate::alias::{head_tree_entries, AliasStore, GitMode};
9use crate::blob_store::{
10 BlobPlane, BlobStore, CallgraphKey, FullKey, PutOutcome, CALLGRAPH_PRODUCER_VERSION,
11};
12use crate::callgraph_store::join::CallgraphBlob;
13use crate::parser::detect_language;
14use crate::path_status::PathStatusStore;
15use crate::pins::AssemblyPin;
16
17use super::{
18 ArtifactPlane, ByteString, ClosureRequirements, Manifest, ManifestEntry, PublicationArtifacts,
19 PublicationClosure, PublicationRequest, PublishOutcome, RegularPlanes, RelPath, Result,
20 ViewError, ViewStore,
21};
22
23#[derive(Clone, Debug)]
24pub struct AssemblyRequest {
25 pub storage: PathBuf,
26 pub project_root: PathBuf,
27 pub family: String,
28 pub scope: String,
29 pub desired_head: String,
30 pub changed_paths: BTreeSet<Vec<u8>>,
31 pub semantic_keys: BTreeMap<Vec<u8>, String>,
32 pub require_semantic: bool,
33 pub allow_blob_put: bool,
34}
35
36#[derive(Clone, Debug)]
37pub struct AssemblyReport {
38 pub generation: Option<String>,
39 pub manifest: Option<Manifest>,
40 pub blob_puts: usize,
41 pub pending_paths: BTreeSet<Vec<u8>>,
42 pub published: bool,
43}
44
45struct Candidate {
46 path: RelPath,
47 entry: ManifestEntry,
48 key: Option<FullKey>,
49 payload: Option<Vec<u8>>,
50 tracked: Option<crate::alias::TrackedPath>,
51 source: Option<Vec<u8>>,
52}
53
54pub fn head_tree_fingerprint(entries: &[crate::alias::TrackedPath]) -> String {
55 let mut hasher = blake3::Hasher::new();
56 for entry in entries {
57 hasher.update(&(entry.rel_path.len() as u64).to_le_bytes());
58 hasher.update(&entry.rel_path);
59 hasher.update(entry.mode.as_bytes());
60 hasher.update(entry.git_oid.as_bytes());
61 }
62 hasher.finalize().to_hex().to_string()
63}
64
65pub fn publish_checkout(request: &AssemblyRequest) -> Result<AssemblyReport> {
66 let mut prepared = prepare_checkout(request, &mut |_| Ok(()))?;
67 prepared.commit()
68}
69
70pub struct PreparedAssembly {
73 report: AssemblyReport,
74 publication: Option<super::PreparedPublication>,
75 files: Option<(ViewStore, String)>,
76 derived_checkpoint: Option<(PathBuf, Connection)>,
77 pin: Option<AssemblyPin>,
78 _base_pin: Option<crate::pins::QueryPin>,
79 profile: PublicationProfile,
80}
81
82impl PreparedAssembly {
83 pub fn report(&self) -> &AssemblyReport {
84 &self.report
85 }
86
87 pub fn commit(&mut self) -> Result<AssemblyReport> {
88 if let Some(publication) = self.publication.take() {
89 let pointer_started = Instant::now();
90 let outcome = publication.commit();
91 self.profile.pointer_ms = pointer_started.elapsed().as_millis();
92 match outcome? {
93 PublishOutcome::Published => {
94 self.report.published = true;
95 self.files = None;
96 if let Some((path, connection)) = self.derived_checkpoint.take() {
97 super::generation::schedule_derived_checkpoint(path, connection);
98 }
99 self.profile.outcome = "published";
100 }
101 PublishOutcome::Conflict { current_generation } => {
102 self.profile.outcome = "conflict";
103 return Err(ViewError::InvalidManifest(format!(
104 "publication base changed to {current_generation:?}"
105 )));
106 }
107 }
108 }
109 self.profile.finish();
110 Ok(AssemblyReport {
111 generation: self.report.generation.take(),
112 manifest: self.report.manifest.take(),
113 blob_puts: self.report.blob_puts,
114 pending_paths: std::mem::take(&mut self.report.pending_paths),
115 published: self.report.published,
116 })
117 }
118}
119
120impl Drop for PreparedAssembly {
121 fn drop(&mut self) {
122 if let Some((view, generation)) = &self.files {
123 if view
124 .current_generation()
125 .is_ok_and(|current| current.as_deref() != Some(generation))
126 {
127 view.remove_generation_files(generation);
128 }
129 }
130 self.pin.take();
132 }
133}
134
135pub fn prepare_checkout(
136 request: &AssemblyRequest,
137 phase: &mut impl FnMut(&str) -> Result<()>,
138) -> Result<PreparedAssembly> {
139 let mut profile = PublicationProfile::new(&request.project_root);
140 profile.enter(0, phase)?;
141 let view = ViewStore::open(&request.storage, &request.scope)?;
142 let current_generation = view.current_generation()?;
143 let base_pin = current_generation
144 .as_deref()
145 .map(|generation| crate::pins::QueryPin::acquire(view.view_dir(), generation))
146 .transpose()
147 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
148 let previous = current_generation
149 .as_deref()
150 .map(|generation| view.load_manifest(generation))
151 .transpose()?;
152 let head_started = Instant::now();
153 let head = head_tree_entries(&request.project_root)
154 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
155 profile.head_ms = head_started.elapsed().as_millis();
156 let mut callgraph = BlobStore::open(
157 &request.storage,
158 request.family.clone(),
159 BlobPlane::Callgraph,
160 )
161 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
162 let semantic = BlobStore::open(
163 &request.storage,
164 request.family.clone(),
165 BlobPlane::Semantic,
166 )
167 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
168 let mut aliases = AliasStore::open(&request.storage, &request.family)
169 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
170
171 let previous_entries = previous
172 .as_ref()
173 .map(|manifest| {
174 manifest
175 .entries()
176 .map(|(path, entry)| (path.as_bytes().to_vec(), entry.clone()))
177 .collect::<BTreeMap<_, _>>()
178 })
179 .unwrap_or_default();
180 let rebuild_all = request.changed_paths.is_empty();
181 let assembly_started = Instant::now();
182 let mut blocking_paths = BTreeSet::new();
183 let mut semantic_pending_paths = BTreeSet::new();
184 let mut candidates = Vec::with_capacity(head.len());
185
186 for tracked in head {
187 let rel_path = RelPath::new(tracked.rel_path.clone())?;
188 if !rebuild_all && !request.changed_paths.contains(&tracked.rel_path) {
189 if let Some(entry) = previous_entries.get(&tracked.rel_path) {
190 candidates.push(Candidate {
191 path: rel_path,
192 entry: entry.clone(),
193 key: None,
194 payload: None,
195 tracked: None,
196 source: None,
197 });
198 continue;
199 }
200 }
201
202 match tracked.mode {
203 GitMode::Gitlink => candidates.push(Candidate {
204 path: rel_path,
205 entry: ManifestEntry::Gitlink {
206 oid: tracked.git_oid.to_hex(),
207 },
208 key: None,
209 payload: None,
210 tracked: None,
211 source: None,
212 }),
213 GitMode::Symlink => {
214 let target = read_symlink_bytes(
215 &request
216 .project_root
217 .join(path_from_bytes(&tracked.rel_path)),
218 )?;
219 candidates.push(Candidate {
220 path: rel_path,
221 entry: ManifestEntry::Symlink {
222 target_bytes: ByteString::new(target),
223 },
224 key: None,
225 payload: None,
226 tracked: None,
227 source: None,
228 });
229 }
230 GitMode::Regular { executable } => {
231 let absolute = request
232 .project_root
233 .join(path_from_bytes(&tracked.rel_path));
234 let source = fs::read(&absolute)?;
235 let resolution_input = is_resolution_input(&tracked.rel_path);
236 let language = if resolution_input {
237 Some("config".to_string())
238 } else {
239 detect_language(&absolute)
240 .map(|language| format!("{language:?}").to_lowercase())
241 };
242 let (key, payload) = language
243 .as_deref()
244 .map(|language| {
245 let key = CallgraphKey::for_current(&source, language).full_key();
246 let key_hex = key.to_hex();
247 let callgraph_is_current = previous_entries
248 .get(&tracked.rel_path)
249 .is_some_and(|entry| {
250 manifest_entry_callgraph_key(entry) == Some(key_hex.as_str())
251 });
252 let payload = if callgraph_is_current {
253 None
254 } else {
255 let blob = if resolution_input {
256 CallgraphBlob::config(source.clone(), CALLGRAPH_PRODUCER_VERSION)
257 } else {
258 CallgraphBlob::extract(
259 std::str::from_utf8(&source).map_err(|error| {
260 ViewError::InvalidManifest(error.to_string())
261 })?,
262 language,
263 CALLGRAPH_PRODUCER_VERSION,
264 )
265 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?
266 };
267 Some(
268 blob.to_bytes().map_err(|error| {
269 ViewError::InvalidManifest(error.to_string())
270 })?,
271 )
272 };
273 Ok::<_, ViewError>((key, payload))
274 })
275 .transpose()?
276 .map_or((None, None), |(key, payload)| (Some(key), payload));
277 let callgraph_key = key.as_ref().map(FullKey::to_hex);
278 candidates.push(Candidate {
279 path: rel_path,
280 entry: ManifestEntry::Regular {
281 mode: if executable { 0o100755 } else { 0o100644 },
282 planes: RegularPlanes {
283 semantic: request.semantic_keys.get(&tracked.rel_path).cloned(),
284 callgraph: callgraph_key,
285 },
286 resolution_input,
287 },
288 key,
289 payload,
290 tracked: Some(tracked),
291 source: Some(source),
292 });
293 }
294 GitMode::Other(_) => {
295 blocking_paths.insert(tracked.rel_path);
296 }
297 }
298 }
299
300 profile.candidates = candidates.len();
301 profile.assembly_ms = assembly_started.elapsed().as_millis();
302
303 if request.require_semantic {
304 for candidate in &candidates {
305 let missing = matches!(
306 &candidate.entry,
307 ManifestEntry::Regular { planes, .. }
308 if planes.semantic.is_none()
309 && crate::semantic_index::is_semantic_indexed_extension(
310 &request
311 .project_root
312 .join(path_from_bytes(candidate.path.as_bytes()))
313 )
314 );
315 if missing {
316 semantic_pending_paths.insert(candidate.path.as_bytes().to_vec());
317 }
318 }
319 }
320
321 let keys = candidates
322 .iter()
323 .filter_map(|candidate| candidate.key.clone())
324 .collect::<Vec<_>>();
325 let next_generation = next_generation(current_generation.as_deref(), &request.desired_head);
326 let pin = AssemblyPin::create(
327 view.view_dir(),
328 request.family.clone(),
329 request.scope.clone(),
330 next_generation.clone(),
331 &keys,
332 )
333 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
334 let mut prepared = PreparedAssembly {
335 report: AssemblyReport {
336 generation: current_generation.clone(),
337 manifest: previous.clone(),
338 blob_puts: 0,
339 pending_paths: BTreeSet::new(),
340 published: false,
341 },
342 publication: None,
343 files: Some((view.clone(), next_generation.clone())),
344 derived_checkpoint: None,
345 pin: Some(pin),
346 _base_pin: base_pin,
347 profile,
348 };
349 prepared.profile.enter(1, phase)?;
350 let mut blob_puts = 0;
351 for candidate in &candidates {
352 let (Some(key), Some(payload)) = (&candidate.key, &candidate.payload) else {
353 continue;
354 };
355 if request.allow_blob_put {
356 prepared
357 .pin
358 .as_mut()
359 .expect("assembly pin")
360 .renew_if_due()
361 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
362 let put = callgraph
363 .put(key, payload)
364 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
365 blob_puts += usize::from(matches!(put.outcome, PutOutcome::Inserted));
366 if let (Some(tracked), Some(source)) = (&candidate.tracked, &candidate.source) {
367 aliases
368 .seed_proven_alias(tracked, source)
369 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
370 }
371 } else if callgraph
372 .get(key)
373 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?
374 .is_none()
375 {
376 blocking_paths.insert(candidate.path.as_bytes().to_vec());
377 }
378 }
379
380 prepared.profile.blob_puts = blob_puts;
381 prepared.profile.pending_paths = blocking_paths.len() + semantic_pending_paths.len();
382 let mut status = PathStatusStore::open(view.view_dir())
383 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
384 for path in blocking_paths.iter().chain(&semantic_pending_paths) {
385 status
386 .mark_pending(
387 path,
388 if blocking_paths.contains(path) {
389 "shared callgraph blob unavailable"
390 } else {
391 "shared semantic blob unavailable"
392 },
393 generation_number(&next_generation),
394 )
395 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
396 }
397 if !blocking_paths.is_empty() {
398 prepared.profile.outcome = "pending";
399 prepared.report.blob_puts = blob_puts;
400 prepared.report.pending_paths = blocking_paths
401 .union(&semantic_pending_paths)
402 .cloned()
403 .collect();
404 return Ok(prepared);
405 }
406 for candidate in &candidates {
407 if !semantic_pending_paths.contains(candidate.path.as_bytes()) {
408 status
409 .clear(candidate.path.as_bytes())
410 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
411 }
412 }
413 prepared.report.pending_paths = semantic_pending_paths;
414
415 let manifest = Manifest::new(
416 candidates
417 .into_iter()
418 .map(|candidate| (candidate.path, candidate.entry)),
419 )?;
420 if current_generation
426 .as_deref()
427 .is_some_and(|generation| generation.ends_with(&request.desired_head))
428 && previous.as_ref() == Some(&manifest)
429 {
430 prepared.profile.outcome = "no_op";
431 prepared.report.manifest = None;
432 prepared.report.blob_puts = blob_puts;
433 return Ok(prepared);
434 }
435 prepared.profile.enter(2, phase)?;
436 let derived = view.derived_path(&next_generation)?;
437 let mut cloned_base = false;
438 let clone_started = Instant::now();
439 if let Some(base) = current_generation.as_deref() {
440 let base_path = view.derived_path(base)?;
441 if base_path.is_file() {
442 super::generation::clone_derived(&base_path, &derived)?;
443 cloned_base = true;
444 }
445 }
446 prepared.profile.derived_clone_ms = clone_started.elapsed().as_millis();
447 let derived_keeper = Connection::open(&derived)?;
450 derived_keeper.busy_timeout(std::time::Duration::from_secs(5))?;
451 prepared.derived_checkpoint = Some((derived.clone(), derived_keeper));
452 let materialization_started = Instant::now();
453 if let Some(base_manifest) = previous.as_ref().filter(|_| cloned_base) {
454 let (stats, timings) = super::materialization::apply_manifest_diff_profiled(
455 &derived,
456 base_manifest,
457 &manifest,
458 callgraph.path(),
459 )
460 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
461 prepared.profile.materialization = timings;
462 log::info!(
463 "view manifest diff: generation={} stats={stats:?}",
464 next_generation
465 );
466 } else {
467 crate::callgraph_store::materialize_manifest_view_database(
468 &derived,
469 callgraph.path(),
470 &manifest,
471 )
472 .map_err(|error| ViewError::InvalidManifest(error.to_string()))?;
473 }
474 prepared.profile.materialization_call_ms = materialization_started.elapsed().as_millis();
475 let trigram = view.trigram_path(&next_generation)?;
476 fs::write(&trigram, [])?;
477 let artifacts = PublicationArtifacts {
478 blob_databases: vec![
479 semantic.path().to_path_buf(),
480 callgraph.path().to_path_buf(),
481 ],
482 derived_database: derived.clone(),
483 trigram_artifact: trigram.clone(),
484 alias_database: aliases.path().to_path_buf(),
485 };
486 let closure = SqliteClosure {
487 semantic: semantic.path().to_path_buf(),
488 callgraph: callgraph.path().to_path_buf(),
489 trigram,
490 connections: Default::default(),
491 };
492 let closure_started = Instant::now();
493 let publication = view.prepare_with_observer(
494 &PublicationRequest {
495 generation: &next_generation,
496 base_generation: current_generation.as_deref(),
497 manifest: &manifest,
498 artifacts,
499 closure_requirements: ClosureRequirements::default(),
500 },
501 &closure,
502 None,
503 )?;
504 prepared.profile.closure_ms = closure_started.elapsed().as_millis();
505 prepared.profile.derived_bytes = fs::metadata(&derived)
506 .map(|metadata| metadata.len())
507 .unwrap_or(0);
508 prepared.profile.enter(3, phase)?;
509 prepared.publication = Some(publication);
510 prepared.report = AssemblyReport {
511 generation: Some(next_generation),
512 manifest: Some(manifest),
513 blob_puts,
514 pending_paths: prepared.report.pending_paths.clone(),
515 published: false,
516 };
517 Ok(prepared)
518}
519
520struct PublicationProfile {
524 root: PathBuf,
525 outcome: &'static str,
526 candidates: usize,
527 blob_puts: usize,
528 pending_paths: usize,
529 head_ms: u128,
530 assembly_ms: u128,
531 pointer_ms: u128,
532 phase_ms: [u128; 4],
533 active_phase: Option<usize>,
534 phase_started: Instant,
535 started: Instant,
536 total_ms: u128,
537 derived_bytes: u64,
538 derived_clone_ms: u128,
539 materialization_call_ms: u128,
540 closure_ms: u128,
541 materialization: super::materialization::profile::PhaseTimings,
542}
543
544impl PublicationProfile {
545 fn new(root: &Path) -> Self {
546 let now = Instant::now();
547 Self {
548 root: root.to_owned(),
549 outcome: "cancelled_or_failed",
550 candidates: 0,
551 blob_puts: 0,
552 pending_paths: 0,
553 head_ms: 0,
554 assembly_ms: 0,
555 pointer_ms: 0,
556 phase_ms: [0; 4],
557 active_phase: Some(0),
558 phase_started: now,
559 started: now,
560 total_ms: 0,
561 derived_bytes: 0,
562 derived_clone_ms: 0,
563 materialization_call_ms: 0,
564 closure_ms: 0,
565 materialization: super::materialization::profile::PhaseTimings::default(),
566 }
567 }
568
569 fn enter(&mut self, phase: usize, callback: &mut impl FnMut(&str) -> Result<()>) -> Result<()> {
570 self.checkpoint();
571 self.active_phase = Some(phase);
572 self.phase_started = Instant::now();
573 callback(["manifest", "blobs", "derived", "cas"][phase])
574 }
575
576 fn checkpoint(&mut self) {
577 if let Some(phase) = self.active_phase.take() {
578 self.phase_ms[phase] += self.phase_started.elapsed().as_millis();
579 }
580 }
581
582 fn finish(&mut self) {
583 self.checkpoint();
584 self.total_ms = self.started.elapsed().as_millis();
585 }
586}
587
588impl Drop for PublicationProfile {
589 fn drop(&mut self) {
590 if self.active_phase.is_some() {
591 self.finish();
592 }
593 log_publication_profile(self);
594 }
595}
596
597fn log_publication_profile(profile: &PublicationProfile) {
598 crate::slog_info!("{}", publication_profile_line(profile));
599}
600
601fn publication_profile_line(profile: &PublicationProfile) -> String {
602 format!(
606 "index_event kind=view_publication plane=views root={} outcome={} candidates={} blob_puts={} pending_paths={} manifest_ms={} blobs_ms={} derived_ms={} cas_ms={} head_ms={} assembly_ms={} blob_ms={} materialize_ms={} derived_clone_ms={} materialization_call_ms={} closure_ms={} materialize_load_bindings_select_ms={} materialize_delete_rows_ms={} materialize_owned_blob_decode_insert_ms={} materialize_join_load_payloads_ms={} materialize_join_decode_bind_index_entries_ms={} materialize_join_index_surface_replay_ms={} materialize_join_decode_resolved_callers_ms={} materialize_join_resolve_record_ms={} materialize_join_dependency_union_ms={} materialize_selected_join_ms={} materialize_write_bindings_ms={} materialize_emit_refs_edges_ms={} materialize_commit_ms={} pointer_ms={} total_ms={} derived_bytes={}",
607 profile.root.display(), profile.outcome, profile.candidates, profile.blob_puts,
608 profile.pending_paths, profile.phase_ms[0], profile.phase_ms[1],
609 profile.phase_ms[2], profile.phase_ms[3], profile.head_ms, profile.assembly_ms,
610 profile.phase_ms[1], profile.materialization_call_ms,
611 profile.derived_clone_ms,
612 profile.materialization_call_ms,
613 profile.closure_ms,
614 profile.materialization.load_bindings_select_ms,
615 profile.materialization.delete_rows_ms,
616 profile.materialization.owned_blob_decode_and_insert_ms,
617 profile.materialization.join_load_payloads_ms,
618 profile.materialization.join_decode_bind_index_entries_ms,
619 profile.materialization.join_index_and_surface_replay_ms,
620 profile.materialization.join_decode_resolved_callers_ms,
621 profile.materialization.join_resolve_and_record_ms,
622 profile.materialization.join_dependency_union_ms,
623 profile.materialization.selected_join_ms,
624 profile.materialization.write_bindings_ms,
625 profile.materialization.emit_refs_edges_ms,
626 profile.materialization.commit_ms,
627 profile.pointer_ms, profile.total_ms, profile.derived_bytes,
628 )
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn publication_profile_line_attributes_the_canonical_root() {
637 let mut profile = PublicationProfile::new(Path::new("/checkout"));
638 profile.outcome = "published";
639 profile.phase_ms = [5, 6, 7, 8];
640 profile.derived_clone_ms = 9;
641 profile.materialization_call_ms = 10;
642 profile.closure_ms = 11;
643 profile.materialization = crate::views::materialization::profile::PhaseTimings {
644 load_bindings_select_ms: 11,
645 delete_rows_ms: 12,
646 owned_blob_decode_and_insert_ms: 13,
647 join_load_payloads_ms: 14,
648 join_decode_bind_index_entries_ms: 15,
649 join_index_and_surface_replay_ms: 16,
650 join_decode_resolved_callers_ms: 17,
651 join_resolve_and_record_ms: 18,
652 join_dependency_union_ms: 19,
653 selected_join_ms: 20,
654 write_bindings_ms: 21,
655 emit_refs_edges_ms: 22,
656 commit_ms: 23,
657 };
658 let line = publication_profile_line(&profile);
659 assert!(line.contains("plane=views root=/checkout outcome=published"));
660 assert!(line.contains("manifest_ms=5 blobs_ms=6 derived_ms=7 cas_ms=8"));
661 assert!(line.contains("derived_clone_ms=9 materialization_call_ms=10 closure_ms=11"));
662 assert!(line.contains("blob_ms=6 materialize_ms=10"));
663 assert!(line.contains(
664 "materialize_load_bindings_select_ms=11 materialize_delete_rows_ms=12 \
665 materialize_owned_blob_decode_insert_ms=13 materialize_join_load_payloads_ms=14 \
666 materialize_join_decode_bind_index_entries_ms=15 \
667 materialize_join_index_surface_replay_ms=16 \
668 materialize_join_decode_resolved_callers_ms=17 \
669 materialize_join_resolve_record_ms=18 materialize_join_dependency_union_ms=19 \
670 materialize_selected_join_ms=20 materialize_write_bindings_ms=21 \
671 materialize_emit_refs_edges_ms=22 materialize_commit_ms=23"
672 ));
673 assert_eq!(line.matches("index_event kind=view_publication").count(), 1);
674 }
675}
676
677fn manifest_entry_callgraph_key(entry: &ManifestEntry) -> Option<&str> {
678 match entry {
679 ManifestEntry::Regular { planes, .. } => planes.callgraph.as_deref(),
680 ManifestEntry::Synthetic { planes, .. } => Some(&planes.callgraph),
681 ManifestEntry::Symlink { .. } | ManifestEntry::Gitlink { .. } => None,
682 }
683}
684
685fn is_resolution_input(path: &[u8]) -> bool {
686 let name = path.rsplit(|byte| *byte == b'/').next().unwrap_or(path);
687 name == b"package.json"
688 || name == b"Cargo.toml"
689 || name == b".gitignore"
690 || name.starts_with(b"tsconfig") && name.ends_with(b".json")
691}
692
693fn next_generation(current: Option<&str>, desired_head: &str) -> String {
694 let generation = current
695 .map(generation_number)
696 .unwrap_or(0)
697 .saturating_add(1);
698 static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
699 let serial = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
700 let nanos = std::time::SystemTime::now()
701 .duration_since(std::time::UNIX_EPOCH)
702 .unwrap_or_default()
703 .as_nanos();
704 format!(
705 "{generation}-{}-{nanos}-{serial}-{desired_head}",
706 std::process::id()
707 )
708}
709
710fn generation_number(generation: &str) -> u64 {
711 generation
712 .split_once('-')
713 .and_then(|(number, _)| number.parse().ok())
714 .unwrap_or(0)
715}
716
717struct SqliteClosure {
718 semantic: PathBuf,
719 callgraph: PathBuf,
720 trigram: PathBuf,
721 connections: std::cell::RefCell<BTreeMap<bool, crate::db::lifecycle::TrackedConnection>>,
724}
725
726impl PublicationClosure for SqliteClosure {
727 fn contains_blob(&self, plane: ArtifactPlane, full_key: &str) -> Result<bool> {
728 let Some(key) = decode_hex(full_key) else {
729 return Ok(false);
730 };
731 let path = match plane {
732 ArtifactPlane::Semantic => &self.semantic,
733 ArtifactPlane::Callgraph => &self.callgraph,
734 };
735 let mut connections = self.connections.borrow_mut();
736 let semantic = plane == ArtifactPlane::Semantic;
737 if let std::collections::btree_map::Entry::Vacant(entry) = connections.entry(semantic) {
738 entry.insert(crate::db::lifecycle::TrackedConnection::open(
739 path,
740 crate::db::lifecycle::SqliteStore::BlobStore,
741 )?);
742 }
743 let present = connections[&semantic]
744 .prepare_cached("SELECT 1 FROM blob_payloads WHERE full_key = ?1")?
745 .query_row([key], |_| Ok(()))
746 .optional()?
747 .is_some();
748 Ok(present)
749 }
750
751 fn probe_blobs(&self, keys: &[(ArtifactPlane, &str)]) -> Result<()> {
752 let mut present = BTreeSet::new();
753 for (plane_id, plane) in [ArtifactPlane::Semantic, ArtifactPlane::Callgraph]
754 .into_iter()
755 .enumerate()
756 {
757 let wanted = keys
758 .iter()
759 .filter(|(p, _)| *p == plane)
760 .map(|(_, key)| *key)
761 .collect::<BTreeSet<_>>()
762 .into_iter()
763 .collect::<Vec<_>>();
764 for chunk in wanted.chunks(500) {
765 let Some(first_valid) = chunk.iter().find(|key| decode_hex(key).is_some()) else {
768 continue;
769 };
770 self.contains_blob(plane, first_valid)?;
771 let connections = self.connections.borrow();
772 let Some(connection) = connections.get(&(plane == ArtifactPlane::Semantic)) else {
773 continue;
774 };
775 let decoded = chunk
776 .iter()
777 .filter_map(|key| decode_hex(key))
778 .collect::<Vec<_>>();
779 if decoded.is_empty() {
780 continue;
781 }
782 let sql = format!(
783 "SELECT full_key FROM blob_payloads WHERE full_key IN ({})",
784 vec!["?"; decoded.len()].join(",")
785 );
786 let mut statement = connection.prepare_cached(&sql)?;
787 for key in statement.query_map(rusqlite::params_from_iter(&decoded), |row| {
788 row.get::<_, Vec<u8>>(0)
789 })? {
790 present.insert((plane_id, key?));
791 }
792 }
793 }
794 for &(plane, key) in keys {
797 let plane_id = usize::from(plane == ArtifactPlane::Callgraph);
798 if decode_hex(key).is_none_or(|key| !present.contains(&(plane_id, key))) {
799 return Err(ViewError::MissingBlob {
800 plane,
801 key: key.to_owned(),
802 });
803 }
804 }
805 Ok(())
806 }
807
808 fn trigram_is_present(&self) -> Result<bool> {
809 Ok(self.trigram.is_file())
810 }
811
812 fn contains_alias(&self, _git_oid: &str) -> Result<bool> {
813 Ok(true)
814 }
815}
816
817fn decode_hex(value: &str) -> Option<Vec<u8>> {
818 if value.len() != 64 {
819 return None;
820 }
821 (0..value.len())
822 .step_by(2)
823 .map(|index| u8::from_str_radix(&value[index..index + 2], 16).ok())
824 .collect()
825}
826
827fn path_from_bytes(bytes: &[u8]) -> PathBuf {
828 #[cfg(unix)]
829 {
830 use std::os::unix::ffi::OsStringExt as _;
831 PathBuf::from(std::ffi::OsString::from_vec(bytes.to_vec()))
832 }
833 #[cfg(not(unix))]
834 {
835 PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
836 }
837}
838
839fn read_symlink_bytes(path: &Path) -> Result<Vec<u8>> {
840 let target = fs::read_link(path)?;
841 #[cfg(unix)]
842 {
843 use std::os::unix::ffi::OsStrExt as _;
844 Ok(target.as_os_str().as_bytes().to_vec())
845 }
846 #[cfg(not(unix))]
847 {
848 Ok(target.to_string_lossy().as_bytes().to_vec())
849 }
850}
851
852#[cfg(test)]
853#[path = "closure_connection_tests.rs"]
854mod closure_connection_tests;