Skip to main content

nodedb_types/columnar/
column_parse.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! [`std::fmt::Display`] and [`std::str::FromStr`] for [`ColumnType`], plus
4//! [`ColumnTypeParseError`].
5
6use std::fmt;
7use std::str::FromStr;
8
9use super::column_type::ColumnType;
10
11/// Error from parsing a column type string.
12#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
13#[non_exhaustive]
14pub enum ColumnTypeParseError {
15    #[error("unknown column type: '{0}'")]
16    Unknown(String),
17    #[error("'DATETIME' is not a valid type — use 'TIMESTAMP' instead")]
18    UseTimestamp,
19    #[error("invalid VECTOR dimension: '{0}' (must be a positive integer)")]
20    InvalidVectorDim(String),
21    #[error(
22        "invalid DECIMAL/NUMERIC params: '{0}' (expected DECIMAL(precision, scale) with precision 1-38 and scale <= precision)"
23    )]
24    InvalidDecimalParams(String),
25}
26
27impl fmt::Display for ColumnType {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::Int64 => f.write_str("BIGINT"),
31            Self::Float64 => f.write_str("FLOAT64"),
32            Self::String => f.write_str("TEXT"),
33            Self::Bool => f.write_str("BOOL"),
34            Self::Bytes => f.write_str("BYTES"),
35            Self::Timestamp => f.write_str("TIMESTAMP"),
36            Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
37            Self::SystemTimestamp => f.write_str("SYSTEM_TIMESTAMP"),
38            Self::Decimal { precision, scale } => write!(f, "DECIMAL({precision},{scale})"),
39            Self::Geometry => f.write_str("GEOMETRY"),
40            Self::Vector(dim) => write!(f, "VECTOR({dim})"),
41            Self::SparseVector => f.write_str("SPARSEVECTOR"),
42            Self::Uuid => f.write_str("UUID"),
43            Self::Json => f.write_str("JSON"),
44            Self::Ulid => f.write_str("ULID"),
45            Self::Duration => f.write_str("DURATION"),
46            Self::Array => f.write_str("ARRAY"),
47            Self::Set => f.write_str("SET"),
48            Self::Regex => f.write_str("REGEX"),
49            Self::Range => f.write_str("RANGE"),
50            Self::Record => f.write_str("RECORD"),
51        }
52    }
53}
54
55impl FromStr for ColumnType {
56    type Err = ColumnTypeParseError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        let upper = s.trim().to_uppercase();
60
61        // NUMERIC(p,s) / DECIMAL(p,s) special case.
62        if upper.starts_with("NUMERIC") || upper.starts_with("DECIMAL") {
63            let base = if upper.starts_with("NUMERIC") {
64                "NUMERIC"
65            } else {
66                "DECIMAL"
67            };
68            let rest = upper[base.len()..].trim();
69            if rest.is_empty() {
70                return Ok(Self::Decimal {
71                    precision: 38,
72                    scale: 10,
73                });
74            }
75            if rest.starts_with('(') && rest.ends_with(')') {
76                let inner = &rest[1..rest.len() - 1];
77                let parts: Vec<&str> = inner.splitn(2, ',').collect();
78                let precision: u8 = parts[0]
79                    .trim()
80                    .parse()
81                    .map_err(|_| ColumnTypeParseError::InvalidDecimalParams(rest.to_string()))?;
82                let scale: u8 = parts
83                    .get(1)
84                    .map(|p| p.trim())
85                    .unwrap_or("0")
86                    .parse()
87                    .map_err(|_| ColumnTypeParseError::InvalidDecimalParams(rest.to_string()))?;
88                if precision == 0 || precision > 38 {
89                    return Err(ColumnTypeParseError::InvalidDecimalParams(format!(
90                        "precision {precision} out of range 1-38"
91                    )));
92                }
93                if scale > precision {
94                    return Err(ColumnTypeParseError::InvalidDecimalParams(format!(
95                        "scale {scale} must be <= precision {precision}"
96                    )));
97                }
98                return Ok(Self::Decimal { precision, scale });
99            }
100            return Err(ColumnTypeParseError::InvalidDecimalParams(rest.to_string()));
101        }
102
103        // VECTOR(N) special case.
104        if upper.starts_with("VECTOR") {
105            let inner = upper
106                .trim_start_matches("VECTOR")
107                .trim()
108                .trim_start_matches('(')
109                .trim_end_matches(')')
110                .trim();
111            if inner.is_empty() {
112                return Err(ColumnTypeParseError::InvalidVectorDim("empty".into()));
113            }
114            let dim: u32 = inner
115                .parse()
116                .map_err(|_| ColumnTypeParseError::InvalidVectorDim(inner.into()))?;
117            if dim == 0 {
118                return Err(ColumnTypeParseError::InvalidVectorDim("0".into()));
119            }
120            return Ok(Self::Vector(dim));
121        }
122
123        match upper.as_str() {
124            "BIGINT" | "INT64" | "INTEGER" | "INT" => Ok(Self::Int64),
125            "FLOAT64" | "DOUBLE" | "REAL" | "FLOAT" => Ok(Self::Float64),
126            "TEXT" | "STRING" | "VARCHAR" => Ok(Self::String),
127            "BOOL" | "BOOLEAN" => Ok(Self::Bool),
128            "BYTES" | "BYTEA" | "BLOB" => Ok(Self::Bytes),
129            "TIMESTAMP" => Ok(Self::Timestamp),
130            "TIMESTAMPTZ" | "TIMESTAMP WITH TIME ZONE" => Ok(Self::Timestamptz),
131            "SYSTEM_TIMESTAMP" | "SYSTEMTIMESTAMP" => Ok(Self::SystemTimestamp),
132            "GEOMETRY" => Ok(Self::Geometry),
133            // Dimensionless: an exact keyword with no `(N)`. Placed as an exact
134            // match rather than a `starts_with` guard because "SPARSEVECTOR"
135            // does not prefix-collide with the `starts_with("VECTOR")` branch
136            // above ("SPARSEVECTOR" starts with "SPARSE", not "VECTOR").
137            "SPARSEVECTOR" => Ok(Self::SparseVector),
138            "UUID" => Ok(Self::Uuid),
139            "JSON" | "JSONB" => Ok(Self::Json),
140            "ULID" => Ok(Self::Ulid),
141            "DURATION" => Ok(Self::Duration),
142            "ARRAY" => Ok(Self::Array),
143            "SET" => Ok(Self::Set),
144            "REGEX" => Ok(Self::Regex),
145            "RANGE" => Ok(Self::Range),
146            "RECORD" => Ok(Self::Record),
147            "DATETIME" => Err(ColumnTypeParseError::UseTimestamp),
148            other => Err(ColumnTypeParseError::Unknown(other.to_string())),
149        }
150    }
151}