#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ChType {
Named(String),
Nullable(Box<ChType>),
LowCardinality(Box<ChType>),
Array(Box<ChType>),
Map(Box<ChType>, Box<ChType>),
Tuple(Vec<ChType>),
FixedString(u32),
DateTime {
tz: Option<String>,
},
DateTime64 {
precision: u8,
tz: Option<String>,
},
Time64 {
precision: u8,
},
Decimal {
precision: u8,
scale: u8,
},
Enum8,
Enum16,
Other(String),
}
pub(crate) fn parse(s: &str) -> ChType {
let s = s.trim();
try_parse(s).unwrap_or_else(|| ChType::Other(s.to_string()))
}
fn try_parse(s: &str) -> Option<ChType> {
let (name, args) = match s.find('(') {
None => (s.trim(), None),
Some(i) if s.ends_with(')') => (s[..i].trim(), Some(&s[i + 1..s.len() - 1])),
Some(_) => return None,
};
if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return None;
}
let args = match args {
None => Vec::new(),
Some(a) => split_top_level(a)?,
};
Some(match (name, args.as_slice()) {
("Nullable", [t]) => ChType::Nullable(Box::new(parse(t))),
("LowCardinality", [t]) => ChType::LowCardinality(Box::new(parse(t))),
("Array", [t]) => ChType::Array(Box::new(parse(t))),
("Map", [k, v]) => ChType::Map(Box::new(parse(k)), Box::new(parse(v))),
("Tuple", elems) if !elems.is_empty() => {
ChType::Tuple(elems.iter().map(|e| parse_tuple_element(e)).collect())
}
("FixedString", [n]) => ChType::FixedString(n.parse().ok()?),
("DateTime", []) => ChType::DateTime { tz: None },
("DateTime", [tz]) => ChType::DateTime {
tz: Some(unquote(tz)?),
},
("DateTime64", [p]) => ChType::DateTime64 {
precision: p.parse().ok()?,
tz: None,
},
("DateTime64", [p, tz]) => ChType::DateTime64 {
precision: p.parse().ok()?,
tz: Some(unquote(tz)?),
},
("Time64", [p]) => ChType::Time64 {
precision: p.parse().ok()?,
},
("Decimal", [p, s]) => ChType::Decimal {
precision: p.parse().ok()?,
scale: s.parse().ok()?,
},
("Decimal32", [s]) => ChType::Decimal {
precision: 9,
scale: s.parse().ok()?,
},
("Decimal64", [s]) => ChType::Decimal {
precision: 18,
scale: s.parse().ok()?,
},
("Decimal128", [s]) => ChType::Decimal {
precision: 38,
scale: s.parse().ok()?,
},
("Decimal256", [s]) => ChType::Decimal {
precision: 76,
scale: s.parse().ok()?,
},
("Enum8", _) => ChType::Enum8,
("Enum16", _) => ChType::Enum16,
(_, []) => ChType::Named(name.to_string()),
_ => return None,
})
}
fn parse_tuple_element(e: &str) -> ChType {
let e = e.trim();
match top_level_space(e) {
Some(i) => parse(&e[i + 1..]),
None => parse(e),
}
}
fn top_level_space(s: &str) -> Option<usize> {
let mut depth = 0u32;
let mut in_quote = false;
let mut escape = false;
for (i, c) in s.char_indices() {
if escape {
escape = false;
continue;
}
match c {
'\\' if in_quote => escape = true,
'\'' => in_quote = !in_quote,
'(' if !in_quote => depth += 1,
')' if !in_quote => depth = depth.checked_sub(1)?,
' ' if !in_quote && depth == 0 => return Some(i),
_ => {}
}
}
None
}
fn split_top_level(s: &str) -> Option<Vec<&str>> {
let mut parts = Vec::new();
let mut depth = 0u32;
let mut in_quote = false;
let mut escape = false;
let mut start = 0;
for (i, c) in s.char_indices() {
if escape {
escape = false;
continue;
}
match c {
'\\' if in_quote => escape = true,
'\'' => in_quote = !in_quote,
'(' if !in_quote => depth += 1,
')' if !in_quote => depth = depth.checked_sub(1)?,
',' if !in_quote && depth == 0 => {
parts.push(s[start..i].trim());
start = i + 1;
}
_ => {}
}
}
if depth != 0 || in_quote {
return None;
}
let last = s[start..].trim();
if !last.is_empty() || !parts.is_empty() {
parts.push(last);
}
Some(parts)
}
fn unquote(s: &str) -> Option<String> {
let s = s.trim();
let inner = s.strip_prefix('\'')?.strip_suffix('\'')?;
Some(inner.replace("\\'", "'").replace("\\\\", "\\"))
}
#[cfg(test)]
mod tests {
use super::*;
fn named(s: &str) -> ChType {
ChType::Named(s.to_string())
}
#[test]
fn parses_plain_and_nested_types() {
assert_eq!(parse("UInt32"), named("UInt32"));
assert_eq!(parse(" String "), named("String"));
assert_eq!(
parse("Nullable(LowCardinality(String))"),
ChType::Nullable(Box::new(ChType::LowCardinality(Box::new(named("String")))))
);
assert_eq!(
parse("Array(Tuple(UInt32, String))"),
ChType::Array(Box::new(ChType::Tuple(vec![
named("UInt32"),
named("String")
])))
);
assert_eq!(
parse("Map(String, Array(Nullable(Int64)))"),
ChType::Map(
Box::new(named("String")),
Box::new(ChType::Array(Box::new(ChType::Nullable(Box::new(named(
"Int64"
))))))
)
);
}
#[test]
fn parses_named_tuples_dropping_element_names() {
assert_eq!(
parse("Tuple(id UInt64, name String)"),
ChType::Tuple(vec![named("UInt64"), named("String")])
);
assert_eq!(
parse("Tuple(Decimal(18, 4), price Decimal(9, 2))"),
ChType::Tuple(vec![
ChType::Decimal {
precision: 18,
scale: 4
},
ChType::Decimal {
precision: 9,
scale: 2
},
])
);
}
#[test]
fn parses_datetime_and_decimal_parameters() {
assert_eq!(parse("DateTime"), ChType::DateTime { tz: None });
assert_eq!(
parse("DateTime('Europe/London')"),
ChType::DateTime {
tz: Some("Europe/London".into())
}
);
assert_eq!(
parse("DateTime64(3, 'Europe/London')"),
ChType::DateTime64 {
precision: 3,
tz: Some("Europe/London".into())
}
);
assert_eq!(
parse("DateTime64(6)"),
ChType::DateTime64 {
precision: 6,
tz: None
}
);
assert_eq!(parse("Time64(9)"), ChType::Time64 { precision: 9 });
assert_eq!(
parse("Decimal(18, 4)"),
ChType::Decimal {
precision: 18,
scale: 4
}
);
assert_eq!(
parse("Decimal64(4)"),
ChType::Decimal {
precision: 18,
scale: 4
}
);
assert_eq!(parse("FixedString(16)"), ChType::FixedString(16));
}
#[test]
fn enum_value_lists_survive_hostile_quoting() {
assert_eq!(parse("Enum8('a' = 1, 'b' = 2)"), ChType::Enum8);
assert_eq!(parse("Enum8('a,b)' = 1, 'c\\'d' = 2)"), ChType::Enum8);
assert_eq!(parse("Enum16('x' = 300)"), ChType::Enum16);
}
#[test]
fn unknown_types_collapse_to_other() {
for s in [
"AggregateFunction(sum, UInt64)",
"SimpleAggregateFunction(max, DateTime)",
"Variant(Int64, String)",
"Nested(a UInt32, b String)",
"Dynamic",
"SomeFutureType(1, 2)",
] {
match parse(s) {
ChType::Other(o) => assert_eq!(o, s),
ChType::Named(n) => assert_eq!(n, s, "parameterless unknowns stay Named"),
other => panic!("{s} parsed to {other:?}"),
}
}
assert!(matches!(parse("Array(UInt32"), ChType::Other(_)));
assert!(matches!(parse("Tuple)("), ChType::Other(_)));
assert!(matches!(parse(""), ChType::Other(_)));
}
}