Skip to main content

nodedb_types/columnar/
profile.rs

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