use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TypeTag {
String,
FixedString,
UInt8,
UInt16,
UInt32,
UInt64,
UInt128,
UInt256,
Int8,
Int16,
Int32,
Int64,
Int128,
Int256,
Float32,
Float64,
Bool,
Date,
Date32,
DateTime,
DateTime64,
Decimal32,
Decimal64,
Decimal128,
Decimal256,
UUID,
IPv4,
IPv6,
Enum8,
Enum16,
Array,
Map,
Point,
JSON,
Variant,
Dynamic,
Unknown,
}
impl TypeTag {
#[must_use]
pub fn from_base(base: &str) -> Self {
match base {
"String" => Self::String,
"FixedString" => Self::FixedString,
"UInt8" => Self::UInt8,
"UInt16" => Self::UInt16,
"UInt32" => Self::UInt32,
"UInt64" => Self::UInt64,
"UInt128" => Self::UInt128,
"UInt256" => Self::UInt256,
"Int8" => Self::Int8,
"Int16" => Self::Int16,
"Int32" => Self::Int32,
"Int64" => Self::Int64,
"Int128" => Self::Int128,
"Int256" => Self::Int256,
"Float32" => Self::Float32,
"Float64" => Self::Float64,
"Bool" => Self::Bool,
"Date" => Self::Date,
"Date32" => Self::Date32,
"DateTime" => Self::DateTime,
"DateTime64" => Self::DateTime64,
"Decimal32" => Self::Decimal32,
"Decimal64" => Self::Decimal64,
"Decimal128" => Self::Decimal128,
"Decimal256" => Self::Decimal256,
"UUID" => Self::UUID,
"IPv4" => Self::IPv4,
"IPv6" => Self::IPv6,
"Enum8" => Self::Enum8,
"Enum16" => Self::Enum16,
"Array" => Self::Array,
"Map" => Self::Map,
"Point" => Self::Point,
"JSON" | "Object" => Self::JSON,
"Variant" => Self::Variant,
"Dynamic" => Self::Dynamic,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParsedType {
pub raw: String,
pub base: String,
pub tag: TypeTag,
pub nullable: bool,
pub low_cardinality: bool,
pub array_element: Option<Box<ParsedType>>,
pub map_types: Option<(Box<ParsedType>, Box<ParsedType>)>,
pub precision: Option<u8>,
pub scale: Option<u8>,
pub timezone: Option<String>,
pub fixed_size: Option<usize>,
}
impl ParsedType {
#[must_use]
pub fn parse(type_str: &str) -> Self {
let type_str = type_str.trim();
Self::parse_inner(type_str, type_str.to_string())
}
fn parse_inner(type_str: &str, raw: String) -> Self {
let mut result = Self {
raw,
base: String::new(),
tag: TypeTag::Unknown,
nullable: false,
low_cardinality: false,
array_element: None,
map_types: None,
precision: None,
scale: None,
timezone: None,
fixed_size: None,
};
let mut type_str = type_str.trim().to_string();
loop {
let (unwrapped, is_nullable) = Self::unwrap_wrapper(&type_str, "Nullable");
if is_nullable {
result.nullable = true;
type_str = unwrapped;
continue;
}
let (unwrapped, is_lc) = Self::unwrap_wrapper(&type_str, "LowCardinality");
if is_lc {
result.low_cardinality = true;
type_str = unwrapped;
continue;
}
break;
}
if let Some(inner) = Self::extract_wrapper(&type_str, "Array") {
result.base = "Array".to_string();
result.tag = TypeTag::Array;
result.array_element = Some(Box::new(Self::parse(&inner)));
return result;
}
if let Some(inner) = Self::extract_wrapper(&type_str, "Map")
&& let Some((key, value)) = Self::split_type_args(&inner)
{
result.base = "Map".to_string();
result.tag = TypeTag::Map;
result.map_types = Some((Box::new(Self::parse(&key)), Box::new(Self::parse(&value))));
return result;
}
if type_str.starts_with("DateTime64") {
result.base = "DateTime64".to_string();
result.tag = TypeTag::DateTime64;
if let Some(inner) = Self::extract_wrapper(&type_str, "DateTime64") {
let parts: Vec<&str> = inner.splitn(2, ',').collect();
result.precision = parts.first().and_then(|p| p.trim().parse().ok());
result.timezone = parts
.get(1)
.map(|tz| tz.trim().trim_matches('\'').trim_matches('"').to_string());
}
return result;
}
if let Some(inner) = Self::extract_wrapper(&type_str, "FixedString") {
result.base = "FixedString".to_string();
result.tag = TypeTag::FixedString;
result.fixed_size = inner.trim().parse().ok();
return result;
}
if type_str.starts_with("Decimal") {
result.base = Self::parse_decimal_base(&type_str);
result.tag = TypeTag::from_base(&result.base);
if let Some(inner) = Self::extract_parens(&type_str) {
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() == 2 {
result.precision = parts[0].trim().parse().ok();
result.scale = parts[1].trim().parse().ok();
} else if parts.len() == 1 {
result.scale = parts[0].trim().parse().ok();
}
}
return result;
}
if type_str.starts_with("Enum8") || type_str.starts_with("Enum16") {
if type_str.starts_with("Enum8") {
result.base = "Enum8".to_string();
result.tag = TypeTag::Enum8;
} else {
result.base = "Enum16".to_string();
result.tag = TypeTag::Enum16;
}
return result;
}
if type_str.starts_with("Variant(") {
result.base = "Variant".to_string();
result.tag = TypeTag::Variant;
return result;
}
if type_str == "Dynamic" || type_str.starts_with("Dynamic(") {
result.base = "Dynamic".to_string();
result.tag = TypeTag::Dynamic;
return result;
}
result.base = type_str;
result.tag = TypeTag::from_base(&result.base);
result
}
fn unwrap_wrapper(type_str: &str, wrapper: &str) -> (String, bool) {
match Self::extract_wrapper(type_str, wrapper) {
Some(inner) => (inner, true),
None => (type_str.to_string(), false),
}
}
fn extract_wrapper(type_str: &str, wrapper: &str) -> Option<String> {
type_str
.strip_prefix(wrapper)?
.strip_prefix('(')?
.strip_suffix(')')
.map(std::string::ToString::to_string)
}
fn extract_parens(type_str: &str) -> Option<String> {
let start = type_str.find('(')?;
let end = type_str.rfind(')')?;
if start < end {
Some(type_str[start + 1..end].to_string())
} else {
None
}
}
fn parse_decimal_base(type_str: &str) -> String {
if type_str.starts_with("Decimal256") {
"Decimal256".to_string()
} else if type_str.starts_with("Decimal128") {
"Decimal128".to_string()
} else if type_str.starts_with("Decimal64") {
"Decimal64".to_string()
} else if type_str.starts_with("Decimal32") {
"Decimal32".to_string()
} else {
"Decimal".to_string()
}
}
fn split_type_args(inner: &str) -> Option<(String, String)> {
let mut depth = 0;
for (i, c) in inner.char_indices() {
match c {
'(' => depth += 1,
')' => depth -= 1,
',' if depth == 0 => {
return Some((
inner[..i].trim().to_string(),
inner[i + 1..].trim().to_string(),
));
}
_ => {}
}
}
None
}
#[must_use]
pub fn category(&self) -> &str {
match self.base.as_str() {
"String" | "FixedString" => "String",
"Int8" | "Int16" | "Int32" | "Int64" | "Int128" | "Int256" => "Int",
"UInt8" | "UInt16" | "UInt32" | "UInt64" | "UInt128" | "UInt256" => "UInt",
"Float32" | "Float64" => "Float",
"Decimal" | "Decimal32" | "Decimal64" | "Decimal128" | "Decimal256" => "Decimal",
"Bool" => "Bool",
"Date" | "Date32" => "Date",
"DateTime" => "DateTime",
"DateTime64" => "DateTime64",
"UUID" => "UUID",
"IPv4" => "IPv4",
"IPv6" => "IPv6",
"Array" => "Array",
"Map" => "Map",
"JSON" | "Object" => "JSON",
"Variant" => "Variant",
"Dynamic" => "Dynamic",
"Enum8" | "Enum16" => "Enum",
"Point" | "Ring" | "Polygon" | "MultiPolygon" | "LineString" | "MultiLineString" => {
"Geo"
}
_ => "Unknown",
}
}
#[must_use]
pub fn is_numeric(&self) -> bool {
matches!(self.category(), "Int" | "UInt" | "Float" | "Decimal")
}
#[must_use]
pub fn is_string(&self) -> bool {
self.category() == "String"
}
#[must_use]
pub fn is_datetime(&self) -> bool {
matches!(self.category(), "Date" | "DateTime" | "DateTime64")
}
#[must_use]
pub fn is_ip(&self) -> bool {
matches!(self.category(), "IPv4" | "IPv6")
}
#[must_use]
pub fn fixed_byte_size(&self) -> Option<usize> {
match self.base.as_str() {
"UInt8" | "Int8" | "Bool" | "Enum8" => Some(1),
"UInt16" | "Int16" | "Enum16" | "Date" => Some(2),
"UInt32" | "Int32" | "Date32" | "DateTime" | "Float32" | "Decimal32" | "IPv4" => {
Some(4)
}
"UInt64" | "Int64" | "Float64" | "Decimal64" | "DateTime64" => Some(8),
"Int128" | "UInt128" | "Decimal128" | "UUID" | "IPv6" => Some(16),
"Int256" | "UInt256" | "Decimal256" => Some(32),
"FixedString" => self.fixed_size,
_ => None,
}
}
}
impl fmt::Display for ParsedType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_types() {
let t = ParsedType::parse("String");
assert_eq!(t.base, "String");
assert!(!t.nullable);
assert!(!t.low_cardinality);
let t = ParsedType::parse("Int64");
assert_eq!(t.base, "Int64");
assert_eq!(t.category(), "Int");
let t = ParsedType::parse("Float64");
assert_eq!(t.base, "Float64");
assert_eq!(t.category(), "Float");
}
#[test]
fn test_parse_nullable() {
let t = ParsedType::parse("Nullable(String)");
assert_eq!(t.base, "String");
assert!(t.nullable);
assert!(!t.low_cardinality);
}
#[test]
fn test_parse_low_cardinality() {
let t = ParsedType::parse("LowCardinality(String)");
assert_eq!(t.base, "String");
assert!(!t.nullable);
assert!(t.low_cardinality);
}
#[test]
fn test_parse_nullable_low_cardinality() {
let t = ParsedType::parse("LowCardinality(Nullable(String))");
assert_eq!(t.base, "String");
assert!(t.nullable);
assert!(t.low_cardinality);
}
#[test]
fn test_parse_array() {
let t = ParsedType::parse("Array(Int64)");
assert_eq!(t.base, "Array");
assert!(t.array_element.is_some());
let elem = t.array_element.as_ref().unwrap();
assert_eq!(elem.base, "Int64");
}
#[test]
fn test_parse_map() {
let t = ParsedType::parse("Map(String, Int64)");
assert_eq!(t.base, "Map");
assert!(t.map_types.is_some());
let (key, value) = t.map_types.as_ref().unwrap();
assert_eq!(key.base, "String");
assert_eq!(value.base, "Int64");
}
#[test]
fn test_parse_datetime64() {
let t = ParsedType::parse("DateTime64(3)");
assert_eq!(t.base, "DateTime64");
assert_eq!(t.precision, Some(3));
assert!(t.timezone.is_none());
let t = ParsedType::parse("DateTime64(6, 'UTC')");
assert_eq!(t.base, "DateTime64");
assert_eq!(t.precision, Some(6));
assert_eq!(t.timezone, Some("UTC".to_string()));
}
#[test]
fn test_parse_fixed_string() {
let t = ParsedType::parse("FixedString(32)");
assert_eq!(t.base, "FixedString");
assert_eq!(t.fixed_size, Some(32));
}
#[test]
fn test_parse_decimal() {
let t = ParsedType::parse("Decimal(18, 6)");
assert_eq!(t.base, "Decimal");
assert_eq!(t.precision, Some(18));
assert_eq!(t.scale, Some(6));
let t = ParsedType::parse("Decimal64(4)");
assert_eq!(t.base, "Decimal64");
assert_eq!(t.scale, Some(4));
}
#[test]
fn test_parse_enum() {
let t = ParsedType::parse("Enum8('a' = 1, 'b' = 2)");
assert_eq!(t.base, "Enum8");
let t = ParsedType::parse("Enum16('x' = 100)");
assert_eq!(t.base, "Enum16");
}
#[test]
fn test_categories() {
assert_eq!(ParsedType::parse("String").category(), "String");
assert_eq!(ParsedType::parse("Int64").category(), "Int");
assert_eq!(ParsedType::parse("UInt32").category(), "UInt");
assert_eq!(ParsedType::parse("Float64").category(), "Float");
assert_eq!(ParsedType::parse("Bool").category(), "Bool");
assert_eq!(ParsedType::parse("DateTime").category(), "DateTime");
assert_eq!(ParsedType::parse("UUID").category(), "UUID");
assert_eq!(ParsedType::parse("IPv4").category(), "IPv4");
assert_eq!(ParsedType::parse("JSON").category(), "JSON");
assert_eq!(ParsedType::parse("SomeNewType").category(), "Unknown");
assert!(!ParsedType::parse("SomeNewType").is_string());
}
#[test]
fn test_is_helpers() {
assert!(ParsedType::parse("Int64").is_numeric());
assert!(ParsedType::parse("Float64").is_numeric());
assert!(!ParsedType::parse("String").is_numeric());
assert!(ParsedType::parse("String").is_string());
assert!(ParsedType::parse("FixedString(10)").is_string());
assert!(ParsedType::parse("DateTime").is_datetime());
assert!(ParsedType::parse("DateTime64(3)").is_datetime());
assert!(ParsedType::parse("Date").is_datetime());
assert!(ParsedType::parse("IPv4").is_ip());
assert!(ParsedType::parse("IPv6").is_ip());
}
#[test]
fn test_fixed_byte_size() {
assert_eq!(ParsedType::parse("UInt8").fixed_byte_size(), Some(1));
assert_eq!(ParsedType::parse("Int32").fixed_byte_size(), Some(4));
assert_eq!(ParsedType::parse("Float64").fixed_byte_size(), Some(8));
assert_eq!(ParsedType::parse("UUID").fixed_byte_size(), Some(16));
assert_eq!(ParsedType::parse("Int256").fixed_byte_size(), Some(32));
assert_eq!(
ParsedType::parse("FixedString(32)").fixed_byte_size(),
Some(32)
);
assert_eq!(ParsedType::parse("String").fixed_byte_size(), None);
assert_eq!(ParsedType::parse("Array(Int64)").fixed_byte_size(), None);
}
#[test]
fn test_nested_array_map() {
let t = ParsedType::parse("Array(Nullable(String))");
assert_eq!(t.base, "Array");
let elem = t.array_element.as_ref().unwrap();
assert_eq!(elem.base, "String");
assert!(elem.nullable);
let t = ParsedType::parse("Map(String, Array(UInt64))");
assert_eq!(t.base, "Map");
let (k, v) = t.map_types.as_ref().unwrap();
assert_eq!(k.base, "String");
assert_eq!(v.base, "Array");
}
#[test]
fn test_display() {
let t = ParsedType::parse("LowCardinality(Nullable(String))");
assert_eq!(t.to_string(), "LowCardinality(Nullable(String))");
}
#[test]
fn category_date_types() {
assert_eq!(ParsedType::parse("Date").category(), "Date");
assert_eq!(ParsedType::parse("Date32").category(), "Date");
}
#[test]
fn category_decimal_types() {
assert_eq!(ParsedType::parse("Decimal(18, 4)").category(), "Decimal");
assert_eq!(ParsedType::parse("Decimal32(2)").category(), "Decimal");
assert_eq!(ParsedType::parse("Decimal64(4)").category(), "Decimal");
assert_eq!(ParsedType::parse("Decimal128(8)").category(), "Decimal");
}
#[test]
fn category_int_widths() {
for t in ["Int8", "Int16", "Int32", "Int64", "Int128", "Int256"] {
assert_eq!(ParsedType::parse(t).category(), "Int", "Failed for {t}");
}
}
#[test]
fn category_uint_widths() {
for t in ["UInt8", "UInt16", "UInt32", "UInt64", "UInt128", "UInt256"] {
assert_eq!(ParsedType::parse(t).category(), "UInt", "Failed for {t}");
}
}
#[test]
fn category_nullable_preserves_inner() {
assert_eq!(ParsedType::parse("Nullable(Int64)").category(), "Int");
assert_eq!(ParsedType::parse("Nullable(UUID)").category(), "UUID");
}
#[test]
fn category_lc_preserves_inner() {
assert_eq!(
ParsedType::parse("LowCardinality(String)").category(),
"String"
);
}
#[test]
fn parse_empty_string() {
let t = ParsedType::parse("");
assert_eq!(t.base, "");
}
#[test]
fn parse_enum_type() {
let t = ParsedType::parse("Enum8('a' = 1, 'b' = 2)");
assert_eq!(t.category(), "Enum");
}
#[test]
fn variant_and_dynamic_get_their_own_tags() {
let v = ParsedType::parse("Variant(UInt8, String)");
assert_eq!(v.base, "Variant");
assert_eq!(v.tag, TypeTag::Variant);
assert_eq!(v.category(), "Variant");
for raw in ["Dynamic", "Dynamic(max_types=8)"] {
let d = ParsedType::parse(raw);
assert_eq!(d.base, "Dynamic", "failed for {raw}");
assert_eq!(d.tag, TypeTag::Dynamic, "failed for {raw}");
}
}
#[test]
fn parse_preserves_the_raw_type_string() {
for raw in [
"LowCardinality(Nullable(String))",
"DateTime64(6, 'Australia/Sydney')",
"Map(String, Array(UInt64))",
"Enum8('a' = 1, 'b' = 2)",
"SomeTypeFromTheFuture(1, 2)",
] {
assert_eq!(ParsedType::parse(raw).raw, raw);
assert_eq!(ParsedType::parse(raw).to_string(), raw);
}
assert_eq!(ParsedType::parse(" UInt8 ").raw, "UInt8");
}
#[test]
fn parse_nested_array() {
let t = ParsedType::parse("Array(Array(String))");
assert_eq!(t.base, "Array");
let inner = t.array_element.as_ref().unwrap();
assert_eq!(inner.base, "Array");
let innermost = inner.array_element.as_ref().unwrap();
assert_eq!(innermost.base, "String");
}
#[test]
fn parse_fixedstring_with_length() {
let t = ParsedType::parse("FixedString(16)");
assert_eq!(t.base, "FixedString");
assert!(t.is_string());
assert_eq!(t.category(), "String");
}
}