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    pub fn into_parts(self) -> (crate::RelRecord, Box<dyn GraphReadAllocation>) {
153        (self.relationship, self.allocation)
154    }
155}
156
157/// Selected record and the reservation acquired before its values were owned.
158/// Buffered consumers retain both until the record is consumed or dropped.
159pub struct AdmittedProjectedNode {
160    node: crate::ProjectedNodeRecord,
161    allocation: Box<dyn GraphReadAllocation>,
162}
163
164impl AdmittedProjectedNode {
165    pub(crate) fn new(
166        node: crate::ProjectedNodeRecord,
167        allocation: Box<dyn GraphReadAllocation>,
168    ) -> Self {
169        Self { node, allocation }
170    }
171
172    /// Custom readers admit the selected allocation before copying any values.
173    pub fn clone_admitted(
174        node: &crate::NodeRecord,
175        properties: &std::collections::BTreeSet<String>,
176        allocation: Box<dyn GraphReadAllocation>,
177    ) -> hawdb_core::Result<Self> {
178        if allocation.bytes() < hawdb_core::ids::projected_node_allocation_bytes(node, properties) {
179            return Err(hawdb_core::HawDBError::Execution(
180                "projected node admission returned an insufficient allocation permit".into(),
181            ));
182        }
183        Ok(Self {
184            node: hawdb_core::ids::project_node_record_ref(node, properties),
185            allocation,
186        })
187    }
188
189    pub fn into_parts(self) -> (crate::ProjectedNodeRecord, Box<dyn GraphReadAllocation>) {
190        (self.node, self.allocation)
191    }
192}
193
194/// Full node and the reservation acquired before any owned values were copied.
195pub struct AdmittedNodeRecord {
196    node: crate::NodeRecord,
197    allocation: Box<dyn GraphReadAllocation>,
198}
199
200impl AdmittedNodeRecord {
201    pub(crate) fn new(node: crate::NodeRecord, allocation: Box<dyn GraphReadAllocation>) -> Self {
202        Self { node, allocation }
203    }
204
205    /// Custom resident readers can construct the admitted record directly
206    /// from a borrowed source. Reject a short permit before cloning any Value.
207    pub fn clone_admitted(
208        node: &crate::NodeRecord,
209        allocation: Box<dyn GraphReadAllocation>,
210    ) -> hawdb_core::Result<Self> {
211        if allocation.bytes() < hawdb_core::ids::node_allocation_bytes(node) {
212            return Err(hawdb_core::HawDBError::Execution(
213                "full node admission returned an insufficient allocation permit".into(),
214            ));
215        }
216        Ok(Self {
217            node: node.clone(),
218            allocation,
219        })
220    }
221
222    pub(crate) fn from_full_projection(input: AdmittedProjectedNode) -> Self {
223        let (node, allocation) = input.into_parts();
224        Self {
225            node: crate::NodeRecord {
226                id: node.id,
227                labels: node.labels,
228                properties: node.properties,
229            },
230            allocation,
231        }
232    }
233
234    pub fn into_parts(self) -> (crate::NodeRecord, Box<dyn GraphReadAllocation>) {
235        (self.node, self.allocation)
236    }
237}
238
239/// Shared allocation policy for concurrently live graph-read collections.
240pub struct GraphReadAdmission<'a> {
241    admit: std::cell::RefCell<
242        &'a mut dyn FnMut(usize) -> hawdb_core::Result<Box<dyn GraphReadAllocation>>,
243    >,
244}
245
246impl<'a> GraphReadAdmission<'a> {
247    pub fn new(
248        admit: &'a mut dyn FnMut(usize) -> hawdb_core::Result<Box<dyn GraphReadAllocation>>,
249    ) -> Self {
250        Self {
251            admit: std::cell::RefCell::new(admit),
252        }
253    }
254
255    pub fn reserve(&self, bytes: usize) -> hawdb_core::Result<Box<dyn GraphReadAllocation>> {
256        let allocation = (self.admit.borrow_mut())(bytes)?;
257        if allocation.bytes() < bytes {
258            return Err(hawdb_core::HawDBError::Execution(
259                "graph read admission returned an insufficient allocation permit".into(),
260            ));
261        }
262        Ok(allocation)
263    }
264}
265
266/// A graph-read key set whose permit grows before a new key is inserted.
267/// Each set owns a distinct permit, including simultaneously live AND/OR
268/// candidates. Duplicate keys neither allocate tree nodes nor grow the permit.
269pub struct AdmittedKeySet<T: Ord> {
270    keys: std::collections::BTreeSet<T>,
271    allocation: Box<dyn GraphReadAllocation>,
272}
273
274impl<T: Ord> AdmittedKeySet<T> {
275    pub fn new(admission: &GraphReadAdmission<'_>) -> hawdb_core::Result<Self> {
276        Ok(Self::with_allocation(admission.reserve(0)?))
277    }
278
279    pub(crate) fn with_allocation(allocation: Box<dyn GraphReadAllocation>) -> Self {
280        Self {
281            keys: std::collections::BTreeSet::new(),
282            allocation,
283        }
284    }
285
286    pub fn try_insert(&mut self, key: T) -> hawdb_core::Result<bool> {
287        // Grow also carries the caller's task checkpoint. Repeated lookup
288        // values must stay cancellable even when no new tree key is needed.
289        self.allocation.grow(0)?;
290        if self.keys.contains(&key) {
291            return Ok(false);
292        }
293        self.allocation.retain_state()?;
294        // Pinned std B-trees keep up to eleven keys plus node links. Cover
295        // the first sparse node and conservative per-key tree overhead. These
296        // read sets contain NodeId or borrowed Value keys, never owned Values.
297        let key_bytes = std::mem::size_of::<T>();
298        let bytes = self
299            .keys
300            .len()
301            .saturating_add(1)
302            .saturating_mul(64usize.saturating_add(key_bytes.saturating_mul(2)))
303            .saturating_add(128usize.saturating_add(key_bytes.saturating_mul(11)));
304        let additional = bytes.saturating_sub(self.allocation.bytes());
305        self.allocation.grow(additional)?;
306        if self.allocation.bytes() < bytes {
307            return Err(hawdb_core::HawDBError::Execution(
308                "graph read admission did not grow its allocation permit".into(),
309            ));
310        }
311        Ok(self.keys.insert(key))
312    }
313
314    pub fn try_extend(&mut self, keys: impl IntoIterator<Item = T>) -> hawdb_core::Result<()> {
315        for key in keys {
316            self.try_insert(key)?;
317        }
318        Ok(())
319    }
320}
321
322impl<T: Ord> std::ops::Deref for AdmittedKeySet<T> {
323    type Target = std::collections::BTreeSet<T>;
324    fn deref(&self) -> &Self::Target {
325        &self.keys
326    }
327}
328
329/// Capacity is admitted before a graph-read vector grows. Its permit can move
330/// with the vector into a consumer iterator.
331pub struct AdmittedVec<T> {
332    values: Vec<T>,
333    allocation: Box<dyn GraphReadAllocation>,
334}
335
336impl<T> AdmittedVec<T> {
337    pub fn new(admission: &GraphReadAdmission<'_>) -> hawdb_core::Result<Self> {
338        Ok(Self {
339            values: Vec::new(),
340            allocation: admission.reserve(0)?,
341        })
342    }
343
344    pub fn try_push(&mut self, value: T) -> hawdb_core::Result<()> {
345        if self.values.len() == self.values.capacity() {
346            let capacity = self.values.capacity().saturating_mul(2).max(4);
347            let bytes = capacity.saturating_mul(std::mem::size_of::<T>());
348            self.allocation
349                .grow(bytes.saturating_sub(self.allocation.bytes()))?;
350            if self.allocation.bytes() < bytes {
351                return Err(hawdb_core::HawDBError::Execution(
352                    "graph read admission did not grow its allocation permit".into(),
353                ));
354            }
355            self.values.reserve_exact(capacity - self.values.len());
356        }
357        self.values.push(value);
358        Ok(())
359    }
360
361    pub fn into_parts(self) -> (Vec<T>, Box<dyn GraphReadAllocation>) {
362        (self.values, self.allocation)
363    }
364
365    pub fn as_slice(&self) -> &[T] {
366        &self.values
367    }
368
369    /// Mutate admitted elements without changing vector capacity or ownership.
370    pub fn as_mut_slice(&mut self) -> &mut [T] {
371        &mut self.values
372    }
373
374    pub fn clear(&mut self) {
375        // The vector still owns its capacity; retain that allocation permit.
376        self.values.clear();
377    }
378
379    /// Remove a consumed or displaced element, retaining admitted capacity.
380    pub fn swap_remove(&mut self, index: usize) -> T {
381        self.values.swap_remove(index)
382    }
383}
384
385/// The immutable identity of one published database read.
386///
387/// The logical commit epoch controls visibility. The physical generation only
388/// identifies the checkpoint base beneath that logical snapshot; later commits
389/// may be represented by an immutable delta without changing the base.
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub struct PublishedReadView {
392    visible_commit_epoch: u64,
393    checkpoint_commit_epoch: Option<u64>,
394    physical_generation: Option<ManifestGeneration>,
395}
396
397impl PublishedReadView {
398    #[doc(hidden)]
399    pub fn new(
400        visible_commit_epoch: u64,
401        checkpoint_commit_epoch: Option<u64>,
402        physical_generation: Option<ManifestGeneration>,
403    ) -> Self {
404        debug_assert_eq!(
405            checkpoint_commit_epoch.is_some(),
406            physical_generation.is_some(),
407            "checkpoint epoch and physical generation must be published together"
408        );
409        debug_assert!(
410            checkpoint_commit_epoch.is_none_or(|epoch| epoch <= visible_commit_epoch),
411            "checkpoint commit epoch must not exceed the visible commit epoch"
412        );
413        Self {
414            visible_commit_epoch,
415            checkpoint_commit_epoch,
416            physical_generation,
417        }
418    }
419
420    pub const fn visible_commit_epoch(self) -> u64 {
421        self.visible_commit_epoch
422    }
423
424    pub const fn checkpoint_commit_epoch(self) -> Option<u64> {
425        self.checkpoint_commit_epoch
426    }
427
428    pub const fn physical_generation(self) -> Option<ManifestGeneration> {
429        self.physical_generation
430    }
431
432    pub const fn physical_base_is_current(self) -> bool {
433        matches!(
434            self.checkpoint_commit_epoch,
435            Some(checkpoint_commit_epoch) if checkpoint_commit_epoch == self.visible_commit_epoch
436        )
437    }
438
439    pub const fn has_delta_after_physical_generation(self) -> bool {
440        matches!(
441            self.checkpoint_commit_epoch,
442            Some(checkpoint_commit_epoch) if checkpoint_commit_epoch < self.visible_commit_epoch
443        )
444    }
445}