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