Skip to main content

rudb_common/
types.rs

1//! The type system, per `spec/10-sql-and-types.md` section 10.1.
2//!
3//! A logical type is what SQL talks about. A physical type is how it is laid out. Keeping the two
4//! apart is what lets `DECIMAL(9, 2)` be stored in an `i32` without the planner having to know,
5//! and it is the same separation that later lets a `VARCHAR` column be handed to an operator as
6//! dictionary codes.
7//!
8//! Type names are spelled the way DuckDB spells them, including the aliases, because
9//! `spec/12-duckdb-compat.md` makes the dialect a compatibility surface and `CREATE TABLE t (a
10//! INT4)` is a thing people write.
11
12use std::fmt;
13
14use crate::error::{Error, Result};
15
16/// A named field of a `STRUCT` or a `UNION`.
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct Field {
19    /// The field name, unquoted and case sensitive as stored.
20    pub name: String,
21    /// The field type.
22    pub ty: LogicalType,
23}
24
25impl Field {
26    /// A field with a name and a type.
27    pub fn new(name: impl Into<String>, ty: LogicalType) -> Self {
28        Self { name: name.into(), ty }
29    }
30}
31
32/// What SQL thinks a value is.
33///
34/// Nulls are not in here. `spec/10-sql-and-types.md` says null is a per-value property at every
35/// nesting level, which makes it a property of a vector's validity mask rather than of a type.
36/// The one exception is [`LogicalType::Null`], which is the type of a literal `NULL` before
37/// anything has told it what it is, and which every other type absorbs during resolution.
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
39#[non_exhaustive]
40pub enum LogicalType {
41    /// The type of an untyped `NULL` literal.
42    Null,
43    /// `BOOLEAN`.
44    Boolean,
45    /// `TINYINT`, 8 bits signed.
46    TinyInt,
47    /// `SMALLINT`, 16 bits signed.
48    SmallInt,
49    /// `INTEGER`, 32 bits signed.
50    Integer,
51    /// `BIGINT`, 64 bits signed.
52    BigInt,
53    /// `HUGEINT`, 128 bits signed.
54    HugeInt,
55    /// `UTINYINT`, 8 bits unsigned.
56    UTinyInt,
57    /// `USMALLINT`, 16 bits unsigned.
58    USmallInt,
59    /// `UINTEGER`, 32 bits unsigned.
60    UInteger,
61    /// `UBIGINT`, 64 bits unsigned.
62    UBigInt,
63    /// `UHUGEINT`, 128 bits unsigned.
64    UHugeInt,
65    /// `FLOAT`, IEEE 754 binary32.
66    Float,
67    /// `DOUBLE`, IEEE 754 binary64.
68    Double,
69    /// `DECIMAL(width, scale)`, stored in the narrowest integer that holds `width` digits.
70    Decimal {
71        /// Total number of decimal digits, 1 through 38.
72        width: u8,
73        /// Digits to the right of the point, no greater than `width`.
74        scale: u8,
75    },
76    /// `VARCHAR`. Length modifiers parse and are then ignored, as they are in DuckDB.
77    Varchar,
78    /// `BLOB`.
79    Blob,
80    /// `BIT`, a bit string.
81    Bit,
82    /// `UUID`.
83    Uuid,
84    /// `DATE`, days since 1970-01-01.
85    Date,
86    /// `TIME`, microseconds since midnight.
87    Time,
88    /// `TIME WITH TIME ZONE`.
89    TimeTz,
90    /// `TIMESTAMP`, microseconds since the epoch.
91    Timestamp,
92    /// `TIMESTAMP_S`, seconds since the epoch.
93    TimestampS,
94    /// `TIMESTAMP_MS`, milliseconds since the epoch.
95    TimestampMs,
96    /// `TIMESTAMP_NS`, nanoseconds since the epoch.
97    TimestampNs,
98    /// `TIMESTAMP WITH TIME ZONE`.
99    TimestampTz,
100    /// `INTERVAL`, the months, days and microseconds triple.
101    Interval,
102    /// `T[]`, a variable length list.
103    List(Box<LogicalType>),
104    /// `T[n]`, a fixed length array.
105    Array(Box<LogicalType>, u32),
106    /// `STRUCT(name type, ...)`.
107    Struct(Vec<Field>),
108    /// `MAP(key, value)`.
109    Map(Box<LogicalType>, Box<LogicalType>),
110    /// `UNION(tag type, ...)`.
111    Union(Vec<Field>),
112}
113
114/// How a value is actually laid out in a vector.
115///
116/// The planner picks operators off this rather than off the logical type, which is why `DATE` and
117/// `INTEGER` share an implementation of everything that does not care what the number means.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119#[non_exhaustive]
120pub enum PhysicalType {
121    /// One byte per value, zero or one.
122    Bool,
123    /// 8 bit signed.
124    Int8,
125    /// 16 bit signed.
126    Int16,
127    /// 32 bit signed.
128    Int32,
129    /// 64 bit signed.
130    Int64,
131    /// 128 bit signed.
132    Int128,
133    /// 8 bit unsigned.
134    UInt8,
135    /// 16 bit unsigned.
136    UInt16,
137    /// 32 bit unsigned.
138    UInt32,
139    /// 64 bit unsigned.
140    UInt64,
141    /// 128 bit unsigned.
142    UInt128,
143    /// IEEE 754 binary32.
144    Float32,
145    /// IEEE 754 binary64.
146    Float64,
147    /// The months, days and microseconds triple.
148    Interval,
149    /// The 16 byte string representation from `spec/07-execution.md` section 7.1.
150    Varlen,
151    /// Offsets plus one child column.
152    List,
153    /// A fixed stride into one child column.
154    Array,
155    /// Named child columns, one per field.
156    Struct,
157    /// No storage. Only the validity mask says anything.
158    Empty,
159}
160
161impl LogicalType {
162    /// A `DECIMAL(width, scale)`, checked.
163    ///
164    /// # Errors
165    ///
166    /// If the width is zero or above 38, or the scale is greater than the width. Those are the
167    /// same bounds DuckDB enforces and the message is the same message.
168    pub fn decimal(width: u8, scale: u8) -> Result<Self> {
169        if width == 0 || width > MAX_DECIMAL_WIDTH {
170            return Err(Error::binder(format!("Width must be between 1 and {MAX_DECIMAL_WIDTH}!")));
171        }
172        if scale > width {
173            return Err(Error::binder(format!(
174                "Scale cannot be bigger than width, {scale} is bigger than {width}"
175            )));
176        }
177        Ok(Self::Decimal { width, scale })
178    }
179
180    /// A list of `element`.
181    #[must_use]
182    pub fn list(element: Self) -> Self {
183        Self::List(Box::new(element))
184    }
185
186    /// A fixed length array of `element`.
187    #[must_use]
188    pub fn array(element: Self, length: u32) -> Self {
189        Self::Array(Box::new(element), length)
190    }
191
192    /// A map from `key` to `value`.
193    #[must_use]
194    pub fn map(key: Self, value: Self) -> Self {
195        Self::Map(Box::new(key), Box::new(value))
196    }
197
198    /// How this type is laid out.
199    #[must_use]
200    pub fn physical(&self) -> PhysicalType {
201        match self {
202            Self::Null => PhysicalType::Empty,
203            Self::Boolean => PhysicalType::Bool,
204            Self::TinyInt => PhysicalType::Int8,
205            Self::SmallInt => PhysicalType::Int16,
206            Self::Integer | Self::Date => PhysicalType::Int32,
207            Self::BigInt
208            | Self::Time
209            | Self::TimeTz
210            | Self::Timestamp
211            | Self::TimestampS
212            | Self::TimestampMs
213            | Self::TimestampNs
214            | Self::TimestampTz => PhysicalType::Int64,
215            Self::HugeInt | Self::Uuid => PhysicalType::Int128,
216            Self::UTinyInt => PhysicalType::UInt8,
217            Self::USmallInt => PhysicalType::UInt16,
218            Self::UInteger => PhysicalType::UInt32,
219            Self::UBigInt => PhysicalType::UInt64,
220            Self::UHugeInt => PhysicalType::UInt128,
221            Self::Float => PhysicalType::Float32,
222            Self::Double => PhysicalType::Float64,
223            // The narrowest integer that holds the requested number of digits, which is the
224            // standard representation and the one DuckDB uses. A DECIMAL(9, 2) column costs four
225            // bytes a value and not sixteen.
226            Self::Decimal { width, .. } => match width {
227                0..=4 => PhysicalType::Int16,
228                5..=9 => PhysicalType::Int32,
229                10..=18 => PhysicalType::Int64,
230                _ => PhysicalType::Int128,
231            },
232            Self::Varchar | Self::Blob | Self::Bit => PhysicalType::Varlen,
233            Self::Interval => PhysicalType::Interval,
234            // A map is a list of two-field structs, which is how Arrow does it and how every
235            // engine that has to interoperate with Arrow ends up doing it.
236            Self::List(_) | Self::Map(_, _) => PhysicalType::List,
237            Self::Array(_, _) => PhysicalType::Array,
238            Self::Struct(_) | Self::Union(_) => PhysicalType::Struct,
239        }
240    }
241
242    /// Whether arithmetic applies.
243    #[must_use]
244    pub fn is_numeric(&self) -> bool {
245        self.is_integer() || matches!(self, Self::Float | Self::Double | Self::Decimal { .. })
246    }
247
248    /// Whether this is one of the integer types, signed or unsigned.
249    #[must_use]
250    pub fn is_integer(&self) -> bool {
251        matches!(
252            self,
253            Self::TinyInt
254                | Self::SmallInt
255                | Self::Integer
256                | Self::BigInt
257                | Self::HugeInt
258                | Self::UTinyInt
259                | Self::USmallInt
260                | Self::UInteger
261                | Self::UBigInt
262                | Self::UHugeInt
263        )
264    }
265
266    /// Whether this is a date, a time, a timestamp or an interval.
267    #[must_use]
268    pub fn is_temporal(&self) -> bool {
269        matches!(
270            self,
271            Self::Date
272                | Self::Time
273                | Self::TimeTz
274                | Self::Timestamp
275                | Self::TimestampS
276                | Self::TimestampMs
277                | Self::TimestampNs
278                | Self::TimestampTz
279                | Self::Interval
280        )
281    }
282
283    /// Whether this type contains other types.
284    ///
285    /// Nested types are stored columnar all the way down, so this is the question of whether a
286    /// column of this type is one column chunk or several.
287    #[must_use]
288    pub fn is_nested(&self) -> bool {
289        matches!(
290            self,
291            Self::List(_) | Self::Array(_, _) | Self::Struct(_) | Self::Map(_, _) | Self::Union(_)
292        )
293    }
294
295    /// The types this one contains, in child column order, or empty for a scalar type.
296    #[must_use]
297    pub fn children(&self) -> Vec<Self> {
298        match self {
299            Self::List(inner) | Self::Array(inner, _) => vec![inner.as_ref().clone()],
300            Self::Map(key, value) => vec![key.as_ref().clone(), value.as_ref().clone()],
301            Self::Struct(fields) | Self::Union(fields) => {
302                fields.iter().map(|f| f.ty.clone()).collect()
303            }
304            _ => Vec::new(),
305        }
306    }
307
308    /// Parses a SQL type name, aliases included.
309    ///
310    /// # Errors
311    ///
312    /// If the text is not a type name this understands. The message names the offending word,
313    /// because a type parse failure two levels inside a `STRUCT` is otherwise unreadable.
314    pub fn parse(text: &str) -> Result<Self> {
315        let tokens = lex(text)?;
316        let mut parser = TypeParser { tokens: &tokens, position: 0 };
317        let ty = parser.parse_type()?;
318        if parser.position != parser.tokens.len() {
319            return Err(Error::parser(format!("Type \"{text}\" has trailing text")));
320        }
321        Ok(ty)
322    }
323}
324
325/// The largest number of decimal digits a `DECIMAL` can carry, because 38 digits is what fits in
326/// 128 bits and there is no wider physical type.
327pub const MAX_DECIMAL_WIDTH: u8 = 38;
328
329impl fmt::Display for LogicalType {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        match self {
332            Self::Null => f.write_str("\"NULL\""),
333            Self::Boolean => f.write_str("BOOLEAN"),
334            Self::TinyInt => f.write_str("TINYINT"),
335            Self::SmallInt => f.write_str("SMALLINT"),
336            Self::Integer => f.write_str("INTEGER"),
337            Self::BigInt => f.write_str("BIGINT"),
338            Self::HugeInt => f.write_str("HUGEINT"),
339            Self::UTinyInt => f.write_str("UTINYINT"),
340            Self::USmallInt => f.write_str("USMALLINT"),
341            Self::UInteger => f.write_str("UINTEGER"),
342            Self::UBigInt => f.write_str("UBIGINT"),
343            Self::UHugeInt => f.write_str("UHUGEINT"),
344            Self::Float => f.write_str("FLOAT"),
345            Self::Double => f.write_str("DOUBLE"),
346            Self::Decimal { width, scale } => write!(f, "DECIMAL({width},{scale})"),
347            Self::Varchar => f.write_str("VARCHAR"),
348            Self::Blob => f.write_str("BLOB"),
349            Self::Bit => f.write_str("BIT"),
350            Self::Uuid => f.write_str("UUID"),
351            Self::Date => f.write_str("DATE"),
352            Self::Time => f.write_str("TIME"),
353            Self::TimeTz => f.write_str("TIME WITH TIME ZONE"),
354            Self::Timestamp => f.write_str("TIMESTAMP"),
355            Self::TimestampS => f.write_str("TIMESTAMP_S"),
356            Self::TimestampMs => f.write_str("TIMESTAMP_MS"),
357            Self::TimestampNs => f.write_str("TIMESTAMP_NS"),
358            Self::TimestampTz => f.write_str("TIMESTAMP WITH TIME ZONE"),
359            Self::Interval => f.write_str("INTERVAL"),
360            Self::List(inner) => write!(f, "{inner}[]"),
361            Self::Array(inner, length) => write!(f, "{inner}[{length}]"),
362            Self::Map(key, value) => write!(f, "MAP({key}, {value})"),
363            Self::Struct(fields) => write_fields(f, "STRUCT", fields),
364            Self::Union(fields) => write_fields(f, "UNION", fields),
365        }
366    }
367}
368
369fn write_fields(f: &mut fmt::Formatter<'_>, keyword: &str, fields: &[Field]) -> fmt::Result {
370    f.write_str(keyword)?;
371    f.write_str("(")?;
372    for (index, field) in fields.iter().enumerate() {
373        if index > 0 {
374            f.write_str(", ")?;
375        }
376        write_identifier(f, &field.name)?;
377        write!(f, " {}", field.ty)?;
378    }
379    f.write_str(")")
380}
381
382/// Writes a field name, quoting it if it would not survive being read back unquoted.
383fn write_identifier(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
384    let plain = !name.is_empty()
385        && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
386        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
387    if plain {
388        f.write_str(name)
389    } else {
390        f.write_str("\"")?;
391        for c in name.chars() {
392            if c == '"' {
393                f.write_str("\"\"")?;
394            } else {
395                write!(f, "{c}")?;
396            }
397        }
398        f.write_str("\"")
399    }
400}
401
402#[derive(Debug, Clone, PartialEq, Eq)]
403enum Token {
404    Word(String),
405    Quoted(String),
406    Number(u32),
407    LeftParen,
408    RightParen,
409    LeftBracket,
410    RightBracket,
411    Comma,
412}
413
414fn lex(text: &str) -> Result<Vec<Token>> {
415    let mut tokens = Vec::new();
416    let chars: Vec<char> = text.chars().collect();
417    let mut i = 0;
418    while i < chars.len() {
419        let c = chars[i];
420        match c {
421            c if c.is_whitespace() => i += 1,
422            '(' => {
423                tokens.push(Token::LeftParen);
424                i += 1;
425            }
426            ')' => {
427                tokens.push(Token::RightParen);
428                i += 1;
429            }
430            '[' => {
431                tokens.push(Token::LeftBracket);
432                i += 1;
433            }
434            ']' => {
435                tokens.push(Token::RightBracket);
436                i += 1;
437            }
438            ',' => {
439                tokens.push(Token::Comma);
440                i += 1;
441            }
442            '"' => {
443                let mut name = String::new();
444                i += 1;
445                loop {
446                    let Some(&c) = chars.get(i) else {
447                        return Err(Error::parser(format!(
448                            "Type \"{text}\" has an unterminated quoted name"
449                        )));
450                    };
451                    i += 1;
452                    if c == '"' {
453                        if chars.get(i) == Some(&'"') {
454                            name.push('"');
455                            i += 1;
456                            continue;
457                        }
458                        break;
459                    }
460                    name.push(c);
461                }
462                tokens.push(Token::Quoted(name));
463            }
464            c if c.is_ascii_digit() => {
465                let start = i;
466                while chars.get(i).is_some_and(char::is_ascii_digit) {
467                    i += 1;
468                }
469                let digits: String = chars[start..i].iter().collect();
470                let number = digits.parse::<u32>().map_err(|_| {
471                    Error::parser(format!("Type \"{text}\" has a number that is too large"))
472                })?;
473                tokens.push(Token::Number(number));
474            }
475            c if c.is_alphabetic() || c == '_' => {
476                let start = i;
477                while chars.get(i).is_some_and(|&c| c.is_alphanumeric() || c == '_') {
478                    i += 1;
479                }
480                tokens.push(Token::Word(chars[start..i].iter().collect()));
481            }
482            other => {
483                return Err(Error::parser(format!(
484                    "Type \"{text}\" has an unexpected character {other:?}"
485                )));
486            }
487        }
488    }
489    Ok(tokens)
490}
491
492struct TypeParser<'a> {
493    tokens: &'a [Token],
494    position: usize,
495}
496
497impl TypeParser<'_> {
498    fn peek(&self) -> Option<&Token> {
499        self.tokens.get(self.position)
500    }
501
502    fn eat(&mut self, token: &Token) -> bool {
503        if self.peek() == Some(token) {
504            self.position += 1;
505            true
506        } else {
507            false
508        }
509    }
510
511    /// Consumes `word` if it is next, case insensitively.
512    fn eat_word(&mut self, word: &str) -> bool {
513        match self.peek() {
514            Some(Token::Word(found)) if found.eq_ignore_ascii_case(word) => {
515                self.position += 1;
516                true
517            }
518            _ => false,
519        }
520    }
521
522    fn parse_type(&mut self) -> Result<LogicalType> {
523        let mut ty = self.parse_base()?;
524        // Suffixes bind left to right, so INTEGER[][3] is an array of three lists.
525        loop {
526            if !self.eat(&Token::LeftBracket) {
527                break;
528            }
529            if let Some(&Token::Number(length)) = self.peek() {
530                self.position += 1;
531                expect(self.eat(&Token::RightBracket), "]")?;
532                ty = LogicalType::array(ty, length);
533            } else {
534                expect(self.eat(&Token::RightBracket), "]")?;
535                ty = LogicalType::list(ty);
536            }
537        }
538        Ok(ty)
539    }
540
541    fn parse_base(&mut self) -> Result<LogicalType> {
542        // A quoted name is accepted here because the null type prints as "NULL" with the quotes,
543        // which is DuckDB's spelling and which has to read back.
544        let word = match self.peek().cloned() {
545            Some(Token::Word(word) | Token::Quoted(word)) => {
546                self.position += 1;
547                word
548            }
549            _ => return Err(Error::parser("Expected a type name".to_string())),
550        };
551        let upper = word.to_ascii_uppercase();
552
553        match upper.as_str() {
554            "STRUCT" | "ROW" => return self.parse_fields().map(LogicalType::Struct),
555            "UNION" => return self.parse_fields().map(LogicalType::Union),
556            "MAP" => {
557                expect(self.eat(&Token::LeftParen), "(")?;
558                let key = self.parse_type()?;
559                expect(self.eat(&Token::Comma), ",")?;
560                let value = self.parse_type()?;
561                expect(self.eat(&Token::RightParen), ")")?;
562                return Ok(LogicalType::map(key, value));
563            }
564            "DECIMAL" | "NUMERIC" | "DEC" => {
565                if !self.eat(&Token::LeftParen) {
566                    // Bare DECIMAL is DECIMAL(18, 3) in DuckDB, which is a surprising default and
567                    // is nonetheless the one people's queries depend on.
568                    return LogicalType::decimal(18, 3);
569                }
570                let width = self.parse_number()?;
571                let scale = if self.eat(&Token::Comma) { self.parse_number()? } else { 0 };
572                expect(self.eat(&Token::RightParen), ")")?;
573                let narrow = |n: u32| u8::try_from(n).unwrap_or(u8::MAX);
574                return LogicalType::decimal(narrow(width), narrow(scale));
575            }
576            // Multiword names. Each of these is a word that only means something with the words
577            // after it, so the lookahead is checked before the alias table is consulted.
578            "DOUBLE" => {
579                self.eat_word("PRECISION");
580                return Ok(LogicalType::Double);
581            }
582            "CHARACTER" => {
583                self.eat_word("VARYING");
584                self.eat_length_modifier()?;
585                return Ok(LogicalType::Varchar);
586            }
587            "TIME" | "TIMESTAMP" => {
588                let with_zone = self.eat_time_zone_suffix();
589                return Ok(match (upper.as_str(), with_zone) {
590                    ("TIME", false) => LogicalType::Time,
591                    ("TIME", true) => LogicalType::TimeTz,
592                    (_, false) => LogicalType::Timestamp,
593                    (_, true) => LogicalType::TimestampTz,
594                });
595            }
596            _ => {}
597        }
598
599        // A length modifier on a string type parses and is discarded, which is what DuckDB does:
600        // VARCHAR(10) does not truncate and does not reject, it is VARCHAR.
601        self.eat_length_modifier()?;
602        alias(&upper).ok_or_else(|| Error::parser(format!("Unrecognized type name \"{word}\"")))
603    }
604
605    /// `WITH TIME ZONE` or `WITHOUT TIME ZONE`, returning whether the zone is carried.
606    fn eat_time_zone_suffix(&mut self) -> bool {
607        let start = self.position;
608        let with = if self.eat_word("WITH") {
609            true
610        } else if self.eat_word("WITHOUT") {
611            false
612        } else {
613            return false;
614        };
615        if self.eat_word("TIME") && self.eat_word("ZONE") {
616            with
617        } else {
618            self.position = start;
619            false
620        }
621    }
622
623    fn eat_length_modifier(&mut self) -> Result<()> {
624        if self.eat(&Token::LeftParen) {
625            self.parse_number()?;
626            expect(self.eat(&Token::RightParen), ")")?;
627        }
628        Ok(())
629    }
630
631    fn parse_fields(&mut self) -> Result<Vec<Field>> {
632        expect(self.eat(&Token::LeftParen), "(")?;
633        let mut fields = Vec::new();
634        if self.eat(&Token::RightParen) {
635            return Ok(fields);
636        }
637        loop {
638            let name = match self.peek().cloned() {
639                Some(Token::Word(name) | Token::Quoted(name)) => {
640                    self.position += 1;
641                    name
642                }
643                _ => return Err(Error::parser("Expected a field name".to_string())),
644            };
645            let ty = self.parse_type()?;
646            fields.push(Field::new(name, ty));
647            if self.eat(&Token::Comma) {
648                continue;
649            }
650            expect(self.eat(&Token::RightParen), ")")?;
651            return Ok(fields);
652        }
653    }
654
655    fn parse_number(&mut self) -> Result<u32> {
656        match self.peek() {
657            Some(&Token::Number(n)) => {
658                self.position += 1;
659                Ok(n)
660            }
661            _ => Err(Error::parser("Expected a number".to_string())),
662        }
663    }
664}
665
666fn expect(matched: bool, what: &str) -> Result<()> {
667    if matched { Ok(()) } else { Err(Error::parser(format!("Expected \"{what}\""))) }
668}
669
670/// The single word type names, aliases included.
671///
672/// The aliases are DuckDB's, and they are here rather than in the parser because `CREATE TABLE t
673/// (a INT4)` and `CAST(x AS INT4)` have to agree and there is only one table.
674fn alias(upper: &str) -> Option<LogicalType> {
675    Some(match upper {
676        "NULL" => LogicalType::Null,
677        "BOOLEAN" | "BOOL" | "LOGICAL" => LogicalType::Boolean,
678        "TINYINT" | "INT1" => LogicalType::TinyInt,
679        "SMALLINT" | "INT2" | "SHORT" => LogicalType::SmallInt,
680        "INTEGER" | "INT" | "INT4" | "SIGNED" => LogicalType::Integer,
681        "BIGINT" | "INT8" | "LONG" => LogicalType::BigInt,
682        "HUGEINT" | "INT128" => LogicalType::HugeInt,
683        "UTINYINT" | "UINT1" => LogicalType::UTinyInt,
684        "USMALLINT" | "UINT2" => LogicalType::USmallInt,
685        "UINTEGER" | "UINT4" => LogicalType::UInteger,
686        "UBIGINT" | "UINT8" => LogicalType::UBigInt,
687        "UHUGEINT" | "UINT128" => LogicalType::UHugeInt,
688        "FLOAT" | "FLOAT4" | "REAL" => LogicalType::Float,
689        "FLOAT8" => LogicalType::Double,
690        "VARCHAR" | "CHAR" | "BPCHAR" | "TEXT" | "STRING" => LogicalType::Varchar,
691        "BLOB" | "BYTEA" | "BINARY" | "VARBINARY" => LogicalType::Blob,
692        "BIT" | "BITSTRING" => LogicalType::Bit,
693        "UUID" | "GUID" => LogicalType::Uuid,
694        "DATE" => LogicalType::Date,
695        "TIMETZ" => LogicalType::TimeTz,
696        "DATETIME" => LogicalType::Timestamp,
697        "TIMESTAMP_S" | "TIMESTAMP_SEC" | "TIMESTAMP_SECONDS" => LogicalType::TimestampS,
698        "TIMESTAMP_MS" | "TIMESTAMP_MILLISECONDS" => LogicalType::TimestampMs,
699        "TIMESTAMP_NS" | "TIMESTAMP_NANOSECONDS" => LogicalType::TimestampNs,
700        "TIMESTAMPTZ" => LogicalType::TimestampTz,
701        "INTERVAL" => LogicalType::Interval,
702        _ => return None,
703    })
704}
705
706#[cfg(test)]
707mod tests {
708    use super::{Field, LogicalType, PhysicalType};
709
710    /// Every type this crate knows about, used by the round trip test and by anything else that
711    /// wants to be exhaustive without listing them again.
712    fn every_type() -> Vec<LogicalType> {
713        vec![
714            LogicalType::Null,
715            LogicalType::Boolean,
716            LogicalType::TinyInt,
717            LogicalType::SmallInt,
718            LogicalType::Integer,
719            LogicalType::BigInt,
720            LogicalType::HugeInt,
721            LogicalType::UTinyInt,
722            LogicalType::USmallInt,
723            LogicalType::UInteger,
724            LogicalType::UBigInt,
725            LogicalType::UHugeInt,
726            LogicalType::Float,
727            LogicalType::Double,
728            LogicalType::Decimal { width: 18, scale: 3 },
729            LogicalType::Decimal { width: 38, scale: 0 },
730            LogicalType::Varchar,
731            LogicalType::Blob,
732            LogicalType::Bit,
733            LogicalType::Uuid,
734            LogicalType::Date,
735            LogicalType::Time,
736            LogicalType::TimeTz,
737            LogicalType::Timestamp,
738            LogicalType::TimestampS,
739            LogicalType::TimestampMs,
740            LogicalType::TimestampNs,
741            LogicalType::TimestampTz,
742            LogicalType::Interval,
743            LogicalType::list(LogicalType::Integer),
744            LogicalType::list(LogicalType::list(LogicalType::Varchar)),
745            LogicalType::array(LogicalType::Double, 3),
746            LogicalType::map(LogicalType::Varchar, LogicalType::Integer),
747            LogicalType::Struct(vec![
748                Field::new("a", LogicalType::Integer),
749                Field::new("b", LogicalType::list(LogicalType::Varchar)),
750            ]),
751            LogicalType::Union(vec![
752                Field::new("num", LogicalType::Integer),
753                Field::new("str", LogicalType::Varchar),
754            ]),
755        ]
756    }
757
758    #[test]
759    fn every_type_survives_being_printed_and_read_back() {
760        // The textual plan format in spec/04-architecture.md round trips, and a plan carries
761        // types, so this is the bottom of that guarantee. Failing it means a plan that cannot be
762        // reparsed, which is the whole reason the format exists.
763        for ty in every_type() {
764            let printed = ty.to_string();
765            let parsed = LogicalType::parse(&printed)
766                .unwrap_or_else(|e| panic!("{printed} did not parse back: {e}"));
767            assert_eq!(parsed, ty, "{printed} parsed to something else");
768        }
769    }
770
771    #[test]
772    fn a_field_name_that_needs_quoting_gets_quoted() {
773        let ty = LogicalType::Struct(vec![
774            Field::new("plain", LogicalType::Integer),
775            Field::new("has space", LogicalType::Integer),
776            Field::new("has\"quote", LogicalType::Integer),
777            Field::new("2leading", LogicalType::Integer),
778        ]);
779        assert_eq!(
780            ty.to_string(),
781            "STRUCT(plain INTEGER, \"has space\" INTEGER, \"has\"\"quote\" INTEGER, \
782             \"2leading\" INTEGER)"
783        );
784        assert_eq!(LogicalType::parse(&ty.to_string()).unwrap(), ty);
785    }
786
787    #[test]
788    fn the_duckdb_aliases_resolve() {
789        let cases = [
790            ("int4", LogicalType::Integer),
791            ("INT", LogicalType::Integer),
792            ("signed", LogicalType::Integer),
793            ("int8", LogicalType::BigInt),
794            ("float4", LogicalType::Float),
795            ("float8", LogicalType::Double),
796            ("double precision", LogicalType::Double),
797            ("text", LogicalType::Varchar),
798            ("varchar(10)", LogicalType::Varchar),
799            ("character varying(255)", LogicalType::Varchar),
800            ("bool", LogicalType::Boolean),
801            ("datetime", LogicalType::Timestamp),
802            ("numeric(9, 2)", LogicalType::Decimal { width: 9, scale: 2 }),
803            ("decimal", LogicalType::Decimal { width: 18, scale: 3 }),
804            ("timestamp without time zone", LogicalType::Timestamp),
805            ("timestamp with time zone", LogicalType::TimestampTz),
806            ("time with time zone", LogicalType::TimeTz),
807        ];
808        for (text, expected) in cases {
809            assert_eq!(LogicalType::parse(text).unwrap(), expected, "{text}");
810        }
811    }
812
813    #[test]
814    fn list_and_array_suffixes_bind_left_to_right() {
815        assert_eq!(
816            LogicalType::parse("INTEGER[][3]").unwrap(),
817            LogicalType::array(LogicalType::list(LogicalType::Integer), 3)
818        );
819        assert_eq!(
820            LogicalType::parse("STRUCT(a INT)[]").unwrap(),
821            LogicalType::list(LogicalType::Struct(vec![Field::new("a", LogicalType::Integer)]))
822        );
823    }
824
825    #[test]
826    fn a_decimal_is_stored_in_the_narrowest_integer_that_holds_it() {
827        assert_eq!(LogicalType::decimal(4, 2).unwrap().physical(), PhysicalType::Int16);
828        assert_eq!(LogicalType::decimal(9, 2).unwrap().physical(), PhysicalType::Int32);
829        assert_eq!(LogicalType::decimal(18, 2).unwrap().physical(), PhysicalType::Int64);
830        assert_eq!(LogicalType::decimal(38, 2).unwrap().physical(), PhysicalType::Int128);
831    }
832
833    #[test]
834    fn a_decimal_outside_the_bounds_is_rejected_rather_than_clamped() {
835        assert!(LogicalType::decimal(0, 0).is_err());
836        assert!(LogicalType::decimal(39, 0).is_err());
837        assert!(LogicalType::decimal(4, 5).is_err());
838        assert!(LogicalType::parse("DECIMAL(39,0)").is_err());
839    }
840
841    #[test]
842    fn a_date_and_an_integer_share_a_layout_and_not_a_meaning() {
843        assert_eq!(LogicalType::Date.physical(), LogicalType::Integer.physical());
844        assert_ne!(LogicalType::Date, LogicalType::Integer);
845        assert!(LogicalType::Date.is_temporal());
846        assert!(!LogicalType::Date.is_numeric());
847    }
848
849    #[test]
850    fn nesting_reports_its_children_in_child_column_order() {
851        let ty = LogicalType::map(LogicalType::Varchar, LogicalType::Integer);
852        assert!(ty.is_nested());
853        assert_eq!(ty.children(), vec![LogicalType::Varchar, LogicalType::Integer]);
854        assert_eq!(LogicalType::Integer.children(), Vec::new());
855    }
856
857    #[test]
858    fn text_that_is_not_a_type_is_rejected_with_the_word_that_broke_it() {
859        let error = LogicalType::parse("INTEGRE").unwrap_err();
860        assert!(error.message().contains("INTEGRE"), "{error}");
861        assert!(LogicalType::parse("INTEGER JUNK").is_err());
862        assert!(LogicalType::parse("STRUCT(a)").is_err());
863        assert!(LogicalType::parse("").is_err());
864    }
865}