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