use nodedb_types::Value;
use nodedb_types::columnar::{ColumnType, DecimalTypmod};
use crate::error::SqlError;
#[derive(Debug, Clone, PartialEq)]
pub enum TypeExpr {
Null,
Simple(SimpleType),
TypedArray(Box<TypeExpr>),
TypedSet(Box<TypeExpr>),
Union(Vec<TypeExpr>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum SimpleType {
Int,
Float,
String,
Bool,
Bytes,
Timestamp,
Timestamptz,
SystemTimestamp,
Decimal(Option<DecimalTypmod>),
Uuid,
Ulid,
Geometry,
Duration,
Array,
Object,
Json,
Set,
Regex,
Range,
Record,
Vector(u32),
SparseVector,
}
pub fn parse_type_expr(s: &str) -> Result<TypeExpr, SqlError> {
let s = s.trim();
if s.is_empty() {
return Err(SqlError::Parse {
detail: "empty type expression".to_string(),
});
}
let mut pos = 0usize;
let chars: Vec<char> = s.chars().collect();
let expr = parse_union(&chars, &mut pos, false)?;
skip_ws(&chars, &mut pos);
if pos < chars.len() {
let rest: String = chars[pos..].iter().collect();
return Err(SqlError::Parse {
detail: format!("unexpected trailing input in type expression: '{rest}'"),
});
}
Ok(expr)
}
fn parse_union(chars: &[char], pos: &mut usize, stop_at_gt: bool) -> Result<TypeExpr, SqlError> {
let mut variants: Vec<TypeExpr> = Vec::new();
variants.push(parse_single(chars, pos)?);
loop {
skip_ws(chars, pos);
if *pos >= chars.len() {
break;
}
if stop_at_gt && chars[*pos] == '>' {
break;
}
if chars[*pos] != '|' {
break;
}
*pos += 1; skip_ws(chars, pos);
variants.push(parse_single(chars, pos)?);
}
if variants.len() == 1 {
Ok(variants.remove(0))
} else {
Ok(TypeExpr::Union(variants))
}
}
fn parse_single(chars: &[char], pos: &mut usize) -> Result<TypeExpr, SqlError> {
skip_ws(chars, pos);
let keyword = read_keyword(chars, pos);
if keyword.is_empty() {
return Err(SqlError::Parse {
detail: format!(
"expected type keyword at position {pos}, found: {:?}",
chars.get(*pos)
),
});
}
match keyword.as_str() {
"NULL" => Ok(TypeExpr::Null),
"OBJECT" => Ok(TypeExpr::Simple(SimpleType::Object)),
"ARRAY" | "SET" => {
skip_ws(chars, pos);
if *pos < chars.len() && chars[*pos] == '<' {
*pos += 1; skip_ws(chars, pos);
let inner = Box::new(parse_union(chars, pos, true)?);
skip_ws(chars, pos);
if *pos >= chars.len() || chars[*pos] != '>' {
return Err(SqlError::Parse {
detail: format!("expected '>' to close {keyword}<...> at position {pos}"),
});
}
*pos += 1; return Ok(if keyword == "ARRAY" {
TypeExpr::TypedArray(inner)
} else {
TypeExpr::TypedSet(inner)
});
}
parse_leaf(chars, pos, &keyword)
}
_ => parse_leaf(chars, pos, &keyword),
}
}
fn parse_leaf(chars: &[char], pos: &mut usize, keyword: &str) -> Result<TypeExpr, SqlError> {
let spelling = read_spelling(chars, pos, keyword)?;
let column_type: ColumnType = spelling.parse().map_err(|e| SqlError::Parse {
detail: format!("type '{spelling}': {e}"),
})?;
Ok(TypeExpr::Simple(simple_from_column_type(
column_type,
&spelling,
)?))
}
fn read_spelling(chars: &[char], pos: &mut usize, keyword: &str) -> Result<String, SqlError> {
let mut spelling = keyword.to_string();
loop {
if *pos < chars.len() && chars[*pos] == '(' {
spelling.push_str(&read_paren_group(chars, pos)?);
continue;
}
let mut probe = *pos;
skip_ws(chars, &mut probe);
if probe == *pos {
break;
}
let word = read_keyword(chars, &mut probe);
if word.is_empty() {
break;
}
spelling.push(' ');
spelling.push_str(&word);
*pos = probe;
}
Ok(spelling)
}
fn read_paren_group(chars: &[char], pos: &mut usize) -> Result<String, SqlError> {
let start = *pos;
let mut depth = 0usize;
let mut group = String::new();
while *pos < chars.len() {
let ch = chars[*pos];
group.push(ch);
*pos += 1;
match ch {
'(' => depth += 1,
')' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Ok(group);
}
}
_ => {}
}
}
Err(SqlError::Parse {
detail: format!("unclosed '(' at position {start}"),
})
}
fn simple_from_column_type(
column_type: ColumnType,
spelling: &str,
) -> Result<SimpleType, SqlError> {
Ok(match column_type {
ColumnType::Int64 => SimpleType::Int,
ColumnType::Float64 => SimpleType::Float,
ColumnType::String => SimpleType::String,
ColumnType::Bool => SimpleType::Bool,
ColumnType::Bytes => SimpleType::Bytes,
ColumnType::Timestamp => SimpleType::Timestamp,
ColumnType::Timestamptz => SimpleType::Timestamptz,
ColumnType::SystemTimestamp => SimpleType::SystemTimestamp,
ColumnType::Decimal(typmod) => SimpleType::Decimal(typmod),
ColumnType::Geometry => SimpleType::Geometry,
ColumnType::Vector(dim) => SimpleType::Vector(dim),
ColumnType::SparseVector => SimpleType::SparseVector,
ColumnType::Uuid => SimpleType::Uuid,
ColumnType::Json => SimpleType::Json,
ColumnType::Ulid => SimpleType::Ulid,
ColumnType::Duration => SimpleType::Duration,
ColumnType::Array => SimpleType::Array,
ColumnType::Set => SimpleType::Set,
ColumnType::Regex => SimpleType::Regex,
ColumnType::Range => SimpleType::Range,
ColumnType::Record => SimpleType::Record,
other => {
return Err(SqlError::Parse {
detail: format!(
"type '{spelling}' resolves to {other}, which no typeguard validates"
),
});
}
})
}
fn skip_ws(chars: &[char], pos: &mut usize) {
while *pos < chars.len() && chars[*pos].is_ascii_whitespace() {
*pos += 1;
}
}
fn read_keyword(chars: &[char], pos: &mut usize) -> String {
let mut s = String::new();
while *pos < chars.len() {
let c = chars[*pos];
if c.is_ascii_alphanumeric() || c == '_' {
s.push(c.to_ascii_uppercase());
*pos += 1;
} else {
break;
}
}
s
}
pub fn value_matches_type(value: &Value, expr: &TypeExpr) -> bool {
match expr {
TypeExpr::Null => matches!(value, Value::Null),
TypeExpr::Simple(simple) => value_matches_simple(value, simple),
TypeExpr::TypedArray(inner) => match value {
Value::Array(items) => items.iter().all(|item| value_matches_type(item, inner)),
_ => false,
},
TypeExpr::TypedSet(inner) => match value {
Value::Set(items) => items.iter().all(|item| value_matches_type(item, inner)),
_ => false,
},
TypeExpr::Union(variants) => variants.iter().any(|v| value_matches_type(value, v)),
}
}
fn value_matches_simple(value: &Value, simple: &SimpleType) -> bool {
match simple {
SimpleType::Int => matches!(value, Value::Integer(_)),
SimpleType::Float => matches!(value, Value::Float(_) | Value::Integer(_)),
SimpleType::String => matches!(value, Value::String(_)),
SimpleType::Bool => matches!(value, Value::Bool(_)),
SimpleType::Bytes => matches!(value, Value::Bytes(_)),
SimpleType::Timestamp => matches!(
value,
Value::NaiveDateTime(_) | Value::Integer(_) | Value::String(_)
),
SimpleType::Timestamptz => matches!(
value,
Value::DateTime(_) | Value::Integer(_) | Value::String(_)
),
SimpleType::SystemTimestamp => matches!(value, Value::DateTime(_) | Value::Integer(_)),
SimpleType::Decimal(_) => matches!(
value,
Value::Decimal(_) | Value::Float(_) | Value::Integer(_) | Value::String(_)
),
SimpleType::Uuid => matches!(value, Value::Uuid(_) | Value::String(_)),
SimpleType::Ulid => matches!(value, Value::Ulid(_) | Value::String(_)),
SimpleType::Geometry => matches!(value, Value::Geometry(_) | Value::String(_)),
SimpleType::Duration => matches!(value, Value::Duration(_)),
SimpleType::Array => matches!(value, Value::Array(_)),
SimpleType::Object => matches!(value, Value::Object(_)),
SimpleType::Json => true, SimpleType::Set => matches!(value, Value::Set(_)),
SimpleType::Regex => matches!(value, Value::Regex(_)),
SimpleType::Range => matches!(value, Value::Range { .. }),
SimpleType::Record => matches!(value, Value::Record { .. }),
SimpleType::Vector(_) => matches!(value, Value::Array(_) | Value::Bytes(_)),
SimpleType::SparseVector => matches!(value, Value::String(_) | Value::Bytes(_)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple() {
assert_eq!(
parse_type_expr("STRING").unwrap(),
TypeExpr::Simple(SimpleType::String)
);
}
#[test]
fn parse_union() {
assert_eq!(
parse_type_expr("STRING|NULL").unwrap(),
TypeExpr::Union(vec![TypeExpr::Simple(SimpleType::String), TypeExpr::Null])
);
}
#[test]
fn parse_typed_array() {
assert_eq!(
parse_type_expr("ARRAY<STRING>").unwrap(),
TypeExpr::TypedArray(Box::new(TypeExpr::Simple(SimpleType::String)))
);
}
#[test]
fn parse_typed_array_union() {
assert_eq!(
parse_type_expr("ARRAY<INT|FLOAT>").unwrap(),
TypeExpr::TypedArray(Box::new(TypeExpr::Union(vec![
TypeExpr::Simple(SimpleType::Int),
TypeExpr::Simple(SimpleType::Float),
])))
);
}
#[test]
fn parse_vector() {
assert_eq!(
parse_type_expr("VECTOR(384)").unwrap(),
TypeExpr::Simple(SimpleType::Vector(384))
);
}
#[test]
fn parse_sparsevector() {
assert_eq!(
parse_type_expr("SPARSEVECTOR").unwrap(),
TypeExpr::Simple(SimpleType::SparseVector)
);
assert_eq!(
parse_type_expr("sparsevector").unwrap(),
TypeExpr::Simple(SimpleType::SparseVector)
);
}
#[test]
fn match_sparsevector_accepts_string_literal() {
let expr = parse_type_expr("SPARSEVECTOR").unwrap();
assert!(value_matches_type(&Value::String("{3:0.5}".into()), &expr));
assert!(value_matches_type(&Value::Bytes(vec![1, 2, 3]), &expr));
assert!(!value_matches_type(&Value::Integer(1), &expr));
}
#[test]
fn parse_case_insensitive() {
assert_eq!(
parse_type_expr("string|null").unwrap(),
TypeExpr::Union(vec![TypeExpr::Simple(SimpleType::String), TypeExpr::Null])
);
}
#[test]
fn parse_aliases() {
assert_eq!(
parse_type_expr("INTEGER").unwrap(),
TypeExpr::Simple(SimpleType::Int)
);
assert_eq!(
parse_type_expr("BIGINT").unwrap(),
TypeExpr::Simple(SimpleType::Int)
);
assert_eq!(
parse_type_expr("INT64").unwrap(),
TypeExpr::Simple(SimpleType::Int)
);
assert_eq!(
parse_type_expr("TEXT").unwrap(),
TypeExpr::Simple(SimpleType::String)
);
assert_eq!(
parse_type_expr("BOOLEAN").unwrap(),
TypeExpr::Simple(SimpleType::Bool)
);
assert_eq!(
parse_type_expr("BYTEA").unwrap(),
TypeExpr::Simple(SimpleType::Bytes)
);
}
#[test]
fn parse_typed_set() {
assert_eq!(
parse_type_expr("SET<INT>").unwrap(),
TypeExpr::TypedSet(Box::new(TypeExpr::Simple(SimpleType::Int)))
);
}
#[test]
fn parse_untyped_array() {
assert_eq!(
parse_type_expr("ARRAY").unwrap(),
TypeExpr::Simple(SimpleType::Array)
);
}
#[test]
fn parse_untyped_set() {
assert_eq!(
parse_type_expr("SET").unwrap(),
TypeExpr::Simple(SimpleType::Set)
);
}
#[test]
fn parse_timestamp_zone_spellings() {
assert_eq!(
parse_type_expr("TIMESTAMP WITH TIME ZONE").unwrap(),
TypeExpr::Simple(SimpleType::Timestamptz)
);
assert_eq!(
parse_type_expr("timestamp without time zone").unwrap(),
TypeExpr::Simple(SimpleType::Timestamp)
);
}
#[test]
fn parse_error_trailing_words() {
assert!(parse_type_expr("TIMESTAMP GARBAGE WORDS").is_err());
assert!(parse_type_expr("STRING FOO").is_err());
assert!(parse_type_expr("TIMESTAMP WITH").is_err());
assert!(parse_type_expr("INT )").is_err());
assert!(parse_type_expr("VECTOR(3) EXTRA").is_err());
}
#[test]
fn parse_every_declared_numeric_keyword() {
use nodedb_types::columnar::{DECLARED_FLOAT_KEYWORDS, DECLARED_INT_KEYWORDS};
for keyword in DECLARED_INT_KEYWORDS {
assert_eq!(
parse_type_expr(keyword).unwrap(),
TypeExpr::Simple(SimpleType::Int),
"{keyword} must parse as an integer leaf"
);
}
for keyword in DECLARED_FLOAT_KEYWORDS {
assert_eq!(
parse_type_expr(keyword).unwrap(),
TypeExpr::Simple(SimpleType::Float),
"{keyword} must parse as a float leaf"
);
}
}
#[test]
fn parse_jsonb() {
assert_eq!(
parse_type_expr("JSONB").unwrap(),
TypeExpr::Simple(SimpleType::Json)
);
}
#[test]
fn parse_decimal_carries_declared_params() {
assert_eq!(
parse_type_expr("DECIMAL(10, 2)").unwrap(),
TypeExpr::Simple(SimpleType::Decimal(Some(
DecimalTypmod::new(10, 2).expect("valid typmod")
)))
);
assert_eq!(
parse_type_expr("NUMERIC").unwrap(),
TypeExpr::Simple(SimpleType::Decimal(None))
);
assert!(parse_type_expr("DECIMAL(1001,0)").is_err());
assert!(parse_type_expr("DECIMAL(0)").is_err());
}
#[test]
fn parse_varchar_length() {
let expr = parse_type_expr("VARCHAR(255)").unwrap();
assert_eq!(expr, TypeExpr::Simple(SimpleType::String));
assert!(value_matches_type(&Value::String("x".into()), &expr));
assert!(parse_type_expr("VARCHAR(0)").is_err());
}
#[test]
fn parse_and_match_system_timestamp() {
let expr = parse_type_expr("SYSTEM_TIMESTAMP").unwrap();
assert_eq!(expr, TypeExpr::Simple(SimpleType::SystemTimestamp));
let dt = nodedb_types::NdbDateTime::from_micros(1_700_000_000_000_000);
assert!(value_matches_type(&Value::DateTime(dt), &expr));
assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
assert!(!value_matches_type(
&Value::String("2024-01-01".into()),
&expr
));
}
#[test]
fn parse_error_unknown_keyword() {
assert!(parse_type_expr("FOOBAR").is_err());
}
#[test]
fn parse_error_empty() {
assert!(parse_type_expr("").is_err());
assert!(parse_type_expr(" ").is_err());
}
#[test]
fn parse_error_vector_zero_dim() {
assert!(parse_type_expr("VECTOR(0)").is_err());
}
#[test]
fn match_string() {
let expr = parse_type_expr("STRING").unwrap();
assert!(value_matches_type(&Value::String("hello".into()), &expr));
assert!(!value_matches_type(&Value::Integer(1), &expr));
}
#[test]
fn match_null_union() {
let expr = parse_type_expr("STRING|NULL").unwrap();
assert!(value_matches_type(&Value::Null, &expr));
assert!(value_matches_type(&Value::String("x".into()), &expr));
assert!(!value_matches_type(&Value::Integer(1), &expr));
}
#[test]
fn match_typed_array() {
let expr = parse_type_expr("ARRAY<INT>").unwrap();
assert!(value_matches_type(
&Value::Array(vec![Value::Integer(1), Value::Integer(2)]),
&expr
));
}
#[test]
fn match_typed_array_fail() {
let expr = parse_type_expr("ARRAY<INT>").unwrap();
assert!(!value_matches_type(
&Value::Array(vec![Value::Integer(1), Value::String("x".into())]),
&expr
));
}
#[test]
fn match_int_coercion() {
let expr = parse_type_expr("FLOAT").unwrap();
assert!(value_matches_type(&Value::Integer(42), &expr));
}
#[test]
fn no_match() {
let expr = parse_type_expr("STRING").unwrap();
assert!(!value_matches_type(&Value::Integer(99), &expr));
}
#[test]
fn match_timestamp_coercions() {
let expr = parse_type_expr("TIMESTAMP").unwrap();
assert!(value_matches_type(
&Value::String("2024-01-01".into()),
&expr
));
assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
}
#[test]
fn parse_timestamptz() {
assert_eq!(
parse_type_expr("TIMESTAMPTZ").unwrap(),
TypeExpr::Simple(SimpleType::Timestamptz)
);
}
#[test]
fn match_timestamptz_coercions() {
let expr = parse_type_expr("TIMESTAMPTZ").unwrap();
let dt = nodedb_types::NdbDateTime::from_micros(1_700_000_000_000_000);
assert!(value_matches_type(&Value::DateTime(dt), &expr));
assert!(value_matches_type(
&Value::String("2024-01-01T00:00:00Z".into()),
&expr
));
assert!(value_matches_type(&Value::Integer(1_700_000_000), &expr));
}
#[test]
fn match_null_expr() {
let expr = TypeExpr::Null;
assert!(value_matches_type(&Value::Null, &expr));
assert!(!value_matches_type(&Value::Integer(0), &expr));
}
#[test]
fn match_json_accepts_any() {
let expr = TypeExpr::Simple(SimpleType::Json);
assert!(value_matches_type(&Value::Null, &expr));
assert!(value_matches_type(&Value::Integer(1), &expr));
assert!(value_matches_type(&Value::String("x".into()), &expr));
assert!(value_matches_type(&Value::Bool(true), &expr));
}
#[test]
fn match_vector_type() {
let expr = parse_type_expr("VECTOR(128)").unwrap();
assert!(value_matches_type(
&Value::Array(vec![Value::Float(0.1)]),
&expr
));
assert!(value_matches_type(&Value::Bytes(vec![0u8; 512]), &expr));
assert!(!value_matches_type(&Value::Integer(1), &expr));
}
}