Skip to content

Commit

Permalink
Change TIMESTAMP and TIME parsing so that time zone information i…
Browse files Browse the repository at this point in the history
…s preserved (sqlparser-rs#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 <andrew@nerdnetworks.org>
  • Loading branch information
2 people authored and ovr committed Mar 21, 2023
1 parent a71d038 commit c02efae
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 28 deletions.
63 changes: 55 additions & 8 deletions src/ast/data_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"),
Expand All @@ -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 <https://www.postgresql.org/docs/14/datatype-datetime.html>
// for more information
write!(f, "TZ")
}
}
}
}
1 change: 1 addition & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/dialect/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ define_keywords!(
TIME,
TIMESTAMP,
TIMESTAMPTZ,
TIMETZ,
TIMEZONE,
TIMEZONE_HOUR,
TIMEZONE_MINUTE,
Expand Down
19 changes: 12 additions & 7 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 5 additions & 11 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -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]
Expand All @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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\
)");
Expand Down

0 comments on commit c02efae

Please sign in to comment.