Skip to main content

nodedb_array/schema/
array_schema.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Top-level array schema.
4//!
5//! `ArraySchema` is the canonical descriptor every layer of the engine
6//! agrees on: storage uses it for tile layout, the planner uses it for
7//! slice/aggregate validation, and SQL surfaces it through DDL. It is
8//! constructed via [`super::ArraySchemaBuilder`] so all invariants
9//! (dim/tile-extent arity, non-empty attrs, unique names) are enforced
10//! at one site.
11
12use serde::{Deserialize, Serialize};
13
14use super::attr_spec::AttrSpec;
15use super::cell_order::{CellOrder, TileOrder};
16use super::dim_spec::DimSpec;
17
18/// Full array schema.
19#[derive(
20    Debug,
21    Clone,
22    PartialEq,
23    Eq,
24    Serialize,
25    Deserialize,
26    zerompk::ToMessagePack,
27    zerompk::FromMessagePack,
28)]
29pub struct ArraySchema {
30    pub name: String,
31    pub dims: Vec<DimSpec>,
32    pub attrs: Vec<AttrSpec>,
33    /// One tile-extent per dim, same order as `dims`. The product of
34    /// extents (clamped to domain size) is the cells-per-tile budget.
35    pub tile_extents: Vec<u64>,
36    pub cell_order: CellOrder,
37    pub tile_order: TileOrder,
38}
39
40impl ArraySchema {
41    pub fn arity(&self) -> usize {
42        self.dims.len()
43    }
44
45    pub fn dim(&self, name: &str) -> Option<&DimSpec> {
46        self.dims.iter().find(|d| d.name == name)
47    }
48
49    pub fn attr(&self, name: &str) -> Option<&AttrSpec> {
50        self.attrs.iter().find(|a| a.name == name)
51    }
52
53    /// MessagePack encoding of the schema's *content* fields — the parts
54    /// that determine compatibility for cross-array element-wise ops.
55    /// Excludes the array `name` so two structurally-identical schemas
56    /// with different names produce identical bytes (and therefore
57    /// identical `schema_hash`).
58    pub fn content_msgpack(&self) -> Vec<u8> {
59        // Encode tuple of the structural fields. zerompk derives all
60        // cover the inner types.
61        let payload = SchemaContent {
62            dims: &self.dims,
63            attrs: &self.attrs,
64            tile_extents: &self.tile_extents,
65            cell_order: self.cell_order,
66            tile_order: self.tile_order,
67        };
68        zerompk::to_msgpack_vec(&payload).unwrap_or_default()
69    }
70}
71
72/// Borrowed view of the `ArraySchema` fields that participate in
73/// content-only hashing (everything except `name`).
74#[derive(zerompk::ToMessagePack)]
75struct SchemaContent<'a> {
76    dims: &'a Vec<DimSpec>,
77    attrs: &'a Vec<AttrSpec>,
78    tile_extents: &'a Vec<u64>,
79    cell_order: CellOrder,
80    tile_order: TileOrder,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::super::attr_spec::AttrType;
86    use super::super::dim_spec::DimType;
87    use super::*;
88    use crate::types::domain::{Domain, DomainBound};
89
90    #[test]
91    fn content_msgpack_excludes_name() {
92        // Two distinct-named schemas with identical structural content
93        // produce equal content_msgpack — this is the property
94        // ARRAY_ELEMENTWISE relies on to compare arrays by shape.
95        let mk = |name: &str| ArraySchema {
96            name: name.into(),
97            dims: vec![DimSpec::new(
98                "x",
99                DimType::Int64,
100                Domain::new(DomainBound::Int64(0), DomainBound::Int64(15)),
101            )],
102            attrs: vec![AttrSpec::new("v", AttrType::Float64, true)],
103            tile_extents: vec![4],
104            cell_order: CellOrder::Hilbert,
105            tile_order: TileOrder::Hilbert,
106        };
107        let a = mk("alpha");
108        let b = mk("beta");
109        assert_ne!(a.name, b.name);
110        assert_eq!(a.content_msgpack(), b.content_msgpack());
111    }
112
113    #[test]
114    fn schema_arity_matches_dim_count() {
115        let s = ArraySchema {
116            name: "g".into(),
117            dims: vec![
118                DimSpec::new(
119                    "chrom",
120                    DimType::Int64,
121                    Domain::new(DomainBound::Int64(0), DomainBound::Int64(24)),
122                ),
123                DimSpec::new(
124                    "pos",
125                    DimType::Int64,
126                    Domain::new(DomainBound::Int64(0), DomainBound::Int64(300_000_000)),
127                ),
128            ],
129            attrs: vec![AttrSpec::new("variant", AttrType::String, false)],
130            tile_extents: vec![1, 1_000_000],
131            cell_order: CellOrder::Hilbert,
132            tile_order: TileOrder::Hilbert,
133        };
134        assert_eq!(s.arity(), 2);
135        assert!(s.dim("chrom").is_some());
136        assert!(s.attr("variant").is_some());
137        assert!(s.dim("missing").is_none());
138    }
139}