Skip to main content

sqry_db/
input.rs

1//! File-level input storage with per-file revision counters.
2//!
3//! `FileInputStore` maps each `FileId` to a [`FileInput`] record containing the
4//! set of node/edge/scope IDs belonging to that file and a monotonic revision
5//! counter. When a file is re-indexed, its revision bumps, causing all queries
6//! that recorded a dependency on that file to be invalidated on next access.
7
8use std::collections::HashMap;
9
10use smallvec::SmallVec;
11
12use sqry_core::graph::unified::concurrent::GraphSnapshot;
13use sqry_core::graph::unified::file::id::FileId;
14use sqry_core::graph::unified::node::id::NodeId;
15
16/// Per-file fact set with revision counter.
17///
18/// The revision counter starts at 1 for the initial build and is bumped every
19/// time the file is re-indexed via `reindex_files`.
20#[derive(Debug, Clone)]
21pub struct FileInput {
22    /// Node IDs belonging to this file (from the arena segment).
23    pub node_ids: SmallVec<[NodeId; 16]>,
24    /// Monotonic revision counter for this file's content.
25    ///
26    /// Starts at 1. Bumped on every re-index. Queries cache the revision
27    /// at read time and are invalidated when the stored revision differs.
28    pub revision: u64,
29}
30
31impl FileInput {
32    /// Creates a new `FileInput` with the given node IDs at revision 1.
33    #[must_use]
34    pub fn new(node_ids: SmallVec<[NodeId; 16]>) -> Self {
35        Self {
36            node_ids,
37            revision: 1,
38        }
39    }
40
41    /// Returns the current revision.
42    #[inline]
43    #[must_use]
44    pub fn revision(&self) -> u64 {
45        self.revision
46    }
47
48    /// Bumps the revision and replaces node IDs atomically.
49    pub fn update(&mut self, new_node_ids: SmallVec<[NodeId; 16]>) {
50        self.revision = self.revision.saturating_add(1);
51        self.node_ids = new_node_ids;
52    }
53}
54
55/// Storage for per-file input facts, indexed by `FileId`.
56///
57/// This is the Tier 1 dependency source. Queries record the `FileId` + revision
58/// of every file they read during execution. On cache validation, each recorded
59/// `(FileId, revision)` pair is checked against the current store.
60#[derive(Debug, Clone)]
61pub struct FileInputStore {
62    /// Maps FileId (as u32 index) to per-file input.
63    entries: HashMap<FileId, FileInput>,
64}
65
66impl FileInputStore {
67    /// Creates an empty store.
68    #[must_use]
69    pub fn new() -> Self {
70        Self {
71            entries: HashMap::new(),
72        }
73    }
74
75    /// Populates the store from a graph snapshot by scanning the node arena
76    /// for file ownership.
77    ///
78    /// This is called once during `QueryDb` construction from a full build.
79    /// Each file gets revision 1.
80    #[must_use]
81    pub fn from_snapshot(snapshot: &GraphSnapshot) -> Self {
82        let mut file_nodes: HashMap<FileId, SmallVec<[NodeId; 16]>> = HashMap::new();
83        let arena = snapshot.nodes();
84
85        for (idx, slot) in arena.slots().iter().enumerate() {
86            if let Some(entry) = slot.get() {
87                let file_id = entry.file;
88                if file_id != FileId::INVALID {
89                    let node_id = NodeId::new(idx as u32, slot.generation());
90                    file_nodes.entry(file_id).or_default().push(node_id);
91                }
92            }
93        }
94
95        let entries = file_nodes
96            .into_iter()
97            .map(|(fid, nodes)| (fid, FileInput::new(nodes)))
98            .collect();
99
100        Self { entries }
101    }
102
103    /// Returns the current revision for a file, or `None` if unknown.
104    #[inline]
105    #[must_use]
106    pub fn revision(&self, file_id: FileId) -> Option<u64> {
107        self.entries.get(&file_id).map(|fi| fi.revision)
108    }
109
110    /// Returns the file input for a file, or `None` if unknown.
111    #[inline]
112    #[must_use]
113    pub fn get(&self, file_id: FileId) -> Option<&FileInput> {
114        self.entries.get(&file_id)
115    }
116
117    /// Returns a mutable reference to the file input.
118    #[inline]
119    pub fn get_mut(&mut self, file_id: FileId) -> Option<&mut FileInput> {
120        self.entries.get_mut(&file_id)
121    }
122
123    /// Inserts or replaces a file input entry.
124    pub fn insert(&mut self, file_id: FileId, input: FileInput) {
125        self.entries.insert(file_id, input);
126    }
127
128    /// Removes a file input entry, returning it if it existed.
129    pub fn remove(&mut self, file_id: FileId) -> Option<FileInput> {
130        self.entries.remove(&file_id)
131    }
132
133    /// Returns the total number of tracked files.
134    #[inline]
135    #[must_use]
136    pub fn file_count(&self) -> usize {
137        self.entries.len()
138    }
139
140    /// Returns an iterator over all tracked file IDs.
141    pub fn file_ids(&self) -> impl Iterator<Item = FileId> + '_ {
142        self.entries.keys().copied()
143    }
144
145    /// Returns an iterator over all entries.
146    pub fn iter(&self) -> impl Iterator<Item = (FileId, &FileInput)> + '_ {
147        self.entries.iter().map(|(&fid, fi)| (fid, fi))
148    }
149
150    /// Returns a snapshot of all per-file revision counters as a `Vec`.
151    ///
152    /// Each element is `(FileId, revision)`. The order is unspecified (HashMap
153    /// iteration order). Used by the SAVE_PATH persistence unit to capture
154    /// the Tier 1 dependency state into [`DerivedHeader::file_revisions`].
155    #[must_use]
156    pub fn all_revisions(&self) -> Vec<(FileId, u64)> {
157        self.entries
158            .iter()
159            .map(|(&fid, fi)| (fid, fi.revision))
160            .collect()
161    }
162
163    /// Overwrites the stored revision counter for each `(FileId, revision)`
164    /// pair in `revisions`.
165    ///
166    /// Files not present in `revisions` are left untouched. Files in
167    /// `revisions` that are not currently tracked in the store are inserted
168    /// with empty node IDs at the given revision — this ensures that cold-load
169    /// cache entries for files whose nodes haven't been scanned yet can still
170    /// have their Tier 1 revisions validated correctly.
171    ///
172    /// **Infallible by construction**: uses only `HashMap::insert` on `&self`
173    /// via interior mutability through the `FileInput` revision field. Called
174    /// exclusively from [`QueryDb::commit_staged_load`] — the single infallible
175    /// commit boundary in LOAD_PATH.
176    ///
177    /// # Note on `&self`
178    ///
179    /// `FileInputStore` holds a `HashMap` directly without interior mutability
180    /// wrappers — callers must have `&mut FileInputStore`. This method takes
181    /// `&mut self` to mutate entries directly. Infallibility is preserved
182    /// because `HashMap::insert` and field assignment cannot fail.
183    pub(crate) fn restore_revisions(&mut self, revisions: &[(FileId, u64)]) {
184        for &(fid, saved_rev) in revisions {
185            match self.entries.get_mut(&fid) {
186                Some(fi) => {
187                    // Overwrite the revision with the saved value.
188                    fi.revision = saved_rev;
189                }
190                None => {
191                    // File not yet tracked — insert with the saved revision and
192                    // empty node IDs so Tier 1 validation can match it.
193                    let mut fi = FileInput::new(smallvec::SmallVec::new());
194                    fi.revision = saved_rev;
195                    self.entries.insert(fid, fi);
196                }
197            }
198        }
199    }
200}
201
202impl Default for FileInputStore {
203    fn default() -> Self {
204        Self::new()
205    }
206}