1use std::fmt;
7use std::str::FromStr;
8
9use super::column_type::ColumnType;
10use super::decimal_typmod::{DecimalTypmod, DecimalTypmodError};
11use crate::error::sqlstate;
12
13pub const DECLARED_INT_KEYWORDS: [&str; 8] = [
22 "BIGINT", "INT64", "INTEGER", "INT", "INT4", "INT8", "SMALLINT", "INT2",
23];
24
25pub const DECLARED_FLOAT_KEYWORDS: [&str; 8] = [
33 "FLOAT64",
34 "DOUBLE",
35 "DOUBLE PRECISION",
36 "FLOAT8",
37 "REAL",
38 "FLOAT4",
39 "FLOAT32",
40 "FLOAT",
41];
42
43#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
45#[non_exhaustive]
46pub enum ColumnTypeParseError {
47 #[error("unknown column type: '{0}'")]
48 Unknown(String),
49 #[error("'DATETIME' is not a valid type — use 'TIMESTAMP' instead")]
50 UseTimestamp,
51 #[error("invalid VECTOR dimension: '{0}' (must be a positive integer)")]
52 InvalidVectorDim(String),
53 #[error("invalid VARCHAR length: '{0}' (must be a positive integer)")]
54 InvalidCharLength(String),
55 #[error(
56 "invalid DECIMAL/NUMERIC params: '{0}' (expected DECIMAL(precision) or DECIMAL(precision, scale))"
57 )]
58 InvalidDecimalParams(String),
59 #[error("{0}")]
61 InvalidDecimalTypmod(#[from] DecimalTypmodError),
62}
63
64impl ColumnTypeParseError {
65 pub fn sqlstate(&self) -> &'static str {
70 match self {
71 Self::InvalidDecimalTypmod(_) => sqlstate::INVALID_PARAMETER_VALUE,
72 Self::Unknown(_)
73 | Self::UseTimestamp
74 | Self::InvalidVectorDim(_)
75 | Self::InvalidCharLength(_)
76 | Self::InvalidDecimalParams(_) => sqlstate::SYNTAX_ERROR,
77 }
78 }
79}
80
81impl fmt::Display for ColumnType {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::Int64 => f.write_str("BIGINT"),
85 Self::Float64 => f.write_str("FLOAT64"),
86 Self::String => f.write_str("TEXT"),
87 Self::Bool => f.write_str("BOOL"),
88 Self::Bytes => f.write_str("BYTES"),
89 Self::Timestamp => f.write_str("TIMESTAMP"),
90 Self::Timestamptz => f.write_str("TIMESTAMPTZ"),
91 Self::SystemTimestamp => f.write_str("SYSTEM_TIMESTAMP"),
92 Self::Decimal(Some(typmod)) => {
93 write!(f, "DECIMAL({},{})", typmod.precision(), typmod.scale())
94 }
95 Self::Decimal(None) => f.write_str("DECIMAL"),
96 Self::Geometry => f.write_str("GEOMETRY"),
97 Self::Vector(dim) => write!(f, "VECTOR({dim})"),
98 Self::SparseVector => f.write_str("SPARSEVECTOR"),
99 Self::Uuid => f.write_str("UUID"),
100 Self::Json => f.write_str("JSON"),
101 Self::Ulid => f.write_str("ULID"),
102 Self::Duration => f.write_str("DURATION"),
103 Self::Array => f.write_str("ARRAY"),
104 Self::Set => f.write_str("SET"),
105 Self::Regex => f.write_str("REGEX"),
106 Self::Range => f.write_str("RANGE"),
107 Self::Record => f.write_str("RECORD"),
108 }
109 }
110}
111
112impl FromStr for ColumnType {
113 type Err = ColumnTypeParseError;
114
115 fn from_str(s: &str) -> Result<Self, Self::Err> {
116 let upper = s.trim().to_uppercase();
117
118 if let Some(rest) = upper
122 .strip_prefix("NUMERIC")
123 .or_else(|| upper.strip_prefix("DECIMAL"))
124 {
125 let rest = rest.trim();
126 if rest.is_empty() {
127 return Ok(Self::Decimal(None));
128 }
129 if rest.starts_with('(') {
130 return parse_decimal_typmod(rest).map(|typmod| Self::Decimal(Some(typmod)));
131 }
132 }
133
134 if upper.starts_with("VECTOR") {
136 let inner = upper
137 .trim_start_matches("VECTOR")
138 .trim()
139 .trim_start_matches('(')
140 .trim_end_matches(')')
141 .trim();
142 if inner.is_empty() {
143 return Err(ColumnTypeParseError::InvalidVectorDim("empty".into()));
144 }
145 let dim: u32 = inner
146 .parse()
147 .map_err(|_| ColumnTypeParseError::InvalidVectorDim(inner.into()))?;
148 if dim == 0 {
149 return Err(ColumnTypeParseError::InvalidVectorDim("0".into()));
150 }
151 return Ok(Self::Vector(dim));
152 }
153
154 if let Some(rest) = upper.strip_prefix("VARCHAR")
160 && let Some(inner) = rest.strip_prefix('(').and_then(|r| r.strip_suffix(')'))
161 {
162 let length: u32 = inner
163 .trim()
164 .parse()
165 .map_err(|_| ColumnTypeParseError::InvalidCharLength(inner.trim().to_string()))?;
166 if length == 0 {
167 return Err(ColumnTypeParseError::InvalidCharLength("0".into()));
168 }
169 return Ok(Self::String);
170 }
171
172 match upper.as_str() {
173 keyword if DECLARED_INT_KEYWORDS.contains(&keyword) => Ok(Self::Int64),
177 keyword if DECLARED_FLOAT_KEYWORDS.contains(&keyword) => Ok(Self::Float64),
179 "TEXT" | "STRING" | "VARCHAR" => Ok(Self::String),
180 "BOOL" | "BOOLEAN" => Ok(Self::Bool),
181 "BYTES" | "BYTEA" | "BLOB" => Ok(Self::Bytes),
182 "TIMESTAMP" | "TIMESTAMP WITHOUT TIME ZONE" => Ok(Self::Timestamp),
183 "TIMESTAMPTZ" | "TIMESTAMP WITH TIME ZONE" => Ok(Self::Timestamptz),
184 "SYSTEM_TIMESTAMP" | "SYSTEMTIMESTAMP" => Ok(Self::SystemTimestamp),
185 "GEOMETRY" => Ok(Self::Geometry),
186 "SPARSEVECTOR" => Ok(Self::SparseVector),
191 "UUID" => Ok(Self::Uuid),
192 "JSON" | "JSONB" => Ok(Self::Json),
193 "ULID" => Ok(Self::Ulid),
194 "DURATION" => Ok(Self::Duration),
195 "ARRAY" => Ok(Self::Array),
196 "SET" => Ok(Self::Set),
197 "REGEX" => Ok(Self::Regex),
198 "RANGE" => Ok(Self::Range),
199 "RECORD" => Ok(Self::Record),
200 "DATETIME" => Err(ColumnTypeParseError::UseTimestamp),
201 other => Err(ColumnTypeParseError::Unknown(other.to_string())),
202 }
203 }
204}
205
206impl ColumnType {
207 pub fn from_declared_type(declared: &str) -> Option<Self> {
223 Self::parse_declared_type(declared).ok()
224 }
225
226 pub fn parse_declared_type(declared: &str) -> Result<Self, ColumnTypeParseError> {
231 bare_declared_token(declared).parse()
232 }
233}
234
235fn parse_decimal_typmod(params: &str) -> Result<DecimalTypmod, ColumnTypeParseError> {
241 let malformed = || ColumnTypeParseError::InvalidDecimalParams(params.to_string());
242 let inner = params
243 .strip_prefix('(')
244 .and_then(|p| p.strip_suffix(')'))
245 .ok_or_else(malformed)?;
246 let (precision_text, scale_text) = match inner.split_once(',') {
247 Some((precision, scale)) => (precision, scale),
248 None => (inner, "0"),
249 };
250 let precision: i64 = precision_text.trim().parse().map_err(|_| malformed())?;
251 let scale: i64 = scale_text.trim().parse().map_err(|_| malformed())?;
252 Ok(DecimalTypmod::new(precision, scale)?)
253}
254
255fn bare_declared_token(declared: &str) -> &str {
261 let trimmed = declared.trim_start();
262 let mut depth = 0usize;
263 let mut end = trimmed.len();
264 for (index, ch) in trimmed.char_indices() {
265 match ch {
266 '(' => depth += 1,
267 ')' => depth = depth.saturating_sub(1),
268 _ if depth == 0 && ch.is_whitespace() => {
269 end = index;
270 break;
271 }
272 _ => {}
273 }
274 }
275 trimmed.get(..end).unwrap_or(trimmed).trim_end_matches(',')
276}
277
278#[cfg(test)]
279mod tests {
280 use super::super::{FloatWidth, IntWidth};
281 use super::*;
282
283 #[test]
290 fn every_declared_int_keyword_resolves_to_int64_and_a_width() {
291 for keyword in DECLARED_INT_KEYWORDS {
292 assert_eq!(
293 keyword.parse::<ColumnType>(),
294 Ok(ColumnType::Int64),
295 "{keyword} must resolve to Int64"
296 );
297 assert!(
298 IntWidth::from_declared_type(keyword).is_some(),
299 "{keyword} must resolve to a declared IntWidth"
300 );
301 }
302 }
303
304 #[test]
307 fn every_declared_float_keyword_resolves_to_float64_and_a_width() {
308 for keyword in DECLARED_FLOAT_KEYWORDS {
309 assert_eq!(
310 keyword.parse::<ColumnType>(),
311 Ok(ColumnType::Float64),
312 "{keyword} must resolve to Float64"
313 );
314 assert!(
315 FloatWidth::from_declared_type(keyword).is_some(),
316 "{keyword} must resolve to a declared FloatWidth"
317 );
318 }
319 }
320
321 #[test]
322 fn declared_type_ignores_trailing_modifiers() {
323 assert_eq!(
324 ColumnType::from_declared_type("INT DEFAULT 5"),
325 Some(ColumnType::Int64)
326 );
327 assert_eq!(
328 ColumnType::from_declared_type("TEXT NOT NULL PRIMARY KEY"),
329 Some(ColumnType::String)
330 );
331 assert_eq!(
332 ColumnType::from_declared_type("timestamp time_key"),
333 Some(ColumnType::Timestamp)
334 );
335 assert_eq!(
336 ColumnType::from_declared_type("BIGINT,"),
337 Some(ColumnType::Int64)
338 );
339 }
340
341 #[test]
344 fn declared_type_keeps_a_spaced_parameter_list() {
345 assert_eq!(
346 ColumnType::from_declared_type("DECIMAL(10, 2) NOT NULL"),
347 Some(ColumnType::Decimal(Some(
348 DecimalTypmod::new(10, 2).expect("valid typmod")
349 )))
350 );
351 assert_eq!(
352 ColumnType::from_declared_type("VECTOR(768)"),
353 Some(ColumnType::Vector(768))
354 );
355 }
356
357 #[test]
360 fn declared_decimal_typmod_errors_are_typed() {
361 let err = ColumnType::parse_declared_type("DECIMAL(1001,0) NOT NULL")
362 .expect_err("precision 1001 is refused");
363 assert_eq!(
364 err,
365 ColumnTypeParseError::InvalidDecimalTypmod(DecimalTypmodError::PrecisionOutOfRange {
366 precision: 1001
367 })
368 );
369 assert_eq!(err.sqlstate(), "22023");
370 assert_eq!(ColumnType::from_declared_type("DECIMAL(1001,0)"), None);
371
372 let err = ColumnType::parse_declared_type("NUMERIC(x)").expect_err("malformed");
373 assert_eq!(err.sqlstate(), "42601");
374 assert_eq!(
375 ColumnType::parse_declared_type("NUMERIC DEFAULT 1"),
376 Ok(ColumnType::Decimal(None))
377 );
378 }
379
380 #[test]
383 fn both_timestamp_zone_spellings_resolve_whole() {
384 assert_eq!(
385 "TIMESTAMP WITH TIME ZONE".parse::<ColumnType>(),
386 Ok(ColumnType::Timestamptz)
387 );
388 assert_eq!(
389 "timestamp without time zone".parse::<ColumnType>(),
390 Ok(ColumnType::Timestamp)
391 );
392 }
393
394 #[test]
397 fn varchar_length_resolves_to_string() {
398 assert_eq!("VARCHAR(255)".parse::<ColumnType>(), Ok(ColumnType::String));
399 assert_eq!("varchar(1)".parse::<ColumnType>(), Ok(ColumnType::String));
400 assert_eq!(
401 ColumnType::from_declared_type("VARCHAR(8) NOT NULL"),
402 Some(ColumnType::String)
403 );
404 assert_eq!(
405 "VARCHAR(0)".parse::<ColumnType>(),
406 Err(ColumnTypeParseError::InvalidCharLength("0".into()))
407 );
408 assert_eq!(
409 "VARCHAR(abc)".parse::<ColumnType>(),
410 Err(ColumnTypeParseError::InvalidCharLength("ABC".into()))
411 );
412 }
413
414 #[test]
415 fn declared_type_reports_an_unknown_token_as_none() {
416 assert_eq!(ColumnType::from_declared_type("SOMETHING_ELSE"), None);
417 assert_eq!(ColumnType::from_declared_type(""), None);
418 assert_eq!(ColumnType::from_declared_type(" "), None);
419 }
420
421 #[test]
424 fn only_timestamp_types_are_instants() {
425 assert!(ColumnType::Timestamp.is_instant());
426 assert!(ColumnType::Timestamptz.is_instant());
427 assert!(!ColumnType::SystemTimestamp.is_instant());
428 assert!(!ColumnType::Int64.is_instant());
429 assert!(!ColumnType::String.is_instant());
430 }
431}