From a71d0385321698df33a4bcb446107f411e858ad9 Mon Sep 17 00:00:00 2001 From: Wei-Ting Kuo Date: Sat, 27 Aug 2022 05:11:21 +0800 Subject: [PATCH 1/2] add with/without time zone (#589) --- src/ast/data_type.rs | 5 ++++- src/dialect/keywords.rs | 2 ++ src/parser.rs | 11 ++++++++--- src/tokenizer.rs | 2 +- tests/sqlparser_common.rs | 33 ++++++++++++++++++++++++++++----- 5 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 388703e76..9d68feba1 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -53,8 +53,10 @@ pub enum DataType { Date, /// Time Time, - /// Timestamp + /// Timestamp [Without Time Zone] Timestamp, + /// Timestamp With Time Zone + TimestampTz, /// Interval Interval, /// Regclass used in postgresql serial @@ -100,6 +102,7 @@ impl fmt::Display for DataType { DataType::Date => write!(f, "DATE"), DataType::Time => write!(f, "TIME"), DataType::Timestamp => write!(f, "TIMESTAMP"), + DataType::TimestampTz => write!(f, "TIMESTAMPTZ"), DataType::Interval => write!(f, "INTERVAL"), DataType::Regclass => write!(f, "REGCLASS"), DataType::Text => write!(f, "TEXT"), diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 1c481a882..c4337c7ed 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -445,6 +445,8 @@ define_keywords!( TIES, TIME, TIMESTAMP, + TIMESTAMPTZ, + TIMEZONE, TIMEZONE_HOUR, TIMEZONE_MINUTE, TO, diff --git a/src/parser.rs b/src/parser.rs index 5fc3846dc..84432e135 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2024,12 +2024,17 @@ 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::TimestampTz) + } else if self.parse_keyword(Keyword::WITHOUT) { + self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; + Ok(DataType::Timestamp) + } else { + Ok(DataType::Timestamp) } - Ok(DataType::Timestamp) } + Keyword::TIMESTAMPTZ => Ok(DataType::TimestampTz), Keyword::TIME => { // TBD: we throw away "with/without timezone" information if self.parse_keyword(Keyword::WITH) || self.parse_keyword(Keyword::WITHOUT) { 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..768059b24 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1893,7 +1893,7 @@ 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!( @@ -1903,6 +1903,29 @@ fn parse_literal_timestamp() { }, expr_from_projection(only(&select.projection)), ); + + one_statement_parses_to( + "SELECT TIMESTAMP WITHOUT TIME ZONE '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::TimestampTz, + value: "1999-01-01 01:23:34Z".into() + }, + expr_from_projection(only(&select.projection)), + ); + + one_statement_parses_to( + "SELECT TIMESTAMP WITH TIME ZONE '1999-01-01 01:23:34Z'", + sql, + ); } #[test] @@ -2715,10 +2738,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 { .. }, + } ); } From c02efaeffb6f7b4f0a87d73c5b53304bec425fa3 Mon Sep 17 00:00:00 2001 From: AugustoFKL <75763288+AugustoFKL@users.noreply.github.com> Date: Mon, 3 Oct 2022 09:37:17 -0300 Subject: [PATCH 2/2] Change `TIMESTAMP` and `TIME` parsing so that time zone information is preserved (#641) * 640 Fixing time zone printing format for TIMESTAMP and TIME * 640 Removing unnecessary changes * Update src/ast/data_type.rs Fix comment typo Co-authored-by: Andrew Lamb --- src/ast/data_type.rs | 63 ++++++++++++++++++++++++++++++++----- src/ast/mod.rs | 1 + src/dialect/keywords.rs | 1 + src/parser.rs | 19 ++++++----- tests/sqlparser_common.rs | 16 +++------- tests/sqlparser_postgres.rs | 4 +-- 6 files changed, 76 insertions(+), 28 deletions(-) diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 9d68feba1..11158b0cc 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -52,11 +52,11 @@ pub enum DataType { /// Date Date, /// Time - Time, - /// Timestamp [Without Time Zone] - Timestamp, - /// Timestamp With Time Zone - TimestampTz, + Time(TimezoneInfo), + /// Datetime + Datetime, + /// Timestamp + Timestamp(TimezoneInfo), /// Interval Interval, /// Regclass used in postgresql serial @@ -100,9 +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::TimestampTz => write!(f, "TIMESTAMPTZ"), + 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"), @@ -125,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 c4337c7ed..bbbcbbfd3 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -446,6 +446,7 @@ define_keywords!( TIME, TIMESTAMP, TIMESTAMPTZ, + TIMETZ, TIMEZONE, TIMEZONE_HOUR, TIMEZONE_MINUTE, diff --git a/src/parser.rs b/src/parser.rs index 84432e135..85f328dab 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -2026,22 +2026,27 @@ impl<'a> Parser<'a> { Keyword::TIMESTAMP => { if self.parse_keyword(Keyword::WITH) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::TimestampTz) + Ok(DataType::Timestamp(TimezoneInfo::WithTimeZone)) } else if self.parse_keyword(Keyword::WITHOUT) { self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?; - Ok(DataType::Timestamp) + Ok(DataType::Timestamp(TimezoneInfo::WithoutTimeZone)) } else { - Ok(DataType::Timestamp) + Ok(DataType::Timestamp(TimezoneInfo::None)) } } - Keyword::TIMESTAMPTZ => Ok(DataType::TimestampTz), + 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/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 768059b24..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)), @@ -1898,16 +1898,13 @@ fn parse_literal_timestamp_without_time_zone() { 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 WITHOUT TIME ZONE '1999-01-01 01:23:34'", - sql, - ); + one_statement_parses_to("SELECT TIMESTAMP '1999-01-01 01:23:34'", sql); } #[test] @@ -1916,16 +1913,13 @@ fn parse_literal_timestamp_with_time_zone() { let select = verified_only_select(sql); assert_eq!( &Expr::TypedString { - data_type: DataType::TimestampTz, + 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 TIMESTAMP WITH TIME ZONE '1999-01-01 01:23:34Z'", - sql, - ); + one_statement_parses_to("SELECT TIMESTAMPTZ '1999-01-01 01:23:34Z'", sql); } #[test] 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\ )");