Skip to main content

molframe_core/provider/
payload.rs

1//! Typed chunk payloads with shared ownership and borrowed projections.
2
3use super::{ChunkId, DatasetId, LocalRow, LogicalRow, ProviderError};
4use crate::annotation::AtomAnnotation;
5use crate::chunk::AtomChunk;
6use crate::column::Presence;
7use crate::coords::CoordinateBlock;
8use crate::structure::Structure;
9use std::ops::Range;
10use std::sync::Arc;
11
12/// Stable global identity and compact local extent of one chunk.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct ChunkDescriptor {
15    dataset: DatasetId,
16    chunk: ChunkId,
17    logical_start: LogicalRow,
18    rows: u32,
19}
20
21impl ChunkDescriptor {
22    /// Creates a chunk descriptor.
23    ///
24    /// # Errors
25    ///
26    /// Rejects a logical range whose end exceeds `u64`.
27    pub fn new(
28        dataset: DatasetId,
29        chunk: ChunkId,
30        logical_start: LogicalRow,
31        rows: u32,
32    ) -> Result<Self, ProviderError> {
33        if rows > 0
34            && logical_start
35                .get()
36                .checked_add(u64::from(rows - 1))
37                .is_none()
38        {
39            return Err(ProviderError::IdentityOverflow {
40                dataset,
41                field: "logical row range",
42            });
43        }
44        Ok(Self {
45            dataset,
46            chunk,
47            logical_start,
48            rows,
49        })
50    }
51
52    /// Dataset identity.
53    #[must_use]
54    pub const fn dataset(self) -> DatasetId {
55        self.dataset
56    }
57
58    /// Chunk identity.
59    #[must_use]
60    pub const fn chunk(self) -> ChunkId {
61        self.chunk
62    }
63
64    /// First global row represented by local row zero.
65    #[must_use]
66    pub const fn logical_start(self) -> LogicalRow {
67        self.logical_start
68    }
69
70    /// Number of locally addressable rows.
71    #[must_use]
72    pub const fn rows(self) -> u32 {
73        self.rows
74    }
75
76    /// Resolves a compact local row to its stable global row.
77    ///
78    /// # Errors
79    ///
80    /// Returns a typed error when the local row lies outside this chunk.
81    pub fn resolve(self, local: LocalRow) -> Result<LogicalRow, ProviderError> {
82        if local.get() >= self.rows {
83            return Err(ProviderError::LocalRowOutOfRange {
84                row: local.get(),
85                rows: self.rows,
86            });
87        }
88        let value = self
89            .logical_start
90            .get()
91            .checked_add(u64::from(local.get()))
92            .ok_or(ProviderError::IdentityOverflow {
93                dataset: self.dataset,
94                field: "logical row",
95            })?;
96        Ok(LogicalRow::new(value))
97    }
98}
99
100/// One structure chunk retaining its immutable native snapshot.
101#[derive(Clone, Debug)]
102pub struct StructureChunk {
103    descriptor: ChunkDescriptor,
104    structure: Structure,
105    storage_chunk: usize,
106    coordinates: CoordinateBlock,
107    coordinate_range: Range<u32>,
108}
109
110impl StructureChunk {
111    /// Adapts one existing storage chunk without copying coordinates.
112    ///
113    /// # Errors
114    ///
115    /// Rejects missing chunks, models, coordinate ranges, or row mismatches.
116    pub fn shared(
117        descriptor: ChunkDescriptor,
118        structure: Structure,
119        storage_chunk: usize,
120    ) -> Result<Self, ProviderError> {
121        let chunk = structure.data().chunks.get(storage_chunk).ok_or(
122            ProviderError::StorageChunkUnavailable {
123                index: storage_chunk,
124            },
125        )?;
126        if chunk.len() != descriptor.rows() {
127            return Err(ProviderError::PayloadLengthMismatch {
128                declared: descriptor.rows(),
129                actual: chunk.len(),
130            });
131        }
132        let model = crate::ModelIndex::new(chunk.model());
133        let coordinates = structure.data().coords.block(model).cloned().ok_or(
134            ProviderError::ModelUnavailable {
135                model: chunk.model(),
136            },
137        )?;
138        let coordinate_range = chunk.atoms();
139        if coordinates.range(coordinate_range.clone()).is_none() {
140            return Err(ProviderError::StorageChunkUnavailable {
141                index: storage_chunk,
142            });
143        }
144        Ok(Self {
145            descriptor,
146            structure,
147            storage_chunk,
148            coordinates,
149            coordinate_range,
150        })
151    }
152
153    /// Stable chunk metadata.
154    #[must_use]
155    pub const fn descriptor(&self) -> ChunkDescriptor {
156        self.descriptor
157    }
158
159    /// Columnar atom metadata borrowed from the retained snapshot.
160    #[must_use]
161    pub fn atoms(&self) -> &AtomChunk {
162        &self.structure.data().chunks[self.storage_chunk]
163    }
164
165    /// Coordinates borrowed directly from shared native storage.
166    #[must_use]
167    pub fn positions(&self) -> &[[f32; 3]] {
168        match self.coordinates.range(self.coordinate_range.clone()) {
169            Some(positions) => positions,
170            None => &[],
171        }
172    }
173
174    /// Retained immutable structure snapshot.
175    #[must_use]
176    pub const fn structure(&self) -> &Structure {
177        &self.structure
178    }
179}
180
181/// Physical type of a property payload.
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub enum PropertyKind {
184    /// Boolean values.
185    Boolean,
186    /// Signed 64-bit integers.
187    Integer,
188    /// IEEE-754 64-bit values.
189    Real,
190    /// Interned symbols represented by their dictionary identity.
191    Symbol,
192}
193
194/// One property value projected without materialising a column.
195#[derive(Clone, Copy, Debug, PartialEq)]
196pub enum PropertyValue {
197    /// Boolean value and validity.
198    Boolean(bool, Presence),
199    /// Integer value and validity.
200    Integer(i64, Presence),
201    /// Real value and validity.
202    Real(f64, Presence),
203    /// Interned symbol and validity.
204    Symbol(crate::SymbolId, Presence),
205}
206
207/// One typed property chunk retaining its immutable source snapshot.
208#[derive(Clone, Debug)]
209pub struct PropertyChunk {
210    descriptor: ChunkDescriptor,
211    structure: Structure,
212    name: Arc<str>,
213    atom_start: u32,
214    storage_chunk: usize,
215}
216
217impl PropertyChunk {
218    /// Adapts a property over one existing structure chunk.
219    ///
220    /// # Errors
221    ///
222    /// Rejects missing, misaligned, or unavailable source chunks.
223    pub fn shared(
224        descriptor: ChunkDescriptor,
225        structure: Structure,
226        name: Arc<str>,
227        storage_chunk: usize,
228    ) -> Result<Self, ProviderError> {
229        let (atom_start, chunk_rows, property_rows, atom_count) = {
230            let chunk = structure.data().chunks.get(storage_chunk).ok_or(
231                ProviderError::StorageChunkUnavailable {
232                    index: storage_chunk,
233                },
234            )?;
235            let property = structure.annotations().get(&name).ok_or_else(|| {
236                ProviderError::PropertyUnavailable {
237                    name: Box::from(name.as_ref()),
238                }
239            })?;
240            (
241                chunk.atoms().start,
242                chunk.len(),
243                property.len(),
244                structure.atom_count(),
245            )
246        };
247        if chunk_rows != descriptor.rows() {
248            return Err(ProviderError::PayloadLengthMismatch {
249                declared: descriptor.rows(),
250                actual: chunk_rows,
251            });
252        }
253        if property_rows != atom_count {
254            return Err(ProviderError::PropertyLengthMismatch {
255                name: Box::from(name.as_ref()),
256                expected: atom_count,
257                actual: property_rows,
258            });
259        }
260        Ok(Self {
261            descriptor,
262            structure,
263            name,
264            atom_start,
265            storage_chunk,
266        })
267    }
268
269    /// Stable chunk metadata.
270    #[must_use]
271    pub const fn descriptor(&self) -> ChunkDescriptor {
272        self.descriptor
273    }
274
275    /// Exact property name.
276    #[must_use]
277    pub fn name(&self) -> &str {
278        &self.name
279    }
280
281    /// Physical property type.
282    #[must_use]
283    pub fn kind(&self) -> Option<PropertyKind> {
284        Some(match self.property()? {
285            AtomAnnotation::Boolean(_) => PropertyKind::Boolean,
286            AtomAnnotation::Integer(_) => PropertyKind::Integer,
287            AtomAnnotation::Real(_) => PropertyKind::Real,
288            AtomAnnotation::Symbol(_) => PropertyKind::Symbol,
289        })
290    }
291
292    /// Reads one local value without allocating or decoding unrelated rows.
293    ///
294    /// # Errors
295    ///
296    /// Rejects a local row outside this chunk.
297    pub fn value(&self, local: LocalRow) -> Result<PropertyValue, ProviderError> {
298        let _ = self.descriptor.resolve(local)?;
299        let atom =
300            self.atom_start
301                .checked_add(local.get())
302                .ok_or(ProviderError::IdentityOverflow {
303                    dataset: self.descriptor.dataset(),
304                    field: "structure-local atom row",
305                })?;
306        let property = self
307            .property()
308            .ok_or_else(|| ProviderError::PropertyUnavailable {
309                name: Box::from(self.name.as_ref()),
310            })?;
311        match property {
312            AtomAnnotation::Boolean(column) => column
313                .get(atom)
314                .map(|(value, presence)| PropertyValue::Boolean(value, presence)),
315            AtomAnnotation::Integer(column) => column
316                .get(atom)
317                .map(|(value, presence)| PropertyValue::Integer(value, presence)),
318            AtomAnnotation::Real(column) => column
319                .get(atom)
320                .map(|(value, presence)| PropertyValue::Real(value, presence)),
321            AtomAnnotation::Symbol(column) => column
322                .get(atom)
323                .map(|(value, presence)| PropertyValue::Symbol(value, presence)),
324        }
325        .ok_or(ProviderError::LocalRowOutOfRange {
326            row: local.get(),
327            rows: self.descriptor.rows(),
328        })
329    }
330
331    /// Borrowed integer values when this property has integer storage.
332    #[must_use]
333    pub fn integers(&self) -> Option<&[i64]> {
334        let Some(AtomAnnotation::Integer(column)) = self.property() else {
335            return None;
336        };
337        slice_values(column.values(), self.atom_start, self.descriptor.rows())
338    }
339
340    /// Borrowed real values when this property has real storage.
341    #[must_use]
342    pub fn reals(&self) -> Option<&[f64]> {
343        let Some(AtomAnnotation::Real(column)) = self.property() else {
344            return None;
345        };
346        slice_values(column.values(), self.atom_start, self.descriptor.rows())
347    }
348
349    /// Conservative bounds retained in the source chunk metadata.
350    #[must_use]
351    pub fn bounds(&self) -> crate::Aabb {
352        match self.structure.data().chunks.get(self.storage_chunk) {
353            Some(chunk) => chunk.stats().bounds,
354            None => crate::Aabb::EMPTY,
355        }
356    }
357
358    fn property(&self) -> Option<&AtomAnnotation> {
359        self.structure.annotations().get(&self.name)
360    }
361}
362
363/// One coordinate-frame chunk retaining shared aligned storage.
364#[derive(Clone, Debug)]
365pub struct FrameChunk {
366    descriptor: ChunkDescriptor,
367    coordinates: CoordinateBlock,
368    range: Range<u32>,
369}
370
371impl FrameChunk {
372    /// Retains a shared coordinate block and exposes one bounded range.
373    ///
374    /// # Errors
375    ///
376    /// Rejects invalid ranges and descriptor/payload length mismatches.
377    pub fn shared(
378        descriptor: ChunkDescriptor,
379        coordinates: CoordinateBlock,
380        range: Range<u32>,
381    ) -> Result<Self, ProviderError> {
382        let actual =
383            range
384                .end
385                .checked_sub(range.start)
386                .ok_or(ProviderError::PayloadLengthMismatch {
387                    declared: descriptor.rows(),
388                    actual: 0,
389                })?;
390        if actual != descriptor.rows() || coordinates.range(range.clone()).is_none() {
391            return Err(ProviderError::PayloadLengthMismatch {
392                declared: descriptor.rows(),
393                actual,
394            });
395        }
396        Ok(Self {
397            descriptor,
398            coordinates,
399            range,
400        })
401    }
402
403    /// Stable chunk metadata.
404    #[must_use]
405    pub const fn descriptor(&self) -> ChunkDescriptor {
406        self.descriptor
407    }
408
409    /// Coordinates borrowed directly from shared aligned storage.
410    #[must_use]
411    pub fn positions(&self) -> &[[f32; 3]] {
412        match self.coordinates.range(self.range.clone()) {
413            Some(positions) => positions,
414            None => &[],
415        }
416    }
417
418    /// Shared backing block for another zero-copy adapter.
419    #[must_use]
420    pub const fn coordinates(&self) -> &CoordinateBlock {
421        &self.coordinates
422    }
423
424    /// Conservative bounds computed over only this resident chunk.
425    #[must_use]
426    pub fn bounds(&self) -> crate::Aabb {
427        self.coordinates.bounds(self.range.clone())
428    }
429}
430
431fn slice_values<T>(values: &[T], start: u32, rows: u32) -> Option<&[T]> {
432    let start = usize::try_from(start).ok()?;
433    let rows = usize::try_from(rows).ok()?;
434    let end = start.checked_add(rows)?;
435    values.get(start..end)
436}
437
438#[cfg(test)]
439#[path = "payload_tests.rs"]
440mod tests;