Skip to main content

nodedb_array/query/
project.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Attribute projection over a sparse tile.
4//!
5//! Drops attribute columns the caller doesn't need. Coordinates and
6//! per-dim dictionaries are preserved; only `attr_cols` and the MBR's
7//! `attr_stats` are subsetted. The output schema (with the same subset
8//! of attrs in the same order) must be produced by the caller; this
9//! operator does not mutate schemas.
10
11use crate::error::{ArrayError, ArrayResult};
12use crate::tile::sparse_tile::SparseTile;
13
14/// Ordered subset of attribute indices into `schema.attrs`. Duplicates
15/// are allowed (lets callers fan out a column) but each index must be
16/// in range.
17#[derive(Debug, Clone)]
18pub struct Projection {
19    pub attr_indices: Vec<usize>,
20}
21
22impl Projection {
23    pub fn new(attr_indices: Vec<usize>) -> Self {
24        Self { attr_indices }
25    }
26}
27
28/// Build a new tile carrying only the projected attribute columns.
29/// `proj` is validated against `tile.attr_cols.len()` (the tile's own
30/// attribute count, not a schema), so the operator is schema-free.
31pub fn project_sparse(tile: &SparseTile, proj: &Projection) -> ArrayResult<SparseTile> {
32    for &i in &proj.attr_indices {
33        if i >= tile.attr_cols.len() {
34            return Err(ArrayError::InvalidAttr {
35                array: String::new(),
36                attr: format!("idx {i}"),
37                detail: format!("attr index out of range (have {})", tile.attr_cols.len()),
38            });
39        }
40    }
41    let attr_cols = proj
42        .attr_indices
43        .iter()
44        .map(|&i| tile.attr_cols[i].clone())
45        .collect();
46    let attr_stats = proj
47        .attr_indices
48        .iter()
49        .map(|&i| tile.mbr.attr_stats[i].clone())
50        .collect();
51    let mut mbr = tile.mbr.clone();
52    mbr.attr_stats = attr_stats;
53    Ok(SparseTile {
54        dim_dicts: tile.dim_dicts.clone(),
55        attr_cols,
56        surrogates: tile.surrogates.clone(),
57        valid_from_ms: tile.valid_from_ms.clone(),
58        valid_until_ms: tile.valid_until_ms.clone(),
59        row_kinds: tile.row_kinds.clone(),
60        mbr,
61    })
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::schema::ArraySchema;
68    use crate::schema::ArraySchemaBuilder;
69    use crate::schema::attr_spec::{AttrSpec, AttrType};
70    use crate::schema::dim_spec::{DimSpec, DimType};
71    use crate::tile::sparse_tile::SparseTileBuilder;
72    use crate::types::cell_value::value::CellValue;
73    use crate::types::coord::value::CoordValue;
74    use crate::types::domain::{Domain, DomainBound};
75
76    fn schema() -> ArraySchema {
77        ArraySchemaBuilder::new("g")
78            .dim(DimSpec::new(
79                "x",
80                DimType::Int64,
81                Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)),
82            ))
83            .attr(AttrSpec::new("a", AttrType::Int64, true))
84            .attr(AttrSpec::new("b", AttrType::Float64, true))
85            .attr(AttrSpec::new("c", AttrType::String, true))
86            .tile_extents(vec![16])
87            .build()
88            .unwrap()
89    }
90
91    fn tile() -> SparseTile {
92        let s = schema();
93        let mut b = SparseTileBuilder::new(&s);
94        b.push(
95            &[CoordValue::Int64(0)],
96            &[
97                CellValue::Int64(1),
98                CellValue::Float64(1.5),
99                CellValue::String("x".into()),
100            ],
101        )
102        .unwrap();
103        b.push(
104            &[CoordValue::Int64(1)],
105            &[
106                CellValue::Int64(2),
107                CellValue::Float64(2.5),
108                CellValue::String("y".into()),
109            ],
110        )
111        .unwrap();
112        b.build()
113    }
114
115    #[test]
116    fn project_keeps_subset_in_order() {
117        let t = tile();
118        let p = Projection::new(vec![2, 0]);
119        let out = project_sparse(&t, &p).unwrap();
120        assert_eq!(out.attr_cols.len(), 2);
121        assert_eq!(out.attr_cols[0][0], CellValue::String("x".into()));
122        assert_eq!(out.attr_cols[1][0], CellValue::Int64(1));
123        assert_eq!(out.mbr.attr_stats.len(), 2);
124    }
125
126    #[test]
127    fn project_rejects_out_of_range() {
128        let t = tile();
129        let p = Projection::new(vec![5]);
130        assert!(project_sparse(&t, &p).is_err());
131    }
132}