1use nodedb_types::Value;
10use nodedb_types::columnar::{ColumnType, DecimalTypmod};
11
12use crate::error::SqlError;
13
14#[derive(Debug, Clone, PartialEq)]
16pub enum TypeExpr {
17 Null,
19 Simple(SimpleType),
21 TypedArray(Box<TypeExpr>),
23 TypedSet(Box<TypeExpr>),
25 Union(Vec<TypeExpr>),
27}
28
29#[derive(Debug, Clone, PartialEq)]
35pub enum SimpleType {
36 Int,
37 Float,
38 String,
39 Bool,
40 Bytes,
41 Timestamp,
42 Timestamptz,
43 SystemTimestamp,
46 Decimal(Option<DecimalTypmod>),
49 Uuid,
50 Ulid,
51 Geometry,
52 Duration,
53 Array,
55 Object,
58 Json,
59 Set,
61 Regex,
62 Range,
63 Record,
64 Vector(u32),
66 SparseVector,
69}
70
71pub fn parse_type_expr(s: &str) -> Result<TypeExpr, SqlError> {
93 let s = s.trim();
94 if s.is_empty() {
95 return Err(SqlError::Parse {
96 detail: "empty type expression".to_string(),
97 });
98 }
99 let mut pos = 0usize;
100 let chars: Vec<char> = s.chars().collect();
101 let expr = parse_union(&chars, &mut pos, false)?;
102 skip_ws(&chars, &mut pos);
103 if pos < chars.len() {
104 let rest: String = chars[pos..].iter().collect();
105 return Err(SqlError::Parse {
106 detail: format!("unexpected trailing input in type expression: '{rest}'"),
107 });
108 }
109 Ok(expr)
110}
111
112fn parse_union(chars: &[char], pos: &mut usize, stop_at_gt: bool) -> Result<TypeExpr, SqlError> {
117 let mut variants: Vec<TypeExpr> = Vec::new();
118 variants.push(parse_single(chars, pos)?);
119
120 loop {
121 skip_ws(chars, pos);
122 if *pos >= chars.len() {
123 break;
124 }
125 if stop_at_gt && chars[*pos] == '>' {
126 break;
127 }
128 if chars[*pos] != '|' {
129 break;
130 }
131 *pos += 1; skip_ws(chars, pos);
133 variants.push(parse_single(chars, pos)?);
134 }
135
136 if variants.len() == 1 {
137 Ok(variants.remove(0))
138 } else {
139 Ok(TypeExpr::Union(variants))
140 }
141}
142
143fn parse_single(chars: &[char], pos: &mut usize) -> Result<TypeExpr, SqlError> {
145 skip_ws(chars, pos);
146 let keyword = read_keyword(chars, pos);
147 if keyword.is_empty() {
148 return Err(SqlError::Parse {
149 detail: format!(
150 "expected type keyword at position {pos}, found: {:?}",
151 chars.get(*pos)
152 ),
153 });
154 }
155
156 match keyword.as_str() {
157 "NULL" => Ok(TypeExpr::Null),
159
160 "OBJECT" => Ok(TypeExpr::Simple(SimpleType::Object)),
164
165 "ARRAY" | "SET" => {
168 skip_ws(chars, pos);
169 if *pos < chars.len() && chars[*pos] == '<' {
170 *pos += 1; skip_ws(chars, pos);
172 let inner = Box::new(parse_union(chars, pos, true)?);
173 skip_ws(chars, pos);
174 if *pos >= chars.len() || chars[*pos] != '>' {
175 return Err(SqlError::Parse {
176 detail: format!("expected '>' to close {keyword}<...> at position {pos}"),
177 });
178 }
179 *pos += 1; return Ok(if keyword == "ARRAY" {
181 TypeExpr::TypedArray(inner)
182 } else {
183 TypeExpr::TypedSet(inner)
184 });
185 }
186 parse_leaf(chars, pos, &keyword)
187 }
188
189 _ => parse_leaf(chars, pos, &keyword),
190 }
191}
192
193fn parse_leaf(chars: &[char], pos: &mut usize, keyword: &str) -> Result<TypeExpr, SqlError> {
199 let spelling = read_spelling(chars, pos, keyword)?;
200 let column_type: ColumnType = spelling.parse().map_err(|e| SqlError::Parse {
201 detail: format!("type '{spelling}': {e}"),
202 })?;
203 Ok(TypeExpr::Simple(simple_from_column_type(
204 column_type,
205 &spelling,
206 )?))
207}
208
209fn read_spelling(chars: &[char], pos: &mut usize, keyword: &str) -> Result<String, SqlError> {
221 let mut spelling = keyword.to_string();
222 loop {
223 if *pos < chars.len() && chars[*pos] == '(' {
224 spelling.push_str(&read_paren_group(chars, pos)?);
225 continue;
226 }
227 let mut probe = *pos;
228 skip_ws(chars, &mut probe);
229 if probe == *pos {
230 break;
231 }
232 let word = read_keyword(chars, &mut probe);
233 if word.is_empty() {
234 break;
235 }
236 spelling.push(' ');
237 spelling.push_str(&word);
238 *pos = probe;
239 }
240 Ok(spelling)
241}
242
243fn read_paren_group(chars: &[char], pos: &mut usize) -> Result<String, SqlError> {
245 let start = *pos;
246 let mut depth = 0usize;
247 let mut group = String::new();
248 while *pos < chars.len() {
249 let ch = chars[*pos];
250 group.push(ch);
251 *pos += 1;
252 match ch {
253 '(' => depth += 1,
254 ')' => {
255 depth = depth.saturating_sub(1);
256 if depth == 0 {
257 return Ok(group);
258 }
259 }
260 _ => {}
261 }
262 }
263 Err(SqlError::Parse {
264 detail: format!("unclosed '(' at position {start}"),
265 })
266}
267
268fn simple_from_column_type(
273 column_type: ColumnType,
274 spelling: &str,
275) -> Result<SimpleType, SqlError> {
276 Ok(match column_type {
277 ColumnType::Int64 => SimpleType::Int,
278 ColumnType::Float64 => SimpleType::Float,
279 ColumnType::String => SimpleType::String,
280 ColumnType::Bool => SimpleType::Bool,
281 ColumnType::Bytes => SimpleType::Bytes,
282 ColumnType::Timestamp => SimpleType::Timestamp,
283 ColumnType::Timestamptz => SimpleType::Timestamptz,
284 ColumnType::SystemTimestamp => SimpleType::SystemTimestamp,
285 ColumnType::Decimal(typmod) => SimpleType::Decimal(typmod),
286 ColumnType::Geometry => SimpleType::Geometry,
287 ColumnType::Vector(dim) => SimpleType::Vector(dim),
288 ColumnType::SparseVector => SimpleType::SparseVector,
289 ColumnType::Uuid => SimpleType::Uuid,
290 ColumnType::Json => SimpleType::Json,
291 ColumnType::Ulid => SimpleType::Ulid,
292 ColumnType::Duration => SimpleType::Duration,
293 ColumnType::Array => SimpleType::Array,
294 ColumnType::Set => SimpleType::Set,
295 ColumnType::Regex => SimpleType::Regex,
296 ColumnType::Range => SimpleType::Range,
297 ColumnType::Record => SimpleType::Record,
298 other => {
299 return Err(SqlError::Parse {
300 detail: format!(
301 "type '{spelling}' resolves to {other}, which no typeguard validates"
302 ),
303 });
304 }
305 })
306}
307
308fn skip_ws(chars: &[char], pos: &mut usize) {
309 while *pos < chars.len() && chars[*pos].is_ascii_whitespace() {
310 *pos += 1;
311 }
312}
313
314fn read_keyword(chars: &[char], pos: &mut usize) -> String {
316 let mut s = String::new();
317 while *pos < chars.len() {
318 let c = chars[*pos];
319 if c.is_ascii_alphanumeric() || c == '_' {
320 s.push(c.to_ascii_uppercase());
321 *pos += 1;
322 } else {
323 break;
324 }
325 }
326 s
327}
328
329pub fn value_matches_type(value: &Value, expr: &TypeExpr) -> bool {
346 match expr {
347 TypeExpr::Null => matches!(value, Value::Null),
348
349 TypeExpr::Simple(simple) => value_matches_simple(value, simple),
350
351 TypeExpr::TypedArray(inner) => match value {
352 Value::Array(items) => items.iter().all(|item| value_matches_type(item, inner)),
353 _ => false,
354 },
355
356 TypeExpr::TypedSet(inner) => match value {
357 Value::Set(items) => items.iter().all(|item| value_matches_type(item, inner)),
358 _ => false,
359 },
360
361 TypeExpr::Union(variants) => variants.iter().any(|v| value_matches_type(value, v)),
362 }
363}
364
365fn value_matches_simple(value: &Value, simple: &SimpleType) -> bool {
366 match simple {
367 SimpleType::Int => matches!(value, Value::Integer(_)),
368 SimpleType::Float => matches!(value, Value::Float(_) | Value::Integer(_)),
369 SimpleType::String => matches!(value, Value::String(_)),
370 SimpleType::Bool => matches!(value, Value::Bool(_)),
371 SimpleType::Bytes => matches!(value, Value::Bytes(_)),
372 SimpleType::Timestamp => matches!(
373 value,
374 Value::NaiveDateTime(_) | Value::Integer(_) | Value::String(_)
375 ),
376 SimpleType::Timestamptz => matches!(
377 value,
378 Value::DateTime(_) | Value::Integer(_) | Value::String(_)
379 ),
380 SimpleType::SystemTimestamp => matches!(value, Value::DateTime(_) | Value::Integer(_)),
383 SimpleType::Decimal(_) => matches!(
384 value,
385 Value::Decimal(_) | Value::Float(_) | Value::Integer(_) | Value::String(_)
386 ),
387 SimpleType::Uuid => matches!(value, Value::Uuid(_) | Value::String(_)),
388 SimpleType::Ulid => matches!(value, Value::Ulid(_) | Value::String(_)),
389 SimpleType::Geometry => matches!(value, Value::Geometry(_) | Value::String(_)),
390 SimpleType::Duration => matches!(value, Value::Duration(_)),
391 SimpleType::Array => matches!(value, Value::Array(_)),
392 SimpleType::Object => matches!(value, Value::Object(_)),
393 SimpleType::Json => true, SimpleType::Set => matches!(value, Value::Set(_)),
395 SimpleType::Regex => matches!(value, Value::Regex(_)),
396 SimpleType::Range => matches!(value, Value::Range { .. }),
397 SimpleType::Record => matches!(value, Value::Record { .. }),
398 SimpleType::Vector(_) => matches!(value, Value::Array(_) | Value::Bytes(_)),
399 SimpleType::SparseVector => matches!(value, Value::String(_) | Value::Bytes(_)),
402 }
403}
404
405#[cfg(test)]
408mod tests {
409 use super::*;
410
411 #[test]
414 fn parse_simple() {
415 assert_eq!(
416 parse_type_expr("STRING").unwrap(),
417 TypeExpr::Simple(SimpleType::String)
418 );
419 }
420
421 #[test]
422 fn parse_union() {
423 assert_eq!(
424 parse_type_expr("STRING|NULL").unwrap(),
425 TypeExpr::Union(vec![TypeExpr::Simple(SimpleType::String), TypeExpr::Null])
426 );
427 }
428
429 #[test]
430 fn parse_typed_array() {
431 assert_eq!(
432 parse_type_expr("ARRAY<STRING>").unwrap(),
433 TypeExpr::TypedArray(Box::new(TypeExpr::Simple(SimpleType::String)))
434 );
435 }
436
437 #[test]
438 fn parse_typed_array_union() {
439 assert_eq!(
440 parse_type_expr("ARRAY<INT|FLOAT>").unwrap(),
441 TypeExpr::TypedArray(Box::new(TypeExpr::Union(vec![
442 TypeExpr::Simple(SimpleType::Int),
443 TypeExpr::Simple(SimpleType::Float),
444 ])))
445 );
446 }
447
448 #[test]
449 fn parse_vector() {
450 assert_eq!(
451 parse_type_expr("VECTOR(384)").unwrap(),
452 TypeExpr::Simple(SimpleType::Vector(384))
453 );
454 }
455
456 #[test]
457 fn parse_sparsevector() {
458 assert_eq!(
460 parse_type_expr("SPARSEVECTOR").unwrap(),
461 TypeExpr::Simple(SimpleType::SparseVector)
462 );
463 assert_eq!(
464 parse_type_expr("sparsevector").unwrap(),
465 TypeExpr::Simple(SimpleType::SparseVector)
466 );
467 }
468
469 #[test]
470 fn match_sparsevector_accepts_string_literal() {
471 let expr = parse_type_expr("SPARSEVECTOR").unwrap();
472 assert!(value_matches_type(&Value::String("{3:0.5}".into()), &expr));
473 assert!(value_matches_type(&Value::Bytes(vec![1, 2, 3]), &expr));
474 assert!(!value_matches_type(&Value::Integer(1), &expr));
475 }
476
477 #[test]
478 fn parse_case_insensitive() {
479 assert_eq!(
480 parse_type_expr("string|null").unwrap(),
481 TypeExpr::Union(vec![TypeExpr::Simple(SimpleType::String), TypeExpr::Null])
482 );
483 }
484
485 #[test]
486 fn parse_aliases() {
487 assert_eq!(
489 parse_type_expr("INTEGER").unwrap(),
490 TypeExpr::Simple(SimpleType::Int)
491 );
492 assert_eq!(
493 parse_type_expr("BIGINT").unwrap(),
494 TypeExpr::Simple(SimpleType::Int)
495 );
496 assert_eq!(
497 parse_type_expr("INT64").unwrap(),
498 TypeExpr::Simple(SimpleType::Int)
499 );
500 assert_eq!(
502 parse_type_expr("TEXT").unwrap(),
503 TypeExpr::Simple(SimpleType::String)
504 );
505 assert_eq!(
507 parse_type_expr("BOOLEAN").unwrap(),
508 TypeExpr::Simple(SimpleType::Bool)
509 );
510 assert_eq!(
512 parse_type_expr("BYTEA").unwrap(),
513 TypeExpr::Simple(SimpleType::Bytes)
514 );
515 }
516
517 #[test]
518 fn parse_typed_set() {
519 assert_eq!(
520 parse_type_expr("SET<INT>").unwrap(),
521 TypeExpr::TypedSet(Box::new(TypeExpr::Simple(SimpleType::Int)))
522 );
523 }
524
525 #[test]
526 fn parse_untyped_array() {
527 assert_eq!(
528 parse_type_expr("ARRAY").unwrap(),
529 TypeExpr::Simple(SimpleType::Array)
530 );
531 }
532
533 #[test]
534 fn parse_untyped_set() {
535 assert_eq!(
536 parse_type_expr("SET").unwrap(),
537 TypeExpr::Simple(SimpleType::Set)
538 );
539 }
540
541 #[test]
544 fn parse_timestamp_zone_spellings() {
545 assert_eq!(
546 parse_type_expr("TIMESTAMP WITH TIME ZONE").unwrap(),
547 TypeExpr::Simple(SimpleType::Timestamptz)
548 );
549 assert_eq!(
550 parse_type_expr("timestamp without time zone").unwrap(),
551 TypeExpr::Simple(SimpleType::Timestamp)
552 );
553 }
554
555 #[test]
558 fn parse_error_trailing_words() {
559 assert!(parse_type_expr("TIMESTAMP GARBAGE WORDS").is_err());
560 assert!(parse_type_expr("STRING FOO").is_err());
561 assert!(parse_type_expr("TIMESTAMP WITH").is_err());
562 assert!(parse_type_expr("INT )").is_err());
563 assert!(parse_type_expr("VECTOR(3) EXTRA").is_err());
564 }
565
566 #[test]
570 fn parse_every_declared_numeric_keyword() {
571 use nodedb_types::columnar::{DECLARED_FLOAT_KEYWORDS, DECLARED_INT_KEYWORDS};
572 for keyword in DECLARED_INT_KEYWORDS {
573 assert_eq!(
574 parse_type_expr(keyword).unwrap(),
575 TypeExpr::Simple(SimpleType::Int),
576 "{keyword} must parse as an integer leaf"
577 );
578 }
579 for keyword in DECLARED_FLOAT_KEYWORDS {
580 assert_eq!(
581 parse_type_expr(keyword).unwrap(),
582 TypeExpr::Simple(SimpleType::Float),
583 "{keyword} must parse as a float leaf"
584 );
585 }
586 }
587
588 #[test]
589 fn parse_jsonb() {
590 assert_eq!(
591 parse_type_expr("JSONB").unwrap(),
592 TypeExpr::Simple(SimpleType::Json)
593 );
594 }
595
596 #[test]
597 fn parse_decimal_carries_declared_params() {
598 assert_eq!(
599 parse_type_expr("DECIMAL(10, 2)").unwrap(),
600 TypeExpr::Simple(SimpleType::Decimal(Some(
601 DecimalTypmod::new(10, 2).expect("valid typmod")
602 )))
603 );
604 assert_eq!(
605 parse_type_expr("NUMERIC").unwrap(),
606 TypeExpr::Simple(SimpleType::Decimal(None))
607 );
608 assert!(parse_type_expr("DECIMAL(1001,0)").is_err());
609 assert!(parse_type_expr("DECIMAL(0)").is_err());
610 }
611
612 #[test]
615 fn parse_varchar_length() {
616 let expr = parse_type_expr("VARCHAR(255)").unwrap();
617 assert_eq!(expr, TypeExpr::Simple(SimpleType::String));
618 assert!(value_matches_type(&Value::String("x".into()), &expr));
619 assert!(parse_type_expr("VARCHAR(0)").is_err());
620 }
621
622 #[test]
625 fn parse_and_match_system_timestamp() {
626 let expr = parse_type_expr("SYSTEM_TIMESTAMP").unwrap();
627 assert_eq!(expr, TypeExpr::Simple(SimpleType::SystemTimestamp));
628 let dt = nodedb_types::NdbDateTime::from_micros(1_700_000_000_000_000);
629 assert!(value_matches_type(&Value::DateTime(dt), &expr));
630 assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
631 assert!(!value_matches_type(
632 &Value::String("2024-01-01".into()),
633 &expr
634 ));
635 }
636
637 #[test]
638 fn parse_error_unknown_keyword() {
639 assert!(parse_type_expr("FOOBAR").is_err());
640 }
641
642 #[test]
643 fn parse_error_empty() {
644 assert!(parse_type_expr("").is_err());
645 assert!(parse_type_expr(" ").is_err());
646 }
647
648 #[test]
649 fn parse_error_vector_zero_dim() {
650 assert!(parse_type_expr("VECTOR(0)").is_err());
651 }
652
653 #[test]
656 fn match_string() {
657 let expr = parse_type_expr("STRING").unwrap();
658 assert!(value_matches_type(&Value::String("hello".into()), &expr));
659 assert!(!value_matches_type(&Value::Integer(1), &expr));
660 }
661
662 #[test]
663 fn match_null_union() {
664 let expr = parse_type_expr("STRING|NULL").unwrap();
665 assert!(value_matches_type(&Value::Null, &expr));
666 assert!(value_matches_type(&Value::String("x".into()), &expr));
667 assert!(!value_matches_type(&Value::Integer(1), &expr));
668 }
669
670 #[test]
671 fn match_typed_array() {
672 let expr = parse_type_expr("ARRAY<INT>").unwrap();
673 assert!(value_matches_type(
674 &Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
675 &expr
676 ));
677 }
678
679 #[test]
680 fn match_typed_array_fail() {
681 let expr = parse_type_expr("ARRAY<INT>").unwrap();
682 assert!(!value_matches_type(
684 &Value::Array(vec![Value::Integer(1), Value::String("x".into())]),
685 &expr
686 ));
687 }
688
689 #[test]
690 fn match_int_coercion() {
691 let expr = parse_type_expr("FLOAT").unwrap();
693 assert!(value_matches_type(&Value::Integer(42), &expr));
694 }
695
696 #[test]
697 fn no_match() {
698 let expr = parse_type_expr("STRING").unwrap();
699 assert!(!value_matches_type(&Value::Integer(99), &expr));
700 }
701
702 #[test]
703 fn match_timestamp_coercions() {
704 let expr = parse_type_expr("TIMESTAMP").unwrap();
705 assert!(value_matches_type(
706 &Value::String("2024-01-01".into()),
707 &expr
708 ));
709 assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
710 }
711
712 #[test]
713 fn parse_timestamptz() {
714 assert_eq!(
715 parse_type_expr("TIMESTAMPTZ").unwrap(),
716 TypeExpr::Simple(SimpleType::Timestamptz)
717 );
718 }
719
720 #[test]
721 fn match_timestamptz_coercions() {
722 let expr = parse_type_expr("TIMESTAMPTZ").unwrap();
723 let dt = nodedb_types::NdbDateTime::from_micros(1_700_000_000_000_000);
724 assert!(value_matches_type(&Value::DateTime(dt), &expr));
725 assert!(value_matches_type(
726 &Value::String("2024-01-01T00:00:00Z".into()),
727 &expr
728 ));
729 assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
730 }
731
732 #[test]
733 fn match_null_expr() {
734 let expr = TypeExpr::Null;
735 assert!(value_matches_type(&Value::Null, &expr));
736 assert!(!value_matches_type(&Value::Integer(0), &expr));
737 }
738
739 #[test]
740 fn match_json_accepts_any() {
741 let expr = TypeExpr::Simple(SimpleType::Json);
742 assert!(value_matches_type(&Value::Null, &expr));
743 assert!(value_matches_type(&Value::Integer(1), &expr));
744 assert!(value_matches_type(&Value::String("x".into()), &expr));
745 assert!(value_matches_type(&Value::Bool(true), &expr));
746 }
747
748 #[test]
749 fn match_vector_type() {
750 let expr = parse_type_expr("VECTOR(128)").unwrap();
751 assert!(value_matches_type(
753 &Value::Array(vec![Value::Float(0.1)]),
754 &expr
755 ));
756 assert!(value_matches_type(&Value::Bytes(vec![0u8; 512]), &expr));
757 assert!(!value_matches_type(&Value::Integer(1), &expr));
758 }
759}