Skip to main content

nodedb_array/tile/
dense_tile.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Dense tile payload — flat row-major attribute arrays.
4//!
5//! The dense layout drops coordinate columns entirely: cell `i`'s
6//! coordinates are recovered from `i` and the tile's per-dim extents. It
7//! holds no row kinds, surrogates or valid times, so the bitemporal array
8//! store never writes it, and the engine refuses it as corruption wherever
9//! it reads a segment tile.
10
11use serde::{Deserialize, Serialize};
12
13use super::mbr::TileMBR;
14use crate::schema::ArraySchema;
15use crate::types::cell_value::value::CellValue;
16
17/// Dense tile payload.
18#[derive(
19    Debug,
20    Clone,
21    PartialEq,
22    Serialize,
23    Deserialize,
24    zerompk::ToMessagePack,
25    zerompk::FromMessagePack,
26)]
27pub struct DenseTile {
28    /// One column per schema attr, parallel to `schema.attrs`. Each
29    /// column has `cells_per_tile` entries; absent cells are
30    /// [`CellValue::Null`].
31    pub attr_cols: Vec<Vec<CellValue>>,
32    /// `tile_extents` snapshot — needed to recover cell coordinates
33    /// from a flat index without re-reading the schema.
34    pub tile_extents: Vec<u64>,
35    pub mbr: TileMBR,
36}
37
38impl DenseTile {
39    /// Empty dense tile sized for the given schema. All cells are
40    /// pre-filled with [`CellValue::Null`].
41    pub fn empty(schema: &ArraySchema) -> Self {
42        let cells = cells_per_tile(&schema.tile_extents);
43        Self {
44            attr_cols: (0..schema.attrs.len())
45                .map(|_| vec![CellValue::Null; cells])
46                .collect(),
47            tile_extents: schema.tile_extents.clone(),
48            mbr: TileMBR::new(schema.arity(), schema.attrs.len()),
49        }
50    }
51
52    pub fn cell_count(&self) -> usize {
53        cells_per_tile(&self.tile_extents)
54    }
55
56    /// Cell at flat index `i` for attribute column `attr_idx`.
57    pub fn cell(&self, attr_idx: usize, i: usize) -> Option<&CellValue> {
58        self.attr_cols.get(attr_idx).and_then(|c| c.get(i))
59    }
60}
61
62/// Total cells per tile = product of extents (saturating at usize::MAX).
63pub fn cells_per_tile(extents: &[u64]) -> usize {
64    let mut p: u128 = 1;
65    for e in extents {
66        p = p.saturating_mul(*e as u128);
67    }
68    if p > usize::MAX as u128 {
69        usize::MAX
70    } else {
71        p as usize
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::schema::ArraySchemaBuilder;
79    use crate::schema::attr_spec::{AttrSpec, AttrType};
80    use crate::schema::dim_spec::{DimSpec, DimType};
81    use crate::types::domain::{Domain, DomainBound};
82
83    fn schema_4x4() -> ArraySchema {
84        ArraySchemaBuilder::new("g")
85            .dim(DimSpec::new(
86                "x",
87                DimType::Int64,
88                Domain::new(DomainBound::Int64(0), DomainBound::Int64(3)),
89            ))
90            .dim(DimSpec::new(
91                "y",
92                DimType::Int64,
93                Domain::new(DomainBound::Int64(0), DomainBound::Int64(3)),
94            ))
95            .attr(AttrSpec::new("v", AttrType::Int64, true))
96            .tile_extents(vec![4, 4])
97            .build()
98            .unwrap()
99    }
100
101    #[test]
102    fn cells_per_tile_is_product_of_extents() {
103        assert_eq!(cells_per_tile(&[4, 4]), 16);
104        assert_eq!(cells_per_tile(&[2, 3, 5]), 30);
105        assert_eq!(cells_per_tile(&[1]), 1);
106    }
107
108    #[test]
109    fn dense_tile_starts_all_null() {
110        let s = schema_4x4();
111        let t = DenseTile::empty(&s);
112        assert_eq!(t.cell_count(), 16);
113        for col in &t.attr_cols {
114            assert!(col.iter().all(|c| matches!(c, CellValue::Null)));
115        }
116    }
117}