Skip to main content

uqa_storage/document_store/
retained.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Immutable decoded fields keep their payload reservation through every retained reader.
8
9use super::{Arc, Document, DocumentMetadata, StoredDocument};
10use crate::{read_control::StorageReadControl, StorageBackendError, StorageBackendResult};
11use uqa_core::{
12    memory::{Budgeted, MemoryError},
13    FieldName, Value, ValueRetentionError,
14};
15
16/// One adopted field map and its shared payload charge. Caller-owned input and read outputs retain their own allocation responsibility; clones of this handle retain one lease. Opaque B-tree node slack and allocator bookkeeping are outside the decoded payload allowance.
17#[derive(Clone, Debug)]
18pub struct RetainedDocumentFields(Arc<Budgeted<Arc<Document>>>);
19
20impl RetainedDocumentFields {
21    /// Share immutable fields under the invoking allowance. Matching readers reuse their existing lease; a different allowance admits the existing payload before allocating another reader wrapper.
22    pub fn retain_with_control(&self, control: &StorageReadControl) -> StorageBackendResult<Self> {
23        control.check()?;
24        if self.shares_allowance(control) {
25            Ok(self.clone())
26        } else {
27            Self::new(Arc::clone(&**self.0), control)
28        }
29    }
30
31    pub(super) fn shares_allowance(&self, control: &StorageReadControl) -> bool {
32        self.0.budget().shares_allowance(control.memory())
33    }
34
35    /// Move already charged decoded fields into shared immutable storage. The input reservation covers live entries, key capacities and value payloads, excluding the map's inline layout. Foreign or incomplete leases are rejected; shared wrappers are reserved before allocation.
36    pub fn from_budgeted(
37        fields: Budgeted<Document>,
38        control: &StorageReadControl,
39    ) -> StorageBackendResult<Self> {
40        control.check()?;
41        let (fields, mut memory) = fields.into_parts();
42        if !memory.budget().shares_allowance(control.memory()) {
43            drop(fields);
44            return Err(StorageBackendError::Other(
45                "decoded document reservation belongs to a different allowance".into(),
46            ));
47        }
48        let value = Value::Map(fields);
49        let required = value
50            .retained_payload_bytes(control.memory(), control.cancellation())
51            .map_err(retention_error)?;
52        if memory.bytes() < required {
53            return Err(StorageBackendError::Other(
54                "decoded document reservation does not cover its payload".into(),
55            ));
56        }
57        // Removed migration fields and other discarded payloads need no continuing reservation.
58        let surplus = memory.bytes() - required;
59        drop(memory.split(surplus));
60        memory.grow(size_of::<Document>())?;
61        let Value::Map(fields) = value else {
62            unreachable!("document field map");
63        };
64        let fields = Arc::new(fields);
65        control.check()?;
66        Ok(Self(Budgeted::new(fields, memory).into_shared()?))
67    }
68
69    pub fn new(fields: Arc<Document>, control: &StorageReadControl) -> StorageBackendResult<Self> {
70        control.check()?;
71        let entries = fields
72            .len()
73            .checked_mul(size_of::<(FieldName, Value)>())
74            .ok_or(MemoryError::SizeOverflow)?;
75        let mut memory = control.memory().reserve(size_of::<Document>())?;
76        memory.grow(entries)?;
77        for (name, value) in fields.iter() {
78            control.check()?;
79            memory.grow(name.capacity())?;
80            let payload = value
81                .reserve_retained_payload(control.memory(), control.cancellation())
82                .map_err(retention_error)?;
83            memory.absorb(payload);
84        }
85        control.check()?;
86        Ok(Self(Budgeted::new(fields, memory).into_shared()?))
87    }
88
89    /// Transfer uniquely owned fields without copying their values. The returned document is caller-owned; retained siblings keep their original reservation.
90    pub fn into_document(self) -> Document {
91        match Arc::try_unwrap(self.0) {
92            Ok(fields) => {
93                let (fields, _memory) = fields.into_parts();
94                Arc::unwrap_or_clone(fields)
95            }
96            Err(fields) => (**fields).as_ref().clone(),
97        }
98    }
99
100    /// Keep an owned field map under the invoking allowance. Unique fields move with their original payload reservation; shared fields are copied before releasing this reader. Shared wrapper charges end after their values move out of those allocations. A foreign allowance is rejected instead of transferring its charge to another caller.
101    pub fn into_budgeted(
102        self,
103        control: &StorageReadControl,
104    ) -> StorageBackendResult<Budgeted<Document>> {
105        control.check()?;
106        if !self.shares_allowance(control) {
107            return Err(StorageBackendError::Other(
108                "retained document belongs to a different allowance".into(),
109            ));
110        }
111        let shared = match Arc::try_unwrap(self.0) {
112            Ok(fields) => {
113                let (fields, mut memory) = fields.into_parts();
114                drop(memory.split(size_of::<Budgeted<Arc<Document>>>()));
115                return match Arc::try_unwrap(fields) {
116                    Ok(fields) => {
117                        drop(memory.split(size_of::<Document>()));
118                        control.check()?;
119                        Ok(Budgeted::new(fields, memory))
120                    }
121                    Err(fields) => {
122                        let original = Budgeted::new(fields, memory);
123                        super::controlled_rows::copy_fields_budgeted(
124                            original.iter().map(|(name, value)| (name.as_str(), value)),
125                            control,
126                        )
127                    }
128                };
129            }
130            Err(shared) => shared,
131        };
132        super::controlled_rows::copy_fields_budgeted(
133            shared.iter().map(|(name, value)| (name.as_str(), value)),
134            control,
135        )
136    }
137}
138
139pub(super) fn retention_error(error: ValueRetentionError) -> StorageBackendError {
140    match error {
141        ValueRetentionError::Memory(error) => StorageBackendError::Memory(error),
142        ValueRetentionError::Cancelled(error) => StorageBackendError::Cancelled(error),
143        error @ ValueRetentionError::Malformed { .. } => super::decoding::malformed_value(&error),
144    }
145}
146
147/// An immutable decoded tuple keeps its public fields charged through the last shared reader; tuple metadata remains outside the public field namespace.
148#[derive(Clone, Debug)]
149pub struct RetainedStoredDocument {
150    fields: RetainedDocumentFields,
151    metadata: DocumentMetadata,
152}
153
154impl RetainedStoredDocument {
155    pub fn with_metadata(fields: RetainedDocumentFields, metadata: DocumentMetadata) -> Self {
156        Self { fields, metadata }
157    }
158
159    pub fn fields(&self) -> &Document {
160        &self.fields
161    }
162
163    pub fn retained_fields(&self) -> &RetainedDocumentFields {
164        &self.fields
165    }
166
167    pub fn metadata(&self) -> DocumentMetadata {
168        self.metadata
169    }
170
171    pub fn into_parts(self) -> (RetainedDocumentFields, DocumentMetadata) {
172        (self.fields, self.metadata)
173    }
174
175    /// Produce a caller-owned mutable tuple, ending this reader's retained ownership. Unique fields move without copying; retained siblings keep their original payload and lease.
176    pub fn into_stored(self) -> StoredDocument {
177        StoredDocument::with_metadata(self.fields.into_document(), self.metadata)
178    }
179
180    /// Produce an owned tuple without detaching the field payload reservation. Shared inputs are copied under the same invoking allowance, and tuple metadata moves unchanged.
181    pub fn into_budgeted(
182        self,
183        control: &StorageReadControl,
184    ) -> StorageBackendResult<Budgeted<StoredDocument>> {
185        let (fields, memory) = self.fields.into_budgeted(control)?.into_parts();
186        Ok(Budgeted::new(
187            StoredDocument::with_metadata(fields, self.metadata),
188            memory,
189        ))
190    }
191}
192
193impl AsRef<Document> for RetainedDocumentFields {
194    fn as_ref(&self) -> &Document {
195        &self.0
196    }
197}
198
199impl std::ops::Deref for RetainedDocumentFields {
200    type Target = Document;
201
202    fn deref(&self) -> &Self::Target {
203        self.as_ref()
204    }
205}
206
207#[cfg(test)]
208mod tests;