Skip to main content

nodedb_sql/parser/
type_expr.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Parser and validator for type expression strings used by the typeguard system.
4//!
5//! Parses strings like `"STRING"`, `"INT|NULL"`, `"ARRAY<STRING>"` into a
6//! [`TypeExpr`] that can be used to validate [`nodedb_types::Value`] instances
7//! at write time.
8
9use nodedb_types::Value;
10
11use crate::error::SqlError;
12
13/// A parsed type expression that can validate Values.
14#[derive(Debug, Clone, PartialEq)]
15pub enum TypeExpr {
16    /// Matches `Value::Null` / absent.
17    Null,
18    /// Matches a specific Value variant.
19    Simple(SimpleType),
20    /// Typed array: every element must match inner.
21    TypedArray(Box<TypeExpr>),
22    /// Typed set: every element must match inner.
23    TypedSet(Box<TypeExpr>),
24    /// Union: value must match at least one variant.
25    Union(Vec<TypeExpr>),
26}
27
28/// Leaf type variants that map to a single Value discriminant.
29#[derive(Debug, Clone, PartialEq)]
30pub enum SimpleType {
31    Int,
32    Float,
33    String,
34    Bool,
35    Bytes,
36    Timestamp,
37    Timestamptz,
38    Decimal,
39    Uuid,
40    Ulid,
41    Geometry,
42    Duration,
43    /// Untyped array (any element type).
44    Array,
45    Object,
46    Json,
47    /// Untyped set (any element type).
48    Set,
49    Regex,
50    Range,
51    Record,
52    /// Fixed-dimension float32 vector.
53    Vector(u32),
54    /// Dimensionless sparse vector — a `'{id: weight}'` string literal. Unlike
55    /// `Vector(u32)` it takes no `(N)`; the sparse index carries no dimension.
56    SparseVector,
57}
58
59// ── Parser ───────────────────────────────────────────────────────────────────
60
61/// Parse a type expression string into a [`TypeExpr`].
62///
63/// # Examples
64///
65/// ```
66/// use nodedb_sql::parser::type_expr::{parse_type_expr, TypeExpr, SimpleType};
67///
68/// assert_eq!(parse_type_expr("STRING").unwrap(), TypeExpr::Simple(SimpleType::String));
69/// assert_eq!(
70///     parse_type_expr("STRING|NULL").unwrap(),
71///     TypeExpr::Union(vec![TypeExpr::Simple(SimpleType::String), TypeExpr::Null]),
72/// );
73/// ```
74pub fn parse_type_expr(s: &str) -> Result<TypeExpr, SqlError> {
75    let s = s.trim();
76    if s.is_empty() {
77        return Err(SqlError::Parse {
78            detail: "empty type expression".to_string(),
79        });
80    }
81    let mut pos = 0usize;
82    let chars: Vec<char> = s.chars().collect();
83    parse_union(&chars, &mut pos, false)
84}
85
86/// Parse `single_type ('|' single_type)*`.
87///
88/// When `stop_at_gt` is true the parser stops before a `>` character (used
89/// when parsing inside `ARRAY<...>` / `SET<...>`).
90fn parse_union(chars: &[char], pos: &mut usize, stop_at_gt: bool) -> Result<TypeExpr, SqlError> {
91    let mut variants: Vec<TypeExpr> = Vec::new();
92    variants.push(parse_single(chars, pos, stop_at_gt)?);
93
94    loop {
95        skip_ws(chars, pos);
96        if *pos >= chars.len() {
97            break;
98        }
99        if stop_at_gt && chars[*pos] == '>' {
100            break;
101        }
102        if chars[*pos] != '|' {
103            break;
104        }
105        *pos += 1; // consume '|'
106        skip_ws(chars, pos);
107        variants.push(parse_single(chars, pos, stop_at_gt)?);
108    }
109
110    if variants.len() == 1 {
111        Ok(variants.remove(0))
112    } else {
113        Ok(TypeExpr::Union(variants))
114    }
115}
116
117/// Parse a single type token (keyword, ARRAY<...>, SET<...>, VECTOR(N)).
118fn parse_single(chars: &[char], pos: &mut usize, stop_at_gt: bool) -> Result<TypeExpr, SqlError> {
119    skip_ws(chars, pos);
120    let keyword = read_keyword(chars, pos);
121    if keyword.is_empty() {
122        return Err(SqlError::Parse {
123            detail: format!(
124                "expected type keyword at position {pos}, found: {:?}",
125                chars.get(*pos)
126            ),
127        });
128    }
129
130    match keyword.as_str() {
131        "NULL" => Ok(TypeExpr::Null),
132
133        "INT" | "INTEGER" | "BIGINT" | "INT64" => Ok(TypeExpr::Simple(SimpleType::Int)),
134        "FLOAT" | "DOUBLE" | "REAL" | "FLOAT64" => Ok(TypeExpr::Simple(SimpleType::Float)),
135        "STRING" | "TEXT" | "VARCHAR" => Ok(TypeExpr::Simple(SimpleType::String)),
136        "BOOL" | "BOOLEAN" => Ok(TypeExpr::Simple(SimpleType::Bool)),
137        "BYTES" | "BYTEA" | "BLOB" => Ok(TypeExpr::Simple(SimpleType::Bytes)),
138        "TIMESTAMP" => Ok(TypeExpr::Simple(SimpleType::Timestamp)),
139        "TIMESTAMPTZ" => Ok(TypeExpr::Simple(SimpleType::Timestamptz)),
140        "DECIMAL" | "NUMERIC" => Ok(TypeExpr::Simple(SimpleType::Decimal)),
141        "UUID" => Ok(TypeExpr::Simple(SimpleType::Uuid)),
142        "ULID" => Ok(TypeExpr::Simple(SimpleType::Ulid)),
143        "GEOMETRY" => Ok(TypeExpr::Simple(SimpleType::Geometry)),
144        "DURATION" => Ok(TypeExpr::Simple(SimpleType::Duration)),
145        "OBJECT" => Ok(TypeExpr::Simple(SimpleType::Object)),
146        "JSON" => Ok(TypeExpr::Simple(SimpleType::Json)),
147        "REGEX" => Ok(TypeExpr::Simple(SimpleType::Regex)),
148        "RANGE" => Ok(TypeExpr::Simple(SimpleType::Range)),
149        "RECORD" => Ok(TypeExpr::Simple(SimpleType::Record)),
150
151        // Dimensionless — matched before the parameterized "VECTOR" block.
152        // `read_keyword` reads the whole "SPARSEVECTOR" token, so this exact
153        // arm claims it and the "VECTOR" `(N)` parser never sees it.
154        "SPARSEVECTOR" => Ok(TypeExpr::Simple(SimpleType::SparseVector)),
155
156        "VECTOR" => {
157            // Expect '(' digits ')'.
158            skip_ws(chars, pos);
159            if *pos >= chars.len() || chars[*pos] != '(' {
160                return Err(SqlError::Parse {
161                    detail: format!("expected '(' after VECTOR at position {pos}"),
162                });
163            }
164            *pos += 1; // consume '('
165            skip_ws(chars, pos);
166            let digits = read_digits(chars, pos);
167            if digits.is_empty() {
168                return Err(SqlError::Parse {
169                    detail: "expected dimension digits inside VECTOR(...)".to_string(),
170                });
171            }
172            let dim: u32 = digits.parse().map_err(|_| SqlError::Parse {
173                detail: format!("invalid VECTOR dimension: '{digits}'"),
174            })?;
175            if dim == 0 {
176                return Err(SqlError::Parse {
177                    detail: "VECTOR dimension must be > 0".to_string(),
178                });
179            }
180            skip_ws(chars, pos);
181            if *pos >= chars.len() || chars[*pos] != ')' {
182                return Err(SqlError::Parse {
183                    detail: format!("expected ')' to close VECTOR({dim} at position {pos}"),
184                });
185            }
186            *pos += 1; // consume ')'
187            Ok(TypeExpr::Simple(SimpleType::Vector(dim)))
188        }
189
190        "ARRAY" => {
191            // Optional typed variant: ARRAY<inner>
192            skip_ws(chars, pos);
193            if *pos < chars.len() && chars[*pos] == '<' {
194                *pos += 1; // consume '<'
195                skip_ws(chars, pos);
196                let inner = parse_union(chars, pos, true)?;
197                skip_ws(chars, pos);
198                if *pos >= chars.len() || chars[*pos] != '>' {
199                    return Err(SqlError::Parse {
200                        detail: format!("expected '>' to close ARRAY<...> at position {pos}"),
201                    });
202                }
203                *pos += 1; // consume '>'
204                Ok(TypeExpr::TypedArray(Box::new(inner)))
205            } else {
206                Ok(TypeExpr::Simple(SimpleType::Array))
207            }
208        }
209
210        "SET" => {
211            // Optional typed variant: SET<inner>
212            skip_ws(chars, pos);
213            if *pos < chars.len() && chars[*pos] == '<' && !stop_at_gt {
214                *pos += 1; // consume '<'
215                skip_ws(chars, pos);
216                let inner = parse_union(chars, pos, true)?;
217                skip_ws(chars, pos);
218                if *pos >= chars.len() || chars[*pos] != '>' {
219                    return Err(SqlError::Parse {
220                        detail: format!("expected '>' to close SET<...> at position {pos}"),
221                    });
222                }
223                *pos += 1; // consume '>'
224                Ok(TypeExpr::TypedSet(Box::new(inner)))
225            } else if *pos < chars.len() && chars[*pos] == '<' {
226                // Inside a nested context — consume the '<' and inner normally.
227                *pos += 1;
228                skip_ws(chars, pos);
229                let inner = parse_union(chars, pos, true)?;
230                skip_ws(chars, pos);
231                if *pos >= chars.len() || chars[*pos] != '>' {
232                    return Err(SqlError::Parse {
233                        detail: format!("expected '>' to close SET<...> at position {pos}"),
234                    });
235                }
236                *pos += 1;
237                Ok(TypeExpr::TypedSet(Box::new(inner)))
238            } else {
239                Ok(TypeExpr::Simple(SimpleType::Set))
240            }
241        }
242
243        other => Err(SqlError::Parse {
244            detail: format!("unknown type keyword: '{other}'"),
245        }),
246    }
247}
248
249fn skip_ws(chars: &[char], pos: &mut usize) {
250    while *pos < chars.len() && chars[*pos].is_ascii_whitespace() {
251        *pos += 1;
252    }
253}
254
255/// Read a contiguous alphabetic/digit/underscore token (uppercased).
256fn read_keyword(chars: &[char], pos: &mut usize) -> String {
257    let mut s = String::new();
258    while *pos < chars.len() {
259        let c = chars[*pos];
260        if c.is_ascii_alphanumeric() || c == '_' {
261            s.push(c.to_ascii_uppercase());
262            *pos += 1;
263        } else {
264            break;
265        }
266    }
267    s
268}
269
270/// Read contiguous ASCII digits.
271fn read_digits(chars: &[char], pos: &mut usize) -> String {
272    let mut s = String::new();
273    while *pos < chars.len() && chars[*pos].is_ascii_digit() {
274        s.push(chars[*pos]);
275        *pos += 1;
276    }
277    s
278}
279
280// ── Validator ────────────────────────────────────────────────────────────────
281
282/// Check if a [`Value`] matches a [`TypeExpr`].
283///
284/// Coercion rules:
285/// - `Simple(Float)` accepts `Value::Integer` (int→float widening).
286/// - `Simple(Timestamp)` accepts `Value::DateTime`, `Value::Integer`, and
287///   `Value::String` (same rules as `ColumnType::Timestamp.accepts()`).
288/// - `Simple(Decimal)` accepts `Value::Decimal`, `Value::Float`, `Value::Integer`,
289///   and `Value::String`.
290/// - `Simple(Uuid)` accepts `Value::Uuid` and `Value::String`.
291/// - `Simple(Geometry)` accepts `Value::Geometry` and `Value::String`.
292/// - `TypedArray(inner)` matches `Value::Array` where every element matches `inner`.
293/// - `TypedSet(inner)` matches `Value::Set` where every element matches `inner`.
294/// - `Union(variants)` matches if any variant matches.
295pub fn value_matches_type(value: &Value, expr: &TypeExpr) -> bool {
296    match expr {
297        TypeExpr::Null => matches!(value, Value::Null),
298
299        TypeExpr::Simple(simple) => value_matches_simple(value, simple),
300
301        TypeExpr::TypedArray(inner) => match value {
302            Value::Array(items) => items.iter().all(|item| value_matches_type(item, inner)),
303            _ => false,
304        },
305
306        TypeExpr::TypedSet(inner) => match value {
307            Value::Set(items) => items.iter().all(|item| value_matches_type(item, inner)),
308            _ => false,
309        },
310
311        TypeExpr::Union(variants) => variants.iter().any(|v| value_matches_type(value, v)),
312    }
313}
314
315fn value_matches_simple(value: &Value, simple: &SimpleType) -> bool {
316    match simple {
317        SimpleType::Int => matches!(value, Value::Integer(_)),
318        SimpleType::Float => matches!(value, Value::Float(_) | Value::Integer(_)),
319        SimpleType::String => matches!(value, Value::String(_)),
320        SimpleType::Bool => matches!(value, Value::Bool(_)),
321        SimpleType::Bytes => matches!(value, Value::Bytes(_)),
322        SimpleType::Timestamp => matches!(
323            value,
324            Value::NaiveDateTime(_) | Value::Integer(_) | Value::String(_)
325        ),
326        SimpleType::Timestamptz => matches!(
327            value,
328            Value::DateTime(_) | Value::Integer(_) | Value::String(_)
329        ),
330        SimpleType::Decimal => matches!(
331            value,
332            Value::Decimal(_) | Value::Float(_) | Value::Integer(_) | Value::String(_)
333        ),
334        SimpleType::Uuid => matches!(value, Value::Uuid(_) | Value::String(_)),
335        SimpleType::Ulid => matches!(value, Value::Ulid(_) | Value::String(_)),
336        SimpleType::Geometry => matches!(value, Value::Geometry(_) | Value::String(_)),
337        SimpleType::Duration => matches!(value, Value::Duration(_)),
338        SimpleType::Array => matches!(value, Value::Array(_)),
339        SimpleType::Object => matches!(value, Value::Object(_)),
340        SimpleType::Json => true, // Json accepts any value (same as ColumnType::Json)
341        SimpleType::Set => matches!(value, Value::Set(_)),
342        SimpleType::Regex => matches!(value, Value::Regex(_)),
343        SimpleType::Range => matches!(value, Value::Range { .. }),
344        SimpleType::Record => matches!(value, Value::Record { .. }),
345        SimpleType::Vector(_) => matches!(value, Value::Array(_) | Value::Bytes(_)),
346        // Sparse vector arrives as a `'{id: weight}'` string literal (raw bytes
347        // are also accepted, mirroring `ColumnType::SparseVector::accepts`).
348        SimpleType::SparseVector => matches!(value, Value::String(_) | Value::Bytes(_)),
349    }
350}
351
352// ── Tests ────────────────────────────────────────────────────────────────────
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    // ── Parsing ──────────────────────────────────────────────────────────────
359
360    #[test]
361    fn parse_simple() {
362        assert_eq!(
363            parse_type_expr("STRING").unwrap(),
364            TypeExpr::Simple(SimpleType::String)
365        );
366    }
367
368    #[test]
369    fn parse_union() {
370        assert_eq!(
371            parse_type_expr("STRING|NULL").unwrap(),
372            TypeExpr::Union(vec![TypeExpr::Simple(SimpleType::String), TypeExpr::Null])
373        );
374    }
375
376    #[test]
377    fn parse_typed_array() {
378        assert_eq!(
379            parse_type_expr("ARRAY<STRING>").unwrap(),
380            TypeExpr::TypedArray(Box::new(TypeExpr::Simple(SimpleType::String)))
381        );
382    }
383
384    #[test]
385    fn parse_typed_array_union() {
386        assert_eq!(
387            parse_type_expr("ARRAY<INT|FLOAT>").unwrap(),
388            TypeExpr::TypedArray(Box::new(TypeExpr::Union(vec![
389                TypeExpr::Simple(SimpleType::Int),
390                TypeExpr::Simple(SimpleType::Float),
391            ])))
392        );
393    }
394
395    #[test]
396    fn parse_vector() {
397        assert_eq!(
398            parse_type_expr("VECTOR(384)").unwrap(),
399            TypeExpr::Simple(SimpleType::Vector(384))
400        );
401    }
402
403    #[test]
404    fn parse_sparsevector() {
405        // Dimensionless keyword parses to the SparseVector leaf type.
406        assert_eq!(
407            parse_type_expr("SPARSEVECTOR").unwrap(),
408            TypeExpr::Simple(SimpleType::SparseVector)
409        );
410        assert_eq!(
411            parse_type_expr("sparsevector").unwrap(),
412            TypeExpr::Simple(SimpleType::SparseVector)
413        );
414    }
415
416    #[test]
417    fn match_sparsevector_accepts_string_literal() {
418        let expr = parse_type_expr("SPARSEVECTOR").unwrap();
419        assert!(value_matches_type(&Value::String("{3:0.5}".into()), &expr));
420        assert!(value_matches_type(&Value::Bytes(vec![1, 2, 3]), &expr));
421        assert!(!value_matches_type(&Value::Integer(1), &expr));
422    }
423
424    #[test]
425    fn parse_case_insensitive() {
426        assert_eq!(
427            parse_type_expr("string|null").unwrap(),
428            TypeExpr::Union(vec![TypeExpr::Simple(SimpleType::String), TypeExpr::Null])
429        );
430    }
431
432    #[test]
433    fn parse_aliases() {
434        // INT aliases
435        assert_eq!(
436            parse_type_expr("INTEGER").unwrap(),
437            TypeExpr::Simple(SimpleType::Int)
438        );
439        assert_eq!(
440            parse_type_expr("BIGINT").unwrap(),
441            TypeExpr::Simple(SimpleType::Int)
442        );
443        assert_eq!(
444            parse_type_expr("INT64").unwrap(),
445            TypeExpr::Simple(SimpleType::Int)
446        );
447        // TEXT alias
448        assert_eq!(
449            parse_type_expr("TEXT").unwrap(),
450            TypeExpr::Simple(SimpleType::String)
451        );
452        // BOOLEAN alias
453        assert_eq!(
454            parse_type_expr("BOOLEAN").unwrap(),
455            TypeExpr::Simple(SimpleType::Bool)
456        );
457        // BYTEA alias
458        assert_eq!(
459            parse_type_expr("BYTEA").unwrap(),
460            TypeExpr::Simple(SimpleType::Bytes)
461        );
462    }
463
464    #[test]
465    fn parse_typed_set() {
466        assert_eq!(
467            parse_type_expr("SET<INT>").unwrap(),
468            TypeExpr::TypedSet(Box::new(TypeExpr::Simple(SimpleType::Int)))
469        );
470    }
471
472    #[test]
473    fn parse_untyped_array() {
474        assert_eq!(
475            parse_type_expr("ARRAY").unwrap(),
476            TypeExpr::Simple(SimpleType::Array)
477        );
478    }
479
480    #[test]
481    fn parse_untyped_set() {
482        assert_eq!(
483            parse_type_expr("SET").unwrap(),
484            TypeExpr::Simple(SimpleType::Set)
485        );
486    }
487
488    #[test]
489    fn parse_error_unknown_keyword() {
490        assert!(parse_type_expr("FOOBAR").is_err());
491    }
492
493    #[test]
494    fn parse_error_empty() {
495        assert!(parse_type_expr("").is_err());
496        assert!(parse_type_expr("  ").is_err());
497    }
498
499    #[test]
500    fn parse_error_vector_zero_dim() {
501        assert!(parse_type_expr("VECTOR(0)").is_err());
502    }
503
504    // ── Matching ─────────────────────────────────────────────────────────────
505
506    #[test]
507    fn match_string() {
508        let expr = parse_type_expr("STRING").unwrap();
509        assert!(value_matches_type(&Value::String("hello".into()), &expr));
510        assert!(!value_matches_type(&Value::Integer(1), &expr));
511    }
512
513    #[test]
514    fn match_null_union() {
515        let expr = parse_type_expr("STRING|NULL").unwrap();
516        assert!(value_matches_type(&Value::Null, &expr));
517        assert!(value_matches_type(&Value::String("x".into()), &expr));
518        assert!(!value_matches_type(&Value::Integer(1), &expr));
519    }
520
521    #[test]
522    fn match_typed_array() {
523        let expr = parse_type_expr("ARRAY<INT>").unwrap();
524        assert!(value_matches_type(
525            &Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
526            &expr
527        ));
528    }
529
530    #[test]
531    fn match_typed_array_fail() {
532        let expr = parse_type_expr("ARRAY<INT>").unwrap();
533        // Mixed-type array should fail.
534        assert!(!value_matches_type(
535            &Value::Array(vec![Value::Integer(1), Value::String("x".into())]),
536            &expr
537        ));
538    }
539
540    #[test]
541    fn match_int_coercion() {
542        // Integer should match Float (widening coercion).
543        let expr = parse_type_expr("FLOAT").unwrap();
544        assert!(value_matches_type(&Value::Integer(42), &expr));
545    }
546
547    #[test]
548    fn no_match() {
549        let expr = parse_type_expr("STRING").unwrap();
550        assert!(!value_matches_type(&Value::Integer(99), &expr));
551    }
552
553    #[test]
554    fn match_timestamp_coercions() {
555        let expr = parse_type_expr("TIMESTAMP").unwrap();
556        assert!(value_matches_type(
557            &Value::String("2024-01-01".into()),
558            &expr
559        ));
560        assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
561    }
562
563    #[test]
564    fn parse_timestamptz() {
565        assert_eq!(
566            parse_type_expr("TIMESTAMPTZ").unwrap(),
567            TypeExpr::Simple(SimpleType::Timestamptz)
568        );
569    }
570
571    #[test]
572    fn match_timestamptz_coercions() {
573        let expr = parse_type_expr("TIMESTAMPTZ").unwrap();
574        let dt = nodedb_types::NdbDateTime::from_micros(1_700_000_000_000_000);
575        assert!(value_matches_type(&Value::DateTime(dt), &expr));
576        assert!(value_matches_type(
577            &Value::String("2024-01-01T00:00:00Z".into()),
578            &expr
579        ));
580        assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
581    }
582
583    #[test]
584    fn match_null_expr() {
585        let expr = TypeExpr::Null;
586        assert!(value_matches_type(&Value::Null, &expr));
587        assert!(!value_matches_type(&Value::Integer(0), &expr));
588    }
589
590    #[test]
591    fn match_json_accepts_any() {
592        let expr = TypeExpr::Simple(SimpleType::Json);
593        assert!(value_matches_type(&Value::Null, &expr));
594        assert!(value_matches_type(&Value::Integer(1), &expr));
595        assert!(value_matches_type(&Value::String("x".into()), &expr));
596        assert!(value_matches_type(&Value::Bool(true), &expr));
597    }
598
599    #[test]
600    fn match_vector_type() {
601        let expr = parse_type_expr("VECTOR(128)").unwrap();
602        // Accepts Array (float list) or Bytes (packed floats).
603        assert!(value_matches_type(
604            &Value::Array(vec![Value::Float(0.1)]),
605            &expr
606        ));
607        assert!(value_matches_type(&Value::Bytes(vec![0u8; 512]), &expr));
608        assert!(!value_matches_type(&Value::Integer(1), &expr));
609    }
610}