Skip to main content

nodedb_types/columnar/
profile.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ColumnarProfile and DocumentMode — collection storage specializations.
4
5use std::fmt;
6
7use serde::{Deserialize, Serialize};
8
9use super::schema::StrictSchema;
10
11/// Specialization profile for columnar collections.
12#[derive(
13    Debug,
14    Clone,
15    PartialEq,
16    Eq,
17    Serialize,
18    Deserialize,
19    zerompk::ToMessagePack,
20    zerompk::FromMessagePack,
21)]
22#[serde(tag = "profile")]
23pub enum ColumnarProfile {
24    /// General analytics. No special constraints.
25    Plain,
26    /// Time-partitioned append-only storage for metrics and logs.
27    Timeseries { time_key: String, interval: String },
28    /// Geometry-optimized storage with automatic spatial indexing.
29    Spatial {
30        geometry_column: String,
31        auto_rtree: bool,
32        auto_geohash: bool,
33    },
34}
35
36impl ColumnarProfile {
37    pub fn as_str(&self) -> &'static str {
38        match self {
39            Self::Plain => "plain",
40            Self::Timeseries { .. } => "timeseries",
41            Self::Spatial { .. } => "spatial",
42        }
43    }
44
45    pub fn is_timeseries(&self) -> bool {
46        matches!(self, Self::Timeseries { .. })
47    }
48
49    pub fn is_spatial(&self) -> bool {
50        matches!(self, Self::Spatial { .. })
51    }
52}
53
54impl fmt::Display for ColumnarProfile {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str(self.as_str())
57    }
58}
59
60/// Storage mode for document collections.
61#[derive(
62    Debug,
63    Clone,
64    PartialEq,
65    Eq,
66    Serialize,
67    Deserialize,
68    Default,
69    zerompk::ToMessagePack,
70    zerompk::FromMessagePack,
71)]
72#[serde(tag = "mode")]
73pub enum DocumentMode {
74    /// Schemaless MessagePack documents. Default. CRDT-friendly.
75    #[default]
76    Schemaless,
77    /// Schema-enforced Binary Tuple documents.
78    Strict(StrictSchema),
79}
80
81impl DocumentMode {
82    pub fn is_schemaless(&self) -> bool {
83        matches!(self, Self::Schemaless)
84    }
85
86    pub fn is_strict(&self) -> bool {
87        matches!(self, Self::Strict(_))
88    }
89
90    pub fn as_str(&self) -> &'static str {
91        match self {
92            Self::Schemaless => "schemaless",
93            Self::Strict(_) => "strict",
94        }
95    }
96
97    pub fn schema(&self) -> Option<&StrictSchema> {
98        match self {
99            Self::Strict(s) => Some(s),
100            Self::Schemaless => None,
101        }
102    }
103}
104
105impl fmt::Display for DocumentMode {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        f.write_str(self.as_str())
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::columnar::{ColumnDef, ColumnType};
115
116    #[test]
117    fn columnar_profile_serde_roundtrip() {
118        let profiles = vec![
119            ColumnarProfile::Plain,
120            ColumnarProfile::Timeseries {
121                time_key: "time".into(),
122                interval: "1h".into(),
123            },
124            ColumnarProfile::Spatial {
125                geometry_column: "geom".into(),
126                auto_rtree: true,
127                auto_geohash: true,
128            },
129        ];
130        for p in profiles {
131            let json = sonic_rs::to_string(&p).unwrap();
132            let back: ColumnarProfile = sonic_rs::from_str(&json).unwrap();
133            assert_eq!(back, p);
134        }
135    }
136
137    #[test]
138    fn document_mode_default_is_schemaless() {
139        assert!(DocumentMode::default().is_schemaless());
140    }
141
142    #[test]
143    fn document_mode_strict_has_schema() {
144        let schema = StrictSchema::new(vec![ColumnDef::required("id", ColumnType::Int64)]).unwrap();
145        let mode = DocumentMode::Strict(schema.clone());
146        assert!(mode.is_strict());
147        assert_eq!(mode.schema().unwrap(), &schema);
148    }
149
150    #[test]
151    fn document_mode_serde_roundtrip() {
152        let modes = vec![
153            DocumentMode::Schemaless,
154            DocumentMode::Strict(
155                StrictSchema::new(vec![
156                    ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
157                    ColumnDef::nullable("name", ColumnType::String),
158                ])
159                .unwrap(),
160            ),
161        ];
162        for m in modes {
163            let json = sonic_rs::to_string(&m).unwrap();
164            let back: DocumentMode = sonic_rs::from_str(&json).unwrap();
165            assert_eq!(back, m);
166        }
167    }
168}