Skip to main content

hawdb_storage/
read_view.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::cache::ManifestGeneration;
16
17/// Ownership of a query's admitted graph-read allocation. Implementations
18/// release the reservation on Drop; storage retains it through consumption.
19/// Standalone storage callers may use their own explicit allocation policy.
20pub trait GraphReadAllocation {
21    fn bytes(&self) -> usize;
22
23    /// Reserve additional bytes atomically before allocation. Refusal leaves
24    /// the existing reservation intact.
25    fn grow(&mut self, bytes: usize) -> hawdb_core::Result<()>;
26
27    /// Move an admitted read into retained operator state before insertion.
28    /// Query allocators may atomically transfer to a separately bounded state
29    /// account. Standalone allocators retain their existing explicit policy.
30    fn retain_state(&mut self) -> hawdb_core::Result<()> {
31        Ok(())
32    }
33}
34
35/// A borrowed read policy. Work accounting never invokes payload admission:
36/// payload callbacks can add fixed overhead, flush a batch, or request Stop.
37pub struct GraphReadAllocator<'a, T = Box<dyn GraphReadAllocation>> {
38    admit: &'a mut dyn FnMut(usize) -> hawdb_core::Result<T>,
39    work: GraphReadWork<'a>,
40    control: Option<&'a hawdb_core::RuntimeTaskContext>,
41}
42
43impl<'a, T> GraphReadAllocator<'a, T> {
44    pub fn new(
45        admit: &'a mut dyn FnMut(usize) -> hawdb_core::Result<T>,
46        task_context: Option<&'a hawdb_core::RuntimeTaskContext>,
47    ) -> Self {
48        Self {
49            admit,
50            work: GraphReadWork(task_context.filter(|task| task.has_work_limit())),
51            control: task_context,
52        }
53    }
54
55    pub fn reserve(&mut self, bytes: usize) -> hawdb_core::Result<T> {
56        (self.admit)(bytes)
57    }
58
59    pub(crate) fn work(&self) -> GraphReadWork<'a> {
60        self.work
61    }
62
63    pub(crate) fn checkpoint(&self) -> hawdb_core::Result<()> {
64        self.control.map_or(Ok(()), |task| {
65            task.checkpoint().map_err(|reason| {
66                hawdb_core::HawDBError::Execution(format!("runtime task stopped: {reason}"))
67            })
68        })
69    }
70
71    pub(crate) fn reborrow(&mut self) -> GraphReadAllocator<'_, T> {
72        GraphReadAllocator {
73            admit: self.admit,
74            work: self.work,
75            control: self.control,
76        }
77    }
78}
79
80#[derive(Clone, Copy)]
81pub(crate) struct GraphReadWork<'a>(Option<&'a hawdb_core::RuntimeTaskContext>);
82
83impl GraphReadWork<'_> {
84    pub(crate) fn has_work_limit(self) -> bool {
85        self.0.is_some()
86    }
87
88    pub(crate) fn check_work_limit(self) -> hawdb_core::Result<()> {
89        self.0.map_or(Ok(()), |task| task.check_work_limit())
90    }
91
92    pub(crate) fn charge_work(self, units: usize) -> hawdb_core::Result<()> {
93        self.0.map_or(Ok(()), |task| task.charge_work(units))
94    }
95}
96
97/// None requests ordinary scan Stop before selected values are owned.
98pub type ControlledGraphReadAllocator<'a> =
99    GraphReadAllocator<'a, Option<Box<dyn GraphReadAllocation>>>;
100
101/// A point read can be absent from the requested visible label set, or stop
102/// normally before owning its payload. A successful read transfers its permit.
103pub enum AdmittedNodeRead {
104    Missing,
105    Stopped,
106    Node(AdmittedNodeRecord),
107}
108
109/// Relationship point reads transfer the permit acquired before ownership.
110pub enum AdmittedRelationshipRead {
111    Missing,
112    Stopped,
113    Relationship(AdmittedRelationshipRecord),
114}
115
116pub struct AdmittedRelationshipRecord {
117    relationship: crate::RelRecord,
118    allocation: Box<dyn GraphReadAllocation>,
119}
120
121impl AdmittedRelationshipRecord {
122    pub(crate) fn new(
123        relationship: crate::RelRecord,
124        allocation: Box<dyn GraphReadAllocation>,
125    ) -> Self {
126        Self {
127            relationship,
128            allocation,
129        }
130    }
131
132    pub fn clone_admitted(
133        relationship: &crate::RelRecord,
134        allocation: Box<dyn GraphReadAllocation>,
135    ) -> hawdb_core::Result<Self> {
136        if allocation.bytes() < hawdb_core::ids::relationship_allocation_bytes(relationship) {
137            return Err(hawdb_core::HawDBError::Execution(
138                "relationship admission returned an insufficient allocation permit".into(),
139            ));
140        }
141        Ok(Self {
142            relationship: relationship.clone(),
143            allocation,
144        })
145    }
146
147    /// Borrow the admitted record while its source allocation remains owned.
148    pub fn relationship(&self) -> &crate::RelRecord {
149        &self.relationship
150    }
151
152    /// Drops the properties `keep` rejects. The record only shrinks, so its
153    /// allocation still covers it.
154    pub fn retain_properties(&mut self, mut keep: impl FnMut(&str) -> bool) {
155        self.relationship
156            .properties
157            .retain(|name, _| keep(name.as_str()));
158    }
159
160    pub fn into_parts(self) -> (crate::RelRecord, Box<dyn GraphReadAllocation>) {
161        (self.relationship, self.allocation)
162    }
163}
164
165/// Selected record and the reservation acquired before its values were owned.
166/// Buffered consumers retain both until the record is consumed or dropped.
167pub struct AdmittedProjectedNode {
168    node: crate::ProjectedNodeRecord,
169    allocation: Box<dyn GraphReadAllocation>,
170}
171
172impl AdmittedProjectedNode {
173    pub(crate) fn new(
174        node: crate::ProjectedNodeRecord,
175        allocation: Box<dyn GraphReadAllocation>,
176    ) -> Self {
177        Self { node, allocation }
178    }
179
180    /// Custom readers admit the selected allocation before copying any values.
181    pub fn clone_admitted(
182        node: &crate::NodeRecord,
183        properties: &std::collections::BTreeSet<String>,
184        allocation: Box<dyn GraphReadAllocation>,
185    ) -> hawdb_core::Result<Self> {
186        if allocation.bytes() < hawdb_core::ids::projected_node_allocation_bytes(node, properties) {
187            return Err(hawdb_core::HawDBError::Execution(
188                "projected node admission returned an insufficient allocation permit".into(),
189            ));
190        }
191        Ok(Self {
192            node: hawdb_core::ids::project_node_record_ref(node, properties),
193            allocation,
194        })
195    }
196
197    pub fn into_parts(self) -> (crate::ProjectedNodeRecord, Box<dyn GraphReadAllocation>) {
198        (self.node, self.allocation)
199    }
200}
201
202/// Full node and the reservation acquired before any owned values were copied.
203pub struct AdmittedNodeRecord {
204    node: crate::NodeRecord,
205    allocation: Box<dyn GraphReadAllocation>,
206}
207
208impl AdmittedNodeRecord {
209    pub(crate) fn new(node: crate::NodeRecord, allocation: Box<dyn GraphReadAllocation>) -> Self {
210        Self { node, allocation }
211    }
212
213    /// Custom resident readers can construct the admitted record directly
214    /// from a borrowed source. Reject a short permit before cloning any Value.
215    pub fn clone_admitted(
216        node: &crate::NodeRecord,
217        allocation: Box<dyn GraphReadAllocation>,
218    ) -> hawdb_core::Result<Self> {
219        if allocation.bytes() < hawdb_core::ids::node_allocation_bytes(node) {
220            return Err(hawdb_core::HawDBError::Execution(
221                "full node admission returned an insufficient allocation permit".into(),
222            ));
223        }
224        Ok(Self {
225            node: node.clone(),
226            allocation,
227        })
228    }
229
230    pub(crate) fn from_full_projection(input: AdmittedProjectedNode) -> Self {
231        let (node, allocation) = input.into_parts();
232        Self {
233            node: crate::NodeRecord {
234                id: node.id,
235                labels: node.labels,
236                properties: node.properties,
237            },
238            allocation,
239        }
240    }
241
242    pub fn into_parts(self) -> (crate::NodeRecord, Box<dyn GraphReadAllocation>) {
243        (self.node, self.allocation)
244    }
245}
246
247/// Shared allocation policy for concurrently live graph-read collections.
248pub struct GraphReadAdmission<'a> {
249    admit: std::cell::RefCell<
250        &'a mut dyn FnMut(usize) -> hawdb_core::Result<Box<dyn GraphReadAllocation>>,
251    >,
252}
253
254impl<'a> GraphReadAdmission<'a> {
255    pub fn new(
256        admit: &'a mut dyn FnMut(usize) -> hawdb_core::Result<Box<dyn GraphReadAllocation>>,
257    ) -> Self {
258        Self {
259            admit: std::cell::RefCell::new(admit),
260        }
261    }
262
263    pub fn reserve(&self, bytes: usize) -> hawdb_core::Result<Box<dyn GraphReadAllocation>> {
264        let allocation = (self.admit.borrow_mut())(bytes)?;
265        if allocation.bytes() < bytes {
266            return Err(hawdb_core::HawDBError::Execution(
267                "graph read admission returned an insufficient allocation permit".into(),
268            ));
269        }
270        Ok(allocation)
271    }
272}
273
274/// A graph-read key set whose permit grows before a new key is inserted.
275/// Each set owns a distinct permit, including simultaneously live AND/OR
276/// candidates. Duplicate keys neither allocate tree nodes nor grow the permit.
277pub struct AdmittedKeySet<T: Ord> {
278    keys: std::collections::BTreeSet<T>,
279    allocation: Box<dyn GraphReadAllocation>,
280}
281
282impl<T: Ord> AdmittedKeySet<T> {
283    pub fn new(admission: &GraphReadAdmission<'_>) -> hawdb_core::Result<Self> {
284        Ok(Self::with_allocation(admission.reserve(0)?))
285    }
286
287    pub(crate) fn with_allocation(allocation: Box<dyn GraphReadAllocation>) -> Self {
288        Self {
289            keys: std::collections::BTreeSet::new(),
290            allocation,
291        }
292    }
293
294    pub fn try_insert(&mut self, key: T) -> hawdb_core::Result<bool> {
295        // Grow also carries the caller's task checkpoint. Repeated lookup
296        // values must stay cancellable even when no new tree key is needed.
297        self.allocation.grow(0)?;
298        if self.keys.contains(&key) {
299            return Ok(false);
300        }
301        self.allocation.retain_state()?;
302        // Pinned std B-trees keep up to eleven keys plus node links. Cover
303        // the first sparse node and conservative per-key tree overhead. These
304        // read sets contain NodeId or borrowed Value keys, never owned Values.
305        let key_bytes = std::mem::size_of::<T>();
306        let bytes = self
307            .keys
308            .len()
309            .saturating_add(1)
310            .saturating_mul(64usize.saturating_add(key_bytes.saturating_mul(2)))
311            .saturating_add(128usize.saturating_add(key_bytes.saturating_mul(11)));
312        let additional = bytes.saturating_sub(self.allocation.bytes());
313        self.allocation.grow(additional)?;
314        if self.allocation.bytes() < bytes {
315            return Err(hawdb_core::HawDBError::Execution(
316                "graph read admission did not grow its allocation permit".into(),
317            ));
318        }
319        Ok(self.keys.insert(key))
320    }
321
322    pub fn try_extend(&mut self, keys: impl IntoIterator<Item = T>) -> hawdb_core::Result<()> {
323        for key in keys {
324            self.try_insert(key)?;
325        }
326        Ok(())
327    }
328}
329
330impl<T: Ord> std::ops::Deref for AdmittedKeySet<T> {
331    type Target = std::collections::BTreeSet<T>;
332    fn deref(&self) -> &Self::Target {
333        &self.keys
334    }
335}
336
337/// Capacity is admitted before a graph-read vector grows. Its permit can move
338/// with the vector into a consumer iterator.
339pub struct AdmittedVec<T> {
340    values: Vec<T>,
341    allocation: Box<dyn GraphReadAllocation>,
342}
343
344impl<T> AdmittedVec<T> {
345    pub fn new(admission: &GraphReadAdmission<'_>) -> hawdb_core::Result<Self> {
346        Ok(Self {
347            values: Vec::new(),
348            allocation: admission.reserve(0)?,
349        })
350    }
351
352    pub fn try_push(&mut self, value: T) -> hawdb_core::Result<()> {
353        if self.values.len() == self.values.capacity() {
354            let capacity = self.values.capacity().saturating_mul(2).max(4);
355            let bytes = capacity.saturating_mul(std::mem::size_of::<T>());
356            self.allocation
357                .grow(bytes.saturating_sub(self.allocation.bytes()))?;
358            if self.allocation.bytes() < bytes {
359                return Err(hawdb_core::HawDBError::Execution(
360                    "graph read admission did not grow its allocation permit".into(),
361                ));
362            }
363            self.values.reserve_exact(capacity - self.values.len());
364        }
365        self.values.push(value);
366        Ok(())
367    }
368
369    pub fn into_parts(self) -> (Vec<T>, Box<dyn GraphReadAllocation>) {
370        (self.values, self.allocation)
371    }
372
373    pub fn as_slice(&self) -> &[T] {
374        &self.values
375    }
376
377    /// Mutate admitted elements without changing vector capacity or ownership.
378    pub fn as_mut_slice(&mut self) -> &mut [T] {
379        &mut self.values
380    }
381
382    pub fn clear(&mut self) {
383        // The vector still owns its capacity; retain that allocation permit.
384        self.values.clear();
385    }
386
387    /// Remove a consumed or displaced element, retaining admitted capacity.
388    pub fn swap_remove(&mut self, index: usize) -> T {
389        self.values.swap_remove(index)
390    }
391}
392
393/// The immutable identity of one published database read.
394///
395/// The logical commit epoch controls visibility. The physical generation only
396/// identifies the checkpoint base beneath that logical snapshot; later commits
397/// may be represented by an immutable delta without changing the base.
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub struct PublishedReadView {
400    visible_commit_epoch: u64,
401    checkpoint_commit_epoch: Option<u64>,
402    physical_generation: Option<ManifestGeneration>,
403}
404
405impl PublishedReadView {
406    #[doc(hidden)]
407    pub fn new(
408        visible_commit_epoch: u64,
409        checkpoint_commit_epoch: Option<u64>,
410        physical_generation: Option<ManifestGeneration>,
411    ) -> Self {
412        debug_assert_eq!(
413            checkpoint_commit_epoch.is_some(),
414            physical_generation.is_some(),
415            "checkpoint epoch and physical generation must be published together"
416        );
417        debug_assert!(
418            checkpoint_commit_epoch.is_none_or(|epoch| epoch <= visible_commit_epoch),
419            "checkpoint commit epoch must not exceed the visible commit epoch"
420        );
421        Self {
422            visible_commit_epoch,
423            checkpoint_commit_epoch,
424            physical_generation,
425        }
426    }
427
428    pub const fn visible_commit_epoch(self) -> u64 {
429        self.visible_commit_epoch
430    }
431
432    pub const fn checkpoint_commit_epoch(self) -> Option<u64> {
433        self.checkpoint_commit_epoch
434    }
435
436    pub const fn physical_generation(self) -> Option<ManifestGeneration> {
437        self.physical_generation
438    }
439
440    pub const fn physical_base_is_current(self) -> bool {
441        matches!(
442            self.checkpoint_commit_epoch,
443            Some(checkpoint_commit_epoch) if checkpoint_commit_epoch == self.visible_commit_epoch
444        )
445    }
446
447    pub const fn has_delta_after_physical_generation(self) -> bool {
448        matches!(
449            self.checkpoint_commit_epoch,
450            Some(checkpoint_commit_epoch) if checkpoint_commit_epoch < self.visible_commit_epoch
451        )
452    }
453}