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}