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