prov_graph/index.rs
1//! Index — where stable IDs and (later) the materialized graph live.
2//!
3//! An index fuses two natures (DESIGN §5): the **authoritative** id↔path
4//! registry — not rebuildable from the documents — and (to come) the
5//! **derived** resolution cache and adjacency index, which are. Keeping it
6//! behind a trait is deliberate: a sidecar file, an in-memory map, or a
7//! sync-backed store are all valid homes.
8//!
9//! Only the query half is here — [`IdIndex`], the lookups link resolution
10//! needs. Everything that *changes* a registration (`IndexStore`, the
11//! `Rebase` seam, the in-memory and registry-document stores) is
12//! `prov-store`'s `index` module, for the same reason the write half of
13//! [`fs`](crate::fs) is: a read-only consumer must not merely decline to write,
14//! it must have nothing to write with.
15//!
16//! ## Tombstones — IDs are forever
17//!
18//! DESIGN's open question #1 ("does the registry ever need to survive without
19//! its documents?") is answered **yes, minimally**: deleting a document leaves
20//! a *tombstone* — the ID stops resolving but is never forgotten, so it can
21//! never be reminted to mean something else. A dangling `prov:` reference
22//! then stays *diagnosable* (validation can say "that document was deleted")
23//! instead of becoming a silent re-resolution hazard. [`is_known`] is the
24//! question that tells the two apart.
25//!
26//! [`is_known`]: IdIndex::is_known
27
28use std::path::{Path, PathBuf};
29
30use crate::identity::Id;
31
32/// A registration that would displace one the index already holds.
33///
34/// The index is a **bijection**: one id names one path, one path carries one id.
35/// Registering across an existing entry breaks that, and in one of two
36/// directions — worth telling apart, because what the user has to do about them
37/// differs.
38///
39/// Both are ordinary under sync. `id_storage` defaults to `both`, so a document's
40/// id travels *in its own frontmatter*: a transport can land a copy of a document
41/// under a new name, and now two files spell one id with the registry able to
42/// name only one of them. The registry cannot arbitrate that — only the author
43/// can — so an operation that would resolve it silently refuses instead.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum Collision {
46 /// The id already resolves to a *different* live document. Registering would
47 /// take the id away from a document whose frontmatter still spells it.
48 Id {
49 /// The id being registered.
50 id: Id,
51 /// The document that currently holds it.
52 held_by: PathBuf,
53 },
54 /// The path already carries a *different* id. Registering would drop that id
55 /// out of the registry while the document on disk still spells it — turning a
56 /// live id into an unregistered one.
57 Path {
58 /// The path being registered.
59 path: PathBuf,
60 /// The id it currently carries.
61 held: Id,
62 },
63}
64
65impl std::fmt::Display for Collision {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Collision::Id { id, held_by } => {
69 write!(f, "{id} is already registered to {}", held_by.display())
70 }
71 Collision::Path { path, held } => {
72 write!(f, "{} already carries {held}", path.display())
73 }
74 }
75 }
76}
77
78/// The query half of an ID index: the three lookups link resolution needs, and
79/// no way to change what is stored.
80///
81/// This is the trait [`crate::graph`] is generic over, and it is the whole of
82/// what the read core asks of a registry — `id:` resolution
83/// ([`resolve`](IdIndex::resolve)), the reverse lookup a census entry is tagged
84/// with ([`id_for_path`](IdIndex::id_for_path)), and the tombstone question that
85/// distinguishes "never existed" from "retired"
86/// ([`is_known`](IdIndex::is_known)).
87///
88/// Split out of `prov-store`'s `IndexStore` for the same reason
89/// [`ReadStorage`](crate::fs::ReadStorage) is split out of that crate's
90/// `Storage`: a read-only consumer must be able to depend on traversal without
91/// linking the staging machinery, and `IndexStore`'s staging half is not merely
92/// unused by the read core — it is *stated in write vocabulary*, down to
93/// `rebase`, which only a pending mutation has anything to say to.
94pub trait IdIndex {
95 /// Resolve an ID to its current path. `None` for unknown *and* tombstoned
96 /// IDs — use [`is_known`](IdIndex::is_known) to tell them apart.
97 fn resolve(&self, id: &Id) -> Option<PathBuf>;
98
99 /// The ID currently assigned to `path`, if any.
100 fn id_for_path(&self, path: &Path) -> Option<Id>;
101
102 /// Whether `id` has *ever* been issued — live or tombstoned. This is the
103 /// mint-with-rejection predicate: a fresh ID must be `!is_known`.
104 fn is_known(&self, id: &Id) -> bool {
105 self.resolve(id).is_some()
106 }
107}
108
109/// No index — identity-off workspaces. Registers nothing, resolves nothing.
110#[derive(Debug, Clone, Copy, Default)]
111pub struct NoIndex;
112
113impl IdIndex for NoIndex {
114 fn resolve(&self, _id: &Id) -> Option<PathBuf> {
115 None
116 }
117 fn id_for_path(&self, _path: &Path) -> Option<Id> {
118 None
119 }
120}