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