Skip to main content

nodedb_types/columnar/
column_def.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! [`ColumnDef`] and [`ColumnModifier`] — typed column definitions for strict
4//! document and columnar collections.
5
6use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10use super::column_type::ColumnType;
11use super::float_width::FloatWidth;
12use super::int_width::IntWidth;
13
14/// Column-level modifiers that designate special engine roles.
15///
16/// These tell the engine which column serves a specialized purpose.
17/// Extensible for future column roles (e.g., `PartitionKey`, `SortKey`).
18#[derive(
19    Debug,
20    Clone,
21    PartialEq,
22    Eq,
23    Hash,
24    Serialize,
25    Deserialize,
26    zerompk::ToMessagePack,
27    zerompk::FromMessagePack,
28)]
29#[msgpack(c_enum)]
30#[repr(u8)]
31pub enum ColumnModifier {
32    /// This column is the time-partitioning key (timeseries profile).
33    /// Exactly one required for timeseries collections.
34    TimeKey = 0,
35    /// This column has an automatic R-tree spatial index (spatial profile).
36    /// Exactly one required for spatial collections.
37    SpatialIndex = 1,
38}
39
40/// A single column definition in a strict document or columnar schema.
41///
42/// `#[non_exhaustive]` — new fields may be added (e.g. column-level
43/// compression hints, foreign-key metadata).
44#[non_exhaustive]
45#[derive(
46    Debug,
47    Clone,
48    PartialEq,
49    Eq,
50    Serialize,
51    Deserialize,
52    zerompk::ToMessagePack,
53    zerompk::FromMessagePack,
54)]
55pub struct ColumnDef {
56    pub name: String,
57    pub column_type: ColumnType,
58    pub nullable: bool,
59    pub default: Option<String>,
60    pub primary_key: bool,
61    /// Column-level modifiers (TIME_KEY, SPATIAL_INDEX, etc.).
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub modifiers: Vec<ColumnModifier>,
64    /// GENERATED ALWAYS AS expression (serialized SqlExpr JSON).
65    /// When set, this column is computed at write time, not supplied by the user.
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub generated_expr: Option<String>,
68    /// Column names this generated column depends on.
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub generated_deps: Vec<String>,
71    /// Schema version at which this column was added. Original columns have
72    /// version 1 (the default). Columns added via `ALTER ADD COLUMN` record
73    /// the schema version after the bump so the reader can build a physical
74    /// sub-schema for tuples written under older versions.
75    #[serde(default = "default_added_at_version")]
76    pub added_at_version: u32,
77    /// The declared width of an `Int64` column. Storage stays 8 bytes.
78    /// `SMALLINT` and `INTEGER` bound every stored value and narrow the wire
79    /// type. `None` is `BIGINT`.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub int_width: Option<IntWidth>,
82    /// The declared width of a `Float64` column. Storage stays 8 bytes.
83    /// `REAL` refuses a finite value past `f32` and narrows the wire type.
84    /// `None` is `DOUBLE PRECISION`.
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub float_width: Option<FloatWidth>,
87}
88
89fn default_added_at_version() -> u32 {
90    1
91}
92
93impl ColumnDef {
94    pub fn required(name: impl Into<String>, column_type: ColumnType) -> Self {
95        Self {
96            name: name.into(),
97            column_type,
98            nullable: false,
99            default: None,
100            primary_key: false,
101            modifiers: Vec::new(),
102            generated_expr: None,
103            generated_deps: Vec::new(),
104            added_at_version: 1,
105            int_width: None,
106            float_width: None,
107        }
108    }
109
110    pub fn nullable(name: impl Into<String>, column_type: ColumnType) -> Self {
111        Self {
112            name: name.into(),
113            column_type,
114            nullable: true,
115            default: None,
116            primary_key: false,
117            modifiers: Vec::new(),
118            generated_expr: None,
119            generated_deps: Vec::new(),
120            added_at_version: 1,
121            int_width: None,
122            float_width: None,
123        }
124    }
125
126    /// Record the numeric width `declared` names. `declared` is the DDL type
127    /// text of this column, modifiers included.
128    ///
129    /// Only an `Int64` column takes an integer width, and only a `Float64`
130    /// column takes a float width. Every other column keeps no width.
131    pub fn with_declared_width(mut self, declared: &str) -> Self {
132        self.int_width = match self.column_type {
133            ColumnType::Int64 => IntWidth::from_declared_type(declared),
134            _ => None,
135        };
136        self.float_width = match self.column_type {
137            ColumnType::Float64 => FloatWidth::from_declared_type(declared),
138            _ => None,
139        };
140        self
141    }
142
143    /// The SQL type name this column declares: its numeric width when one is
144    /// set, else the name of its column type. The name parses back to the
145    /// same column type and width.
146    pub fn declared_type_name(&self) -> String {
147        match (self.int_width, self.float_width) {
148            (Some(width), _) => width.pg_type_name().to_ascii_uppercase(),
149            (None, Some(width)) => width.pg_type_name().to_ascii_uppercase(),
150            (None, None) => self.column_type.to_string(),
151        }
152    }
153
154    pub fn with_primary_key(mut self) -> Self {
155        self.primary_key = true;
156        self.nullable = false;
157        self
158    }
159
160    /// Check if this column has the TIME_KEY modifier.
161    pub fn is_time_key(&self) -> bool {
162        self.modifiers.contains(&ColumnModifier::TimeKey)
163    }
164
165    /// Check if this column has the SPATIAL_INDEX modifier.
166    pub fn is_spatial_index(&self) -> bool {
167        self.modifiers.contains(&ColumnModifier::SpatialIndex)
168    }
169
170    pub fn with_default(mut self, expr: impl Into<String>) -> Self {
171        self.default = Some(expr.into());
172        self
173    }
174}
175
176impl fmt::Display for ColumnDef {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "{} {}", self.name, self.declared_type_name())?;
179        if !self.nullable {
180            write!(f, " NOT NULL")?;
181        }
182        if self.primary_key {
183            write!(f, " PRIMARY KEY")?;
184        }
185        if let Some(ref d) = self.default {
186            write!(f, " DEFAULT {d}")?;
187        }
188        Ok(())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn declared_width_lands_only_on_its_numeric_family() {
198        let small = ColumnDef::nullable("n", ColumnType::Int64).with_declared_width("SMALLINT");
199        assert_eq!(small.int_width, Some(IntWidth::I16));
200        assert_eq!(small.float_width, None);
201
202        let real = ColumnDef::nullable("r", ColumnType::Float64).with_declared_width("REAL");
203        assert_eq!(real.float_width, Some(FloatWidth::F32));
204        assert_eq!(real.int_width, None);
205
206        let text = ColumnDef::nullable("t", ColumnType::String).with_declared_width("SMALLINT");
207        assert_eq!((text.int_width, text.float_width), (None, None));
208    }
209
210    /// The declared name parses back to the column type the column holds.
211    #[test]
212    fn declared_type_name_parses_back_to_the_same_column() {
213        for declared in ["SMALLINT", "INTEGER", "BIGINT", "REAL", "DOUBLE PRECISION"] {
214            let column_type: ColumnType = declared.parse().expect("declared type parses");
215            let column = ColumnDef::nullable("c", column_type).with_declared_width(declared);
216            assert_eq!(column.declared_type_name(), declared);
217            let reparsed = ColumnDef::nullable("c", column_type)
218                .with_declared_width(&column.declared_type_name());
219            assert_eq!(reparsed, column);
220        }
221        assert_eq!(
222            ColumnDef::nullable("c", ColumnType::Int64).declared_type_name(),
223            "BIGINT"
224        );
225    }
226}