use std::fmt;
use std::str::FromStr;
use super::column_type::ColumnType;
use super::decimal_typmod::{DecimalTypmod, DecimalTypmodError};
use crate::error::sqlstate;
pub const DECLARED_INT_KEYWORDS: [&str; 8] = [
"BIGINT", "INT64", "INTEGER", "INT", "INT4", "INT8", "SMALLINT", "INT2",
];
pub const DECLARED_FLOAT_KEYWORDS: [&str; 8] = [
"FLOAT64",
"DOUBLE",
"DOUBLE PRECISION",
"FLOAT8",
"REAL",
"FLOAT4",
"FLOAT32",
"FLOAT",
];
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ColumnTypeParseError {
#[error("unknown column type: '{0}'")]
Unknown(String),
#[error("'DATETIME' is not a valid type — use 'TIMESTAMP' instead")]
UseTimestamp,
#[error("invalid VECTOR dimension: '{0}' (must be a positive integer)")]
InvalidVectorDim(String),
#[error("invalid VARCHAR length: '{0}' (must be a positive integer)")]
InvalidCharLength(String),
#[error(
"invalid DECIMAL/NUMERIC params: '{0}' (expected DECIMAL(precision) or DECIMAL(precision, scale))"
)]
InvalidDecimalParams(String),
#[error("{0}")]
InvalidDecimalTypmod(#[from] DecimalTypmodError),
}
impl ColumnTypeParseError {
pub fn sqlstate(&self) -> &'static str {
match self {
Self::InvalidDecimalTypmod(_) => sqlstate::INVALID_PARAMETER_VALUE,
Self::Unknown(_)
| Self::UseTimestamp
| Self::InvalidVectorDim(_)
| Self::InvalidCharLength(_)
| Self::InvalidDecimalParams(_) => sqlstate::SYNTAX_ERROR,
}
}
}
impl fmt::Display for ColumnType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Int64 => f.write_str("BIGINT"),
Self::Float64 => f.write_str("FLOAT64"),
Self::String => f.write_str("TEXT"),
Self::Bool => f.write_str("BOOL"),
Self::Bytes => f.write_str("BYTES"),
Self::Timestamp => f.write_str("TIMESTAMP"),
Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
Self::SystemTimestamp => f.write_str("SYSTEM_TIMESTAMP"),
Self::Decimal(Some(typmod)) => {
write!(f, "DECIMAL({},{})", typmod.precision(), typmod.scale())
}
Self::Decimal(None) => f.write_str("DECIMAL"),
Self::Geometry => f.write_str("GEOMETRY"),
Self::Vector(dim) => write!(f, "VECTOR({dim})"),
Self::SparseVector => f.write_str("SPARSEVECTOR"),
Self::Uuid => f.write_str("UUID"),
Self::Json => f.write_str("JSON"),
Self::Ulid => f.write_str("ULID"),
Self::Duration => f.write_str("DURATION"),
Self::Array => f.write_str("ARRAY"),
Self::Set => f.write_str("SET"),
Self::Regex => f.write_str("REGEX"),
Self::Range => f.write_str("RANGE"),
Self::Record => f.write_str("RECORD"),
}
}
}
impl FromStr for ColumnType {
type Err = ColumnTypeParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let upper = s.trim().to_uppercase();
if let Some(rest) = upper
.strip_prefix("NUMERIC")
.or_else(|| upper.strip_prefix("DECIMAL"))
{
let rest = rest.trim();
if rest.is_empty() {
return Ok(Self::Decimal(None));
}
if rest.starts_with('(') {
return parse_decimal_typmod(rest).map(|typmod| Self::Decimal(Some(typmod)));
}
}
if upper.starts_with("VECTOR") {
let inner = upper
.trim_start_matches("VECTOR")
.trim()
.trim_start_matches('(')
.trim_end_matches(')')
.trim();
if inner.is_empty() {
return Err(ColumnTypeParseError::InvalidVectorDim("empty".into()));
}
let dim: u32 = inner
.parse()
.map_err(|_| ColumnTypeParseError::InvalidVectorDim(inner.into()))?;
if dim == 0 {
return Err(ColumnTypeParseError::InvalidVectorDim("0".into()));
}
return Ok(Self::Vector(dim));
}
if let Some(rest) = upper.strip_prefix("VARCHAR")
&& let Some(inner) = rest.strip_prefix('(').and_then(|r| r.strip_suffix(')'))
{
let length: u32 = inner
.trim()
.parse()
.map_err(|_| ColumnTypeParseError::InvalidCharLength(inner.trim().to_string()))?;
if length == 0 {
return Err(ColumnTypeParseError::InvalidCharLength("0".into()));
}
return Ok(Self::String);
}
match upper.as_str() {
keyword if DECLARED_INT_KEYWORDS.contains(&keyword) => Ok(Self::Int64),
keyword if DECLARED_FLOAT_KEYWORDS.contains(&keyword) => Ok(Self::Float64),
"TEXT" | "STRING" | "VARCHAR" => Ok(Self::String),
"BOOL" | "BOOLEAN" => Ok(Self::Bool),
"BYTES" | "BYTEA" | "BLOB" => Ok(Self::Bytes),
"TIMESTAMP" | "TIMESTAMP WITHOUT TIME ZONE" => Ok(Self::Timestamp),
"TIMESTAMPTZ" | "TIMESTAMP WITH TIME ZONE" => Ok(Self::Timestamptz),
"SYSTEM_TIMESTAMP" | "SYSTEMTIMESTAMP" => Ok(Self::SystemTimestamp),
"GEOMETRY" => Ok(Self::Geometry),
"SPARSEVECTOR" => Ok(Self::SparseVector),
"UUID" => Ok(Self::Uuid),
"JSON" | "JSONB" => Ok(Self::Json),
"ULID" => Ok(Self::Ulid),
"DURATION" => Ok(Self::Duration),
"ARRAY" => Ok(Self::Array),
"SET" => Ok(Self::Set),
"REGEX" => Ok(Self::Regex),
"RANGE" => Ok(Self::Range),
"RECORD" => Ok(Self::Record),
"DATETIME" => Err(ColumnTypeParseError::UseTimestamp),
other => Err(ColumnTypeParseError::Unknown(other.to_string())),
}
}
}
impl ColumnType {
pub fn from_declared_type(declared: &str) -> Option<Self> {
Self::parse_declared_type(declared).ok()
}
pub fn parse_declared_type(declared: &str) -> Result<Self, ColumnTypeParseError> {
bare_declared_token(declared).parse()
}
}
fn parse_decimal_typmod(params: &str) -> Result<DecimalTypmod, ColumnTypeParseError> {
let malformed = || ColumnTypeParseError::InvalidDecimalParams(params.to_string());
let inner = params
.strip_prefix('(')
.and_then(|p| p.strip_suffix(')'))
.ok_or_else(malformed)?;
let (precision_text, scale_text) = match inner.split_once(',') {
Some((precision, scale)) => (precision, scale),
None => (inner, "0"),
};
let precision: i64 = precision_text.trim().parse().map_err(|_| malformed())?;
let scale: i64 = scale_text.trim().parse().map_err(|_| malformed())?;
Ok(DecimalTypmod::new(precision, scale)?)
}
fn bare_declared_token(declared: &str) -> &str {
let trimmed = declared.trim_start();
let mut depth = 0usize;
let mut end = trimmed.len();
for (index, ch) in trimmed.char_indices() {
match ch {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
_ if depth == 0 && ch.is_whitespace() => {
end = index;
break;
}
_ => {}
}
}
trimmed.get(..end).unwrap_or(trimmed).trim_end_matches(',')
}
#[cfg(test)]
mod tests {
use super::super::{FloatWidth, IntWidth};
use super::*;
#[test]
fn every_declared_int_keyword_resolves_to_int64_and_a_width() {
for keyword in DECLARED_INT_KEYWORDS {
assert_eq!(
keyword.parse::<ColumnType>(),
Ok(ColumnType::Int64),
"{keyword} must resolve to Int64"
);
assert!(
IntWidth::from_declared_type(keyword).is_some(),
"{keyword} must resolve to a declared IntWidth"
);
}
}
#[test]
fn every_declared_float_keyword_resolves_to_float64_and_a_width() {
for keyword in DECLARED_FLOAT_KEYWORDS {
assert_eq!(
keyword.parse::<ColumnType>(),
Ok(ColumnType::Float64),
"{keyword} must resolve to Float64"
);
assert!(
FloatWidth::from_declared_type(keyword).is_some(),
"{keyword} must resolve to a declared FloatWidth"
);
}
}
#[test]
fn declared_type_ignores_trailing_modifiers() {
assert_eq!(
ColumnType::from_declared_type("INT DEFAULT 5"),
Some(ColumnType::Int64)
);
assert_eq!(
ColumnType::from_declared_type("TEXT NOT NULL PRIMARY KEY"),
Some(ColumnType::String)
);
assert_eq!(
ColumnType::from_declared_type("timestamp time_key"),
Some(ColumnType::Timestamp)
);
assert_eq!(
ColumnType::from_declared_type("BIGINT,"),
Some(ColumnType::Int64)
);
}
#[test]
fn declared_type_keeps_a_spaced_parameter_list() {
assert_eq!(
ColumnType::from_declared_type("DECIMAL(10, 2) NOT NULL"),
Some(ColumnType::Decimal(Some(
DecimalTypmod::new(10, 2).expect("valid typmod")
)))
);
assert_eq!(
ColumnType::from_declared_type("VECTOR(768)"),
Some(ColumnType::Vector(768))
);
}
#[test]
fn declared_decimal_typmod_errors_are_typed() {
let err = ColumnType::parse_declared_type("DECIMAL(1001,0) NOT NULL")
.expect_err("precision 1001 is refused");
assert_eq!(
err,
ColumnTypeParseError::InvalidDecimalTypmod(DecimalTypmodError::PrecisionOutOfRange {
precision: 1001
})
);
assert_eq!(err.sqlstate(), "22023");
assert_eq!(ColumnType::from_declared_type("DECIMAL(1001,0)"), None);
let err = ColumnType::parse_declared_type("NUMERIC(x)").expect_err("malformed");
assert_eq!(err.sqlstate(), "42601");
assert_eq!(
ColumnType::parse_declared_type("NUMERIC DEFAULT 1"),
Ok(ColumnType::Decimal(None))
);
}
#[test]
fn both_timestamp_zone_spellings_resolve_whole() {
assert_eq!(
"TIMESTAMP WITH TIME ZONE".parse::<ColumnType>(),
Ok(ColumnType::Timestamptz)
);
assert_eq!(
"timestamp without time zone".parse::<ColumnType>(),
Ok(ColumnType::Timestamp)
);
}
#[test]
fn varchar_length_resolves_to_string() {
assert_eq!("VARCHAR(255)".parse::<ColumnType>(), Ok(ColumnType::String));
assert_eq!("varchar(1)".parse::<ColumnType>(), Ok(ColumnType::String));
assert_eq!(
ColumnType::from_declared_type("VARCHAR(8) NOT NULL"),
Some(ColumnType::String)
);
assert_eq!(
"VARCHAR(0)".parse::<ColumnType>(),
Err(ColumnTypeParseError::InvalidCharLength("0".into()))
);
assert_eq!(
"VARCHAR(abc)".parse::<ColumnType>(),
Err(ColumnTypeParseError::InvalidCharLength("ABC".into()))
);
}
#[test]
fn declared_type_reports_an_unknown_token_as_none() {
assert_eq!(ColumnType::from_declared_type("SOMETHING_ELSE"), None);
assert_eq!(ColumnType::from_declared_type(""), None);
assert_eq!(ColumnType::from_declared_type(" "), None);
}
#[test]
fn only_timestamp_types_are_instants() {
assert!(ColumnType::Timestamp.is_instant());
assert!(ColumnType::Timestamptz.is_instant());
assert!(!ColumnType::SystemTimestamp.is_instant());
assert!(!ColumnType::Int64.is_instant());
assert!(!ColumnType::String.is_instant());
}
}