diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 388703e76..11158b0cc 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -52,9 +52,11 @@ pub enum DataType { /// Date Date, /// Time - Time, + Time(TimezoneInfo), + /// Datetime + Datetime, /// Timestamp - Timestamp, + Timestamp(TimezoneInfo), /// Interval Interval, /// Regclass used in postgresql serial @@ -98,8 +100,9 @@ impl fmt::Display for DataType { DataType::Double => write!(f, "DOUBLE"), DataType::Boolean => write!(f, "BOOLEAN"), DataType::Date => write!(f, "DATE"), - DataType::Time => write!(f, "TIME"), - DataType::Timestamp => write!(f, "TIMESTAMP"), + DataType::Datetime => write!(f, "DATETIME"), + DataType::Time(timezone_info) => write!(f, "TIME{}", timezone_info), + DataType::Timestamp(timezone_info) => write!(f, "TIMESTAMP{}", timezone_info), DataType::Interval => write!(f, "INTERVAL"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), @@ -122,3 +125,50 @@ fn format_type_with_optional_length( } Ok(()) } + +/// Timestamp and Time data types information about TimeZone formatting. +/// +/// This is more related to a display information than real differences between each variant. To +/// guarantee compatibility with the input query we must maintain its exact information. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum TimezoneInfo { + /// No information about time zone. E.g., TIMESTAMP + None, + /// Temporal type 'WITH TIME ZONE'. E.g., TIMESTAMP WITH TIME ZONE, [standard], [Oracle] + /// + /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type + /// [Oracle]: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/nlspg/datetime-data-types-and-time-zone-support.html#GUID-3F1C388E-C651-43D5-ADBC-1A49E5C2CA05 + WithTimeZone, + /// Temporal type 'WITHOUT TIME ZONE'. E.g., TIME WITHOUT TIME ZONE, [standard], [Postgresql] + /// + /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#datetime-type + /// [Postgresql]: https://www.postgresql.org/docs/current/datatype-datetime.html + WithoutTimeZone, + /// Postgresql specific `WITH TIME ZONE` formatting, for both TIME and TIMESTAMP. E.g., TIMETZ, [Postgresql] + /// + /// [Postgresql]: https://www.postgresql.org/docs/current/datatype-datetime.html + Tz, +} + +impl fmt::Display for TimezoneInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TimezoneInfo::None => { + write!(f, "") + } + TimezoneInfo::WithTimeZone => { + write!(f, " WITH TIME ZONE") + } + TimezoneInfo::WithoutTimeZone => { + write!(f, " WITHOUT TIME ZONE") + } + TimezoneInfo::Tz => { + // TZ is the only one that is displayed BEFORE the precision, so the datatype display + // must be aware of that. Check + // for more information + write!(f, "TZ") + } + } + } +} diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0dbb5319d..09618a083 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -23,6 +23,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; pub use self::data_type::DataType; +pub use self::data_type::TimezoneInfo; pub use self::ddl::{ AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, ReferentialAction, TableConstraint, diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 1c481a882..bbbcbbfd3 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -445,6 +445,9 @@ define_keywords!( TIES, TIME, TIMESTAMP, + TIMESTAMPTZ, + TIMETZ, + TIMEZONE, TIMEZONE_HOUR, TIMEZONE_MINUTE, TO, diff --git a/src/parser.rs b/src/parser.rs index 5fc3846dc..85f328dab 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2024,19 +2024,29 @@ impl<'a> Parser<'a> { Keyword::UUID => Ok(DataType::Uuid), Keyword::DATE => Ok(DataType::Date), Keyword::TIMESTAMP => { - // TBD: we throw away "with/without timezone" information - if self.parse_keyword(Keyword::WITH) || self.parse_keyword(Keyword::WITHOUT) { + if self.parse_keyword(Keyword::WITH) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Timestamp(TimezoneInfo::WithTimeZone)) + } else if self.parse_keyword(Keyword::WITHOUT) { + self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Timestamp(TimezoneInfo::WithoutTimeZone)) + } else { + Ok(DataType::Timestamp(TimezoneInfo::None)) } - Ok(DataType::Timestamp) } + Keyword::TIMESTAMPTZ => Ok(DataType::Timestamp(TimezoneInfo::Tz)), Keyword::TIME => { - // TBD: we throw away "with/without timezone" information - if self.parse_keyword(Keyword::WITH) || self.parse_keyword(Keyword::WITHOUT) { + if self.parse_keyword(Keyword::WITH) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Time(TimezoneInfo::WithTimeZone)) + } else if self.parse_keyword(Keyword::WITHOUT) { + self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Time(TimezoneInfo::WithoutTimeZone)) + } else { + Ok(DataType::Time(TimezoneInfo::None)) } - Ok(DataType::Time) } + Keyword::TIMETZ => Ok(DataType::Time(TimezoneInfo::Tz)), // Interval types can be followed by a complicated interval // qualifier that we don't currently support. See // parse_interval_literal for a taste. diff --git a/src/tokenizer.rs b/src/tokenizer.rs index d04e1d8f7..4222eecca 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -616,7 +616,7 @@ impl<'a> Tokenizer<'a> { 'r' => s.push('\r'), 't' => s.push('\t'), 'Z' => s.push('\x1a'), - x => s.push(x) + x => s.push(x), } } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 4739f146d..a737ed679 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1885,7 +1885,7 @@ fn parse_literal_time() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Time, + data_type: DataType::Time(TimezoneInfo::None), value: "01:23:34".into() }, expr_from_projection(only(&select.projection)), @@ -1893,16 +1893,33 @@ fn parse_literal_time() { } #[test] -fn parse_literal_timestamp() { +fn parse_literal_timestamp_without_time_zone() { let sql = "SELECT TIMESTAMP '1999-01-01 01:23:34'"; let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::Timestamp, + data_type: DataType::Timestamp(TimezoneInfo::None), value: "1999-01-01 01:23:34".into() }, expr_from_projection(only(&select.projection)), ); + + one_statement_parses_to("SELECT TIMESTAMP '1999-01-01 01:23:34'", sql); +} + +#[test] +fn parse_literal_timestamp_with_time_zone() { + let sql = "SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'"; + let select = verified_only_select(sql); + assert_eq!( + &Expr::TypedString { + data_type: DataType::Timestamp(TimezoneInfo::Tz), + value: "1999-01-01 01:23:34Z".into() + }, + expr_from_projection(only(&select.projection)), + ); + + one_statement_parses_to("SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'", sql); } #[test] @@ -2715,10 +2732,10 @@ fn parse_scalar_subqueries() { assert_matches!( verified_expr(sql), Expr::BinaryOp { - op: BinaryOperator::Plus, .. - //left: box Subquery { .. }, - //right: box Subquery { .. }, - } + op: BinaryOperator::Plus, + .. //left: box Subquery { .. }, + //right: box Subquery { .. }, + } ); } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bcfce30fc..126dbb983 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -139,7 +139,7 @@ fn parse_create_table_with_defaults() { }, ColumnDef { name: "last_update".into(), - data_type: DataType::Timestamp, + data_type: DataType::Timestamp(TimezoneInfo::WithoutTimeZone), collation: None, options: vec![ ColumnOptionDef { @@ -212,7 +212,7 @@ fn parse_create_table_from_pg_dump() { activebool BOOLEAN DEFAULT true NOT NULL, \ create_date DATE DEFAULT CAST(now() AS DATE) NOT NULL, \ create_date1 DATE DEFAULT CAST(CAST('now' AS TEXT) AS DATE) NOT NULL, \ - last_update TIMESTAMP DEFAULT now(), \ + last_update TIMESTAMP WITHOUT TIME ZONE DEFAULT now(), \ release_year public.year, \ active INT\ )");