From 805ca84e4d7167514c97fbfb412bdc6072df0ce0 Mon Sep 17 00:00:00 2001 From: Ruchir Khaitan Date: Tue, 12 Nov 2019 13:25:41 -0500 Subject: [PATCH] Support timestamptz / timestamp with time zone (#855) --- src/ast/value.rs | 7 + src/ast/value/datetime.rs | 3 + src/dialect/keywords.rs | 1 + src/parser.rs | 81 +++++++- src/parser/datetime.rs | 411 +++++++++++++++++++++++++++++++++++++- src/test_utils.rs | 22 ++ tests/sqlparser_common.rs | 76 ++++++- 7 files changed, 593 insertions(+), 8 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index b5fad61..696dfe4 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -52,6 +52,8 @@ pub enum Value { Time(String), /// `TIMESTAMP '...'` literals Timestamp(String, ParsedTimestamp), + /// `TIMESTAMP WITH TIME ZONE` literals + TimestampTz(String, ParsedTimestamp), /// INTERVAL literals, roughly in the following format: /// /// ```text @@ -80,6 +82,11 @@ impl fmt::Display for Value { Value::Date(v, _) => write!(f, "DATE '{}'", escape_single_quote_string(v)), Value::Time(v) => write!(f, "TIME '{}'", escape_single_quote_string(v)), Value::Timestamp(v, _) => write!(f, "TIMESTAMP '{}'", escape_single_quote_string(v)), + Value::TimestampTz(v, _) => write!( + f, + "TIMESTAMP WITH TIME ZONE '{}'", + escape_single_quote_string(v) + ), Value::Interval(IntervalValue { parsed: _, value, diff --git a/src/ast/value/datetime.rs b/src/ast/value/datetime.rs index bb6b7ac..0231b7e 100644 --- a/src/ast/value/datetime.rs +++ b/src/ast/value/datetime.rs @@ -280,6 +280,7 @@ pub struct ParsedTimestamp { pub minute: u8, pub second: u8, pub nano: u32, + pub timezone_offset_second: i64, } /// All of the fields that can appear in a literal `DATE`, `TIMESTAMP` or `INTERVAL` string @@ -297,6 +298,7 @@ pub struct ParsedDateTime { pub minute: Option, pub second: Option, pub nano: Option, + pub timezone_offset_second: Option, } impl ParsedDateTime { @@ -320,6 +322,7 @@ impl Default for ParsedDateTime { minute: None, second: None, nano: None, + timezone_offset_second: None, } } } diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 57f2b1e..f4b4370 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -386,6 +386,7 @@ define_keywords!( TIES, TIME, TIMESTAMP, + TIMESTAMPTZ, TIMEZONE_HOUR, TIMEZONE_MINUTE, TO, diff --git a/src/parser.rs b/src/parser.rs index 43aeda1..6fd3c76 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -212,7 +212,8 @@ impl Parser { expr: Box::new(self.parse_subexpr(Self::UNARY_NOT_PREC)?), }), "TIME" => Ok(Expr::Value(Value::Time(self.parse_literal_string()?))), - "TIMESTAMP" => Ok(Expr::Value(self.parse_timestamp()?)), + "TIMESTAMP" => self.parse_timestamp(), + "TIMESTAMPTZ" => self.parse_timestamptz(), // Here `w` is a word, check if it's a part of a multi-part // identifier, a function call, or a simple identifier: _ => match self.peek_token() { @@ -508,16 +509,46 @@ impl Parser { } } - fn parse_timestamp(&mut self) -> Result { + fn parse_timestamp(&mut self) -> Result { + if self.parse_keyword("WITH") { + self.expect_keywords(&["TIME", "ZONE"])?; + return Ok(Expr::Value(self.parse_timestamp_inner(true)?)); + } else if self.parse_keyword("WITHOUT") { + self.expect_keywords(&["TIME", "ZONE"])?; + } + Ok(Expr::Value(self.parse_timestamp_inner(false)?)) + } + + fn parse_timestamptz(&mut self) -> Result { + Ok(Expr::Value(self.parse_timestamp_inner(true)?)) + } + + fn parse_timestamp_inner(&mut self, parse_timezone: bool) -> Result { use std::convert::TryInto; let value = self.parse_literal_string()?; - let pdt = Self::parse_interval_string(&value, &DateTimeField::Year)?; + let pdt = Self::parse_timestamp_string(&value, parse_timezone)?; match ( - pdt.year, pdt.month, pdt.day, pdt.hour, pdt.minute, pdt.second, pdt.nano, + pdt.year, + pdt.month, + pdt.day, + pdt.hour, + pdt.minute, + pdt.second, + pdt.nano, + pdt.timezone_offset_second, ) { - (Some(year), Some(month), Some(day), Some(hour), Some(minute), Some(second), nano) => { + ( + Some(year), + Some(month), + Some(day), + Some(hour), + Some(minute), + Some(second), + nano, + timezone_offset_second, + ) => { let p_err = |e: std::num::TryFromIntError, field: &str| { ParserError::ParserError(format!( "{} in date '{}' is invalid: {}", @@ -555,6 +586,23 @@ impl Parser { if second > 60 { parser_err!("Second in timestamp '{}' cannot be > 60: {}", value, second)?; } + + if parse_timezone { + return Ok(Value::TimestampTz( + value, + ParsedTimestamp { + year, + month, + day, + hour, + minute, + second, + nano: nano.unwrap_or(0), + timezone_offset_second: timezone_offset_second.unwrap_or(0), + }, + )); + } + Ok(Value::Timestamp( value, ParsedTimestamp { @@ -565,6 +613,7 @@ impl Parser { minute, second, nano: nano.unwrap_or(0), + timezone_offset_second: 0, }, )) } @@ -773,6 +822,27 @@ impl Parser { datetime::build_parsed_datetime(&toks, leading_field, value) } + pub fn parse_timestamp_string( + value: &str, + parse_timezone: bool, + ) -> Result { + if value.is_empty() { + return Err(ParserError::ParserError( + "Timestamp string is empty!".to_string(), + )); + } + + let (ts_string, tz_string) = datetime::split_timestamp_string(value); + + let mut pdt = Self::parse_interval_string(ts_string, &DateTimeField::Year)?; + if !parse_timezone || tz_string.is_empty() { + return Ok(pdt); + } + + pdt.timezone_offset_second = Some(datetime::parse_timezone_offset_second(tz_string)?); + Ok(pdt) + } + /// Parses the parens following the `[ NOT ] IN` operator pub fn parse_in(&mut self, expr: Expr, negated: bool) -> Result { self.expect_token(&Token::LParen)?; @@ -1554,6 +1624,7 @@ impl Parser { } Ok(DataType::Timestamp) } + "TIMESTAMPTZ" => Ok(DataType::TimestampTz), "TIME" => { if self.parse_keyword("WITH") { self.expect_keywords(&["TIME", "ZONE"])?; diff --git a/src/parser/datetime.rs b/src/parser/datetime.rs index 8ad098b..fce7970 100644 --- a/src/parser/datetime.rs +++ b/src/parser/datetime.rs @@ -68,6 +68,96 @@ pub(crate) fn tokenize_interval(value: &str) -> Result, Parse Ok(toks) } +fn tokenize_timezone(value: &str) -> Result, ParserError> { + let mut toks: Vec = vec![]; + let mut num_buf = String::with_capacity(4); + // If the timezone string has a colon, we need to parse all numbers naively. + // Otherwise we need to parse long sequences of digits as [..hhhhmm] + let split_nums: bool = !value.contains(':'); + + // Takes a string and tries to parse it as a number token and insert it into the + // token list + fn parse_num( + toks: &mut Vec, + n: &str, + split_nums: bool, + idx: usize, + ) -> Result<(), ParserError> { + if n.is_empty() { + return Ok(()); + } + + let (first, second) = if n.len() > 2 && split_nums == true { + let (first, second) = n.split_at(n.len() - 2); + (first, Some(second)) + } else { + (n, None) + }; + + toks.push(IntervalToken::Num(first.parse().map_err(|e| { + ParserError::ParserError(format!( + "Error tokenizing timezone string: unable to parse value {} as a number at index {}: {}", + first, idx, e + )) + })?)); + + if let Some(second) = second { + toks.push(IntervalToken::Num(second.parse().map_err(|e| { + ParserError::ParserError(format!( + "Error tokenizing timezone string: unable to parse value {} as a number at index {}: {}", + second, idx, e + )) + })?)); + } + + Ok(()) + }; + for (i, chr) in value.chars().enumerate() { + match chr { + '-' => { + parse_num(&mut toks, &num_buf, split_nums, i)?; + num_buf.clear(); + toks.push(IntervalToken::Dash); + } + ' ' => { + parse_num(&mut toks, &num_buf, split_nums, i)?; + num_buf.clear(); + toks.push(IntervalToken::Space); + } + ':' => { + parse_num(&mut toks, &num_buf, split_nums, i)?; + num_buf.clear(); + toks.push(IntervalToken::Colon); + } + '+' => { + parse_num(&mut toks, &num_buf, split_nums, i)?; + num_buf.clear(); + toks.push(IntervalToken::Plus); + } + chr if (chr == 'z' || chr == 'Z') && (i == value.len() - 1) => { + parse_num(&mut toks, &num_buf, split_nums, i)?; + num_buf.clear(); + toks.push(IntervalToken::Zulu); + } + chr if chr.is_digit(10) => num_buf.push(chr), + chr if chr.is_ascii_alphabetic() => { + parse_num(&mut toks, &num_buf, split_nums, i)?; + let substring = &value[i..]; + toks.push(IntervalToken::TzName(substring.to_string())); + return Ok(toks); + } + chr => { + return Err(ParserError::TokenizerError(format!( + "Error tokenizing timezone string ({}): invalid character {:?} at offset {}", + value, chr, i + ))) + } + } + } + parse_num(&mut toks, &num_buf, split_nums, 0)?; + Ok(toks) +} + /// Get the tokens that you *might* end up parsing starting with a most significant unit /// /// For example, parsing `INTERVAL '9-5 4:3' MONTH` is *illegal*, but you @@ -104,14 +194,118 @@ fn potential_interval_tokens(from: &DateTimeField) -> Vec { all_toks[offset..].to_vec() } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +fn build_timezone_offset_second( + tokens: &[IntervalToken], + value: &str, +) -> Result { + use IntervalToken::*; + let all_formats = [ + vec![Plus, Num(0), Colon, Num(0)], + vec![Dash, Num(0), Colon, Num(0)], + vec![Plus, Num(0), Num(0)], + vec![Dash, Num(0), Num(0)], + vec![Plus, Num(0)], + vec![Dash, Num(0)], + vec![TzName("".to_string())], + vec![Zulu], + ]; + + let mut is_positive = true; + let mut hour_offset: Option = None; + let mut minute_offset: Option = None; + + for format in all_formats.iter() { + let actual = tokens.iter(); + + if actual.len() != format.len() { + continue; + } + + for (i, (atok, etok)) in actual.zip(format).enumerate() { + match (atok, etok) { + (Colon, Colon) | (Plus, Plus) => { /* Matching punctuation */ } + (Dash, Dash) => { + is_positive = false; + } + (Num(val), Num(_)) => { + let val = *val; + match (hour_offset, minute_offset) { + (None, None) => if val <= 24 { + hour_offset = Some(val as i64); + } else { + // We can return an error here because in all the formats with numbers + // we require the first number to be an hour and we require it to be <= 24 + return Err(ParserError::ParserError(format!( + "Error parsing timezone string ({}): timezone hour invalid {}", + value, val + ))); + } + (Some(_), None) => if val <= 60 { + minute_offset = Some(val as i64); + } else { + return Err(ParserError::ParserError(format!( + "Error parsing timezone string ({}): timezone minute invalid {}", + value, val + ))); + }, + // We've already seen an hour and a minute so we should never see another number + (Some(_), Some(_)) => return Err(ParserError::ParserError(format!( + "Error parsing timezone string ({}): invalid value {} at token index {}", value, + val, i + ))), + (None, Some(_)) => unreachable!("parsed a minute before an hour!"), + } + } + (Zulu, Zulu) => return Ok(0 as i64), + (TzName(val), TzName(_)) => { + // For now, we don't support named timezones + return Err(ParserError::ParserError(format!( + "Error parsing timezone string ({}): named timezones are not supported. \ + Failed to parse {} at token index {}", + value, val, i + ))); + } + (_, _) => { + // Theres a mismatch between this format and the actual token stream + // Stop trying to parse in this format and go to the next one + is_positive = true; + hour_offset = None; + minute_offset = None; + break; + } + } + } + + // Return the first valid parsed result + if let Some(hour_offset) = hour_offset { + let mut tz_offset_second: i64 = hour_offset * 60 * 60; + + if let Some(minute_offset) = minute_offset { + tz_offset_second += minute_offset * 60; + } + + if !is_positive { + tz_offset_second *= -1 + } + return Ok(tz_offset_second); + } + } + + return Err(ParserError::ParserError(format!("It didnt work"))); +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum IntervalToken { Dash, Space, Colon, Dot, + Plus, + Zulu, Num(u64), Nanos(u32), + // String representation of a named timezone e.g. 'EST' + TzName(String), } pub(crate) fn build_parsed_datetime( @@ -191,6 +385,55 @@ pub(crate) fn build_parsed_datetime( Ok(pdt) } +/// Takes a 'date timezone' 'date time timezone' string and splits +/// it into 'date {time}' and 'timezone' components +pub(crate) fn split_timestamp_string(value: &str) -> (&str, &str) { + // First we need to see if the string contains " +" or " -" because + // timestamps can come in a format YYYY-MM-DD {+|-} (where the + // timezone string can have colons) + let cut = value.find(" +").or_else(|| value.find(" -")); + + if let Some(cut) = cut { + let (first, second) = value.split_at(cut); + return (first.trim(), second.trim()); + } + + // If we have a hh:mm:dd component, we need to go past that to see if we can find a tz + let colon = value.find(':'); + + if let Some(colon) = colon { + let substring = value.get(colon..); + if let Some(substring) = substring { + let tz = substring + .find(|c: char| (c == '-') || (c == '+') || (c == ' ') || c.is_ascii_alphabetic()); + + if let Some(tz) = tz { + let (first, second) = value.split_at(colon + tz); + return (first.trim(), second.trim()); + } + } + + return (value.trim(), ""); + } else { + // We don't have a time, so the only formats available are + // YYY-mm-dd or YYYY-MM-dd + // Numeric offset timezones need to be separated from the ymd by a space + let cut = value.find(|c: char| (c == ' ') || c.is_ascii_alphabetic()); + + if let Some(cut) = cut { + let (first, second) = value.split_at(cut); + return (first.trim(), second.trim()); + } + + return (value.trim(), ""); + } +} + +pub(crate) fn parse_timezone_offset_second(value: &str) -> Result { + let toks = tokenize_timezone(value)?; + Ok(build_timezone_offset_second(&toks, value)?) +} + #[cfg(test)] mod test { use super::*; @@ -234,4 +477,170 @@ mod test { ] ); } + + #[test] + fn test_split_timestamp_string() { + let test_cases = [ + ( + "1969-06-01 10:10:10.410 UTC", + "1969-06-01 10:10:10.410", + "UTC", + ), + ( + "1969-06-01 10:10:10.410+4:00", + "1969-06-01 10:10:10.410", + "+4:00", + ), + ( + "1969-06-01 10:10:10.410-4:00", + "1969-06-01 10:10:10.410", + "-4:00", + ), + ("1969-06-01 10:10:10.410", "1969-06-01 10:10:10.410", ""), + ("1969-06-01 10:10:10.410+4", "1969-06-01 10:10:10.410", "+4"), + ("1969-06-01 10:10:10.410-4", "1969-06-01 10:10:10.410", "-4"), + ("1969-06-01 10:10:10+4:00", "1969-06-01 10:10:10", "+4:00"), + ("1969-06-01 10:10:10-4:00", "1969-06-01 10:10:10", "-4:00"), + ("1969-06-01 10:10:10 UTC", "1969-06-01 10:10:10", "UTC"), + ("1969-06-01 10:10:10", "1969-06-01 10:10:10", ""), + ("1969-06-01 10:10+4:00", "1969-06-01 10:10", "+4:00"), + ("1969-06-01 10:10-4:00", "1969-06-01 10:10", "-4:00"), + ("1969-06-01 10:10 UTC", "1969-06-01 10:10", "UTC"), + ("1969-06-01 10:10", "1969-06-01 10:10", ""), + ("1969-06-01 UTC", "1969-06-01", "UTC"), + ("1969-06-01 +4:00", "1969-06-01", "+4:00"), + ("1969-06-01 -4:00", "1969-06-01", "-4:00"), + ("1969-06-01 +4", "1969-06-01", "+4"), + ("1969-06-01 -4", "1969-06-01", "-4"), + ("1969-06-01", "1969-06-01", ""), + ("1969-06-01 10:10:10.410Z", "1969-06-01 10:10:10.410", "Z"), + ("1969-06-01 10:10:10.410z", "1969-06-01 10:10:10.410", "z"), + ("1969-06-01Z", "1969-06-01", "Z"), + ("1969-06-01z", "1969-06-01", "z"), + ("1969-06-01 10:10:10.410 ", "1969-06-01 10:10:10.410", ""), + ( + "1969-06-01 10:10:10.410 ", + "1969-06-01 10:10:10.410", + "", + ), + (" 1969-06-01 10:10:10.412", "1969-06-01 10:10:10.412", ""), + ( + " 1969-06-01 10:10:10.413 ", + "1969-06-01 10:10:10.413", + "", + ), + ( + "1969-06-01 10:10:10.410 +4:00", + "1969-06-01 10:10:10.410", + "+4:00", + ), + ( + "1969-06-01 10:10:10.410+4 :00", + "1969-06-01 10:10:10.410", + "+4 :00", + ), + ( + "1969-06-01 10:10:10.410 +4:00", + "1969-06-01 10:10:10.410", + "+4:00", + ), + ( + "1969-06-01 10:10:10.410+4:00 ", + "1969-06-01 10:10:10.410", + "+4:00", + ), + ( + "1969-06-01 10:10:10.410 Z ", + "1969-06-01 10:10:10.410", + "Z", + ), + ("1969-06-01 +4 ", "1969-06-01", "+4"), + ("1969-06-01 Z ", "1969-06-01", "Z"), + ]; + + for test in test_cases.iter() { + let (ts, tz) = split_timestamp_string(test.0); + + assert_eq!(ts, test.1); + assert_eq!(tz, test.2); + } + } + + #[test] + fn test_parse_timezone_offset_second() { + let test_cases = [ + ("+0:00", 0), + ("-0:00", 0), + ("+0:000000", 0), + ("+000000:00", 0), + ("+000000:000000", 0), + ("+0", 0), + ("+00", 0), + ("+000", 0), + ("+0000", 0), + ("+00000000", 0), + ("+0000001:000000", 3600), + ("+0000000:000001", 60), + ("+0000001:000001", 3660), + ("+4:00", 14400), + ("-4:00", -14400), + ("+2:30", 9000), + ("-5:15", -18900), + ("+0:20", 1200), + ("-0:20", -1200), + ("+5", 18000), + ("-5", -18000), + ("+05", 18000), + ("-05", -18000), + ("+500", 18000), + ("-500", -18000), + ("+530", 19800), + ("-530", -19800), + ("+050", 3000), + ("-050", -3000), + ("+15", 54000), + ("-15", -54000), + ("+1515", 54900), + ("+015", 900), + ("-015", -900), + ("+0015", 900), + ("-0015", -900), + ("+00015", 900), + ("-00015", -900), + ("+005", 300), + ("-005", -300), + ("+0000005", 300), + ("+00000100", 3600), + ("Z", 0), + ("z", 0), + ]; + + for test in test_cases.iter() { + match parse_timezone_offset_second(test.0) { + Ok(tz_offset) => { + let expected: i64 = test.1 as i64; + + println!("{} {}", expected, tz_offset); + assert_eq!(tz_offset, expected); + } + Err(e) => panic!( + "Test failed when expected to pass test case: {} error: {}", + test.0, e + ), + } + } + + let failure_test_cases = [ + "+25:00", "+120:00", "+0:61", "+0:500", " 12:30", "+-12:30", "+2525", "+2561", + "+255900", "+25", "+5::30", "+5:30:", "+5:30:16", "+5:", "++5:00", "--5:00", "UTC", + " UTC", "a", "zzz", "ZZZ", "ZZ Top", " +", " -", " ", "1", "12", "1234", + ]; + + for test in failure_test_cases.iter() { + match parse_timezone_offset_second(test) { + Ok(t) => panic!("Test passed when expected to fail test case: {} parsed tz offset (seconds): {}", test, t), + Err(e) => println!("{}", e), + } + } + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index d36eeb0..360e550 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -83,6 +83,11 @@ impl TestedDialects { self.one_statement_parses_to(query, query) } + /// Parse `sql` to a single [Statement] without checking for modifications + pub fn unverified_stmt(&self, query: &str) -> Statement { + self.one_statement_parses_to(query, "") + } + /// Ensures that `sql` parses as a single [Query], and is not modified /// after a serialization round-trip. pub fn verified_query(&self, sql: &str) -> Query { @@ -92,6 +97,15 @@ impl TestedDialects { } } + /// Ensures that `sql` parses as a single [Query] + /// Does not check for modifications + pub fn unverified_query(&self, sql: &str) -> Query { + match self.unverified_stmt(sql) { + Statement::Query(query) => *query, + _ => panic!("Expected Query"), + } + } + /// Ensures that `sql` parses as a single [Select], and is not modified /// after a serialization round-trip. pub fn verified_only_select(&self, query: &str) -> Select { @@ -101,6 +115,14 @@ impl TestedDialects { } } + /// Ensures that `sql` parses as a single [Select] + pub fn unverified_only_select(&self, query: &str) -> Select { + match self.unverified_query(query).body { + SetExpr::Select(s) => *s, + _ => panic!("Expected SetExpr::Select"), + } + } + /// Ensures that `sql` parses as an expression, and is not modified /// after a serialization round-trip. pub fn verified_expr(&self, sql: &str) -> Expr { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6a9641d..29737b0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1358,7 +1358,8 @@ fn parse_literal_timestamp() { hour: 1, minute: 23, second: 34, - nano: 0 + nano: 0, + timezone_offset_second: 0, } )), expr_from_projection(only(&select.projection)), @@ -1376,13 +1377,84 @@ fn parse_literal_timestamp() { hour: 1, minute: 23, second: 34, - nano: 555_000_000 + nano: 555_000_000, + timezone_offset_second: 0, } )), expr_from_projection(only(&select.projection)), ); } +#[test] +fn parse_literal_timestamptz() { + let time_formats = [ + "TIMESTAMP", + "TIMESTAMPTZ", + "TIMESTAMP WITH TIME ZONE", + "TIMESTAMP WITHOUT TIME ZONE", + ]; + + #[rustfmt::skip] + let test_cases = [("1999-01-01 01:23:34.555", 1999, 1, 1, 1, 23, 34, 555_000_000, 0), + ("1999-01-01 01:23:34.555+0:00", 1999, 1, 1, 1, 23, 34, 555_000_000, 0), + ("1999-01-01 01:23:34.555+0", 1999, 1, 1, 1, 23, 34, 555_000_000, 0), + ("1999-01-01 01:23:34.555z", 1999, 1, 1, 1, 23, 34, 555_000_000, 0), + ("1999-01-01 01:23:34.555Z", 1999, 1, 1, 1, 23, 34, 555_000_000, 0), + ("1999-01-01 01:23:34.555 z", 1999, 1, 1, 1, 23, 34, 555_000_000, 0), + ("1999-01-01 01:23:34.555 Z", 1999, 1, 1, 1, 23, 34, 555_000_000, 0), + ("1999-01-01 01:23:34.555+4:00", 1999, 1, 1, 1, 23, 34, 555_000_000, 14400), + ("1999-01-01 01:23:34.555-4:00", 1999, 1, 1, 1, 23, 34, 555_000_000, -14400), + ("1999-01-01 01:23:34.555+400", 1999, 1, 1, 1, 23, 34, 555_000_000, 14400), + ("1999-01-01 01:23:34.555+4", 1999, 1, 1, 1, 23, 34, 555_000_000, 14400), + ("1999-01-01 01:23:34.555+4:30", 1999, 1, 1, 1, 23, 34, 555_000_000, 16200), + ("1999-01-01 01:23:34.555+430", 1999, 1, 1, 1, 23, 34, 555_000_000, 16200), + ("1999-01-01 01:23:34.555+4:45", 1999, 1, 1, 1, 23, 34, 555_000_000, 17100), + ("1999-01-01 01:23:34.555+445", 1999, 1, 1, 1, 23, 34, 555_000_000, 17100), + ("1999-01-01 01:23:34.555+14:45", 1999, 1, 1, 1, 23, 34, 555_000_000, 53100), + ("1999-01-01 01:23:34.555-14:45", 1999, 1, 1, 1, 23, 34, 555_000_000, -53100), + ("1999-01-01 01:23:34.555+1445", 1999, 1, 1, 1, 23, 34, 555_000_000, 53100), + ("1999-01-01 01:23:34.555-1445", 1999, 1, 1, 1, 23, 34, 555_000_000, -53100), + ("1999-01-01 01:23:34.555 +14:45", 1999, 1, 1, 1, 23, 34, 555_000_000, 53100), + ("1999-01-01 01:23:34.555 -14:45", 1999, 1, 1, 1, 23, 34, 555_000_000, -53100), + ("1999-01-01 01:23:34.555 +1445", 1999, 1, 1, 1, 23, 34, 555_000_000, 53100), + ("1999-01-01 01:23:34.555 -1445", 1999, 1, 1, 1, 23, 34, 555_000_000, -53100), + ]; + + for test in test_cases.iter() { + for format in time_formats.iter() { + let sql = format!("SELECT {} '{}'", format, test.0); + println!("{}", sql); + let select = all_dialects().unverified_only_select(&sql); + + let mut pts = ParsedTimestamp { + year: test.1, + month: test.2, + day: test.3, + hour: test.4, + minute: test.5, + second: test.6, + nano: test.7, + timezone_offset_second: 0 as i64, + }; + + if *format == "TIMESTAMPTZ" || *format == "TIMESTAMP WITH TIME ZONE" { + pts.timezone_offset_second = test.8; + let value = Value::TimestampTz(test.0.into(), pts); + assert_eq!( + &Expr::Value(value), + expr_from_projection(only(&select.projection)) + ); + } else { + let value = Value::Timestamp(test.0.into(), pts); + assert_eq!( + &Expr::Value(value), + expr_from_projection(only(&select.projection)) + ); + } + } + } +} + #[test] fn parse_literal_interval_monthlike() { let mut iv = single_iv();