Skip to main content

nodedb_physical/physical_plan/document/
declared_column.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! A declared numeric column of a schemaless document or KV collection.
4//!
5//! These engines store a row as a MessagePack map. The Data Plane re-types
6//! every value written to one of these columns by the rule the strict encoder
7//! applies, declared integer or float width included. The Control Plane
8//! derives the list from the catalog and ships it on `DocumentOp::Register`:
9//! from the declared text for a schemaless collection, from the typed schema
10//! for a KV collection.
11
12use nodedb_types::columnar::{ColumnDef, ColumnType, FloatWidth, IntWidth};
13
14/// One declared column whose stored value the write path re-types.
15#[derive(
16    Debug,
17    Clone,
18    PartialEq,
19    serde::Serialize,
20    serde::Deserialize,
21    zerompk::ToMessagePack,
22    zerompk::FromMessagePack,
23)]
24pub struct DeclaredColumn {
25    /// The field name, as the catalog records it.
26    pub name: String,
27    /// `Int64`, `Float64`, or `Decimal(Some(typmod))`.
28    pub column_type: ColumnType,
29    /// The declared integer width. `SMALLINT` and `INTEGER` bound the value.
30    pub int_width: Option<IntWidth>,
31    /// The declared float width. `REAL` refuses a finite value past `f32`.
32    pub float_width: Option<FloatWidth>,
33}
34
35impl DeclaredColumn {
36    /// The column `name` declared as `declared`, the DDL type text with its
37    /// modifiers.
38    ///
39    /// `Some` only for a type that fixes the stored numeric value: an
40    /// integer, a float, or a `DECIMAL(p,s)`. Every other declared type is
41    /// stored as given and returns `None`.
42    pub fn from_declared(name: &str, declared: &str) -> Option<Self> {
43        let column_type = ColumnType::from_declared_type(declared)?;
44        if !fixes_stored_value(&column_type) {
45            return None;
46        }
47        Some(Self {
48            name: name.to_string(),
49            column_type,
50            int_width: IntWidth::from_declared_type(declared),
51            float_width: FloatWidth::from_declared_type(declared),
52        })
53    }
54
55    /// The typed schema column `column`, with its declared width.
56    ///
57    /// `Some` only for a column type that fixes the stored numeric value, as
58    /// [`Self::from_declared`] decides.
59    pub fn from_column_def(column: &ColumnDef) -> Option<Self> {
60        fixes_stored_value(&column.column_type).then(|| Self {
61            name: column.name.clone(),
62            column_type: column.column_type,
63            int_width: column.int_width,
64            float_width: column.float_width,
65        })
66    }
67}
68
69/// Whether a column of `column_type` fixes the numeric value it stores: an
70/// integer, a float, or a `DECIMAL(p,s)`.
71fn fixes_stored_value(column_type: &ColumnType) -> bool {
72    matches!(
73        column_type,
74        ColumnType::Int64 | ColumnType::Float64 | ColumnType::Decimal(Some(_))
75    )
76}
77
78#[cfg(test)]
79mod tests {
80    use nodedb_types::columnar::DecimalTypmod;
81
82    use super::*;
83
84    #[test]
85    fn numeric_declarations_resolve_with_their_width() {
86        let small = DeclaredColumn::from_declared("n", "SMALLINT NOT NULL").expect("smallint");
87        assert_eq!(small.column_type, ColumnType::Int64);
88        assert_eq!(small.int_width, Some(IntWidth::I16));
89
90        let real = DeclaredColumn::from_declared("r", "REAL").expect("real");
91        assert_eq!(real.column_type, ColumnType::Float64);
92        assert_eq!(real.float_width, Some(FloatWidth::F32));
93
94        let typmod = DecimalTypmod::new(5, 2).expect("valid typmod");
95        let dec = DeclaredColumn::from_declared("d", "DECIMAL(5, 2)").expect("decimal");
96        assert_eq!(dec.column_type, ColumnType::Decimal(Some(typmod)));
97    }
98
99    #[test]
100    fn schema_column_keeps_its_declared_width() {
101        let small = ColumnDef::nullable("n", ColumnType::Int64).with_declared_width("SMALLINT");
102        let declared = DeclaredColumn::from_column_def(&small).expect("integer column");
103        assert_eq!(declared.int_width, Some(IntWidth::I16));
104        assert_eq!(
105            declared,
106            DeclaredColumn::from_declared("n", "SMALLINT").expect("smallint")
107        );
108        assert!(
109            DeclaredColumn::from_column_def(&ColumnDef::nullable("t", ColumnType::String))
110                .is_none()
111        );
112    }
113
114    #[test]
115    fn other_declarations_are_not_retyped() {
116        for declared in [
117            "TEXT",
118            "DECIMAL",
119            "BOOL",
120            "TIMESTAMP",
121            "VECTOR(3)",
122            "unknown",
123        ] {
124            assert!(
125                DeclaredColumn::from_declared("c", declared).is_none(),
126                "{declared}"
127            );
128        }
129    }
130}