Skip to content

Commit d5faf3c

Browse files
authored
Support expression in AT TIME ZONE and fix precedence (#1272)
1 parent 9d15f7e commit d5faf3c

File tree

4 files changed

+65
-53
lines changed

4 files changed

+65
-53
lines changed

src/ast/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ pub enum Expr {
584584
/// AT a timestamp to a different timezone e.g. `FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00'`
585585
AtTimeZone {
586586
timestamp: Box<Expr>,
587-
time_zone: String,
587+
time_zone: Box<Expr>,
588588
},
589589
/// Extract a field from a timestamp e.g. `EXTRACT(MONTH FROM foo)`
590590
///
@@ -1270,7 +1270,7 @@ impl fmt::Display for Expr {
12701270
timestamp,
12711271
time_zone,
12721272
} => {
1273-
write!(f, "{timestamp} AT TIME ZONE '{time_zone}'")
1273+
write!(f, "{timestamp} AT TIME ZONE {time_zone}")
12741274
}
12751275
Expr::Interval(interval) => {
12761276
write!(f, "{interval}")

src/parser/mod.rs

+14-48
Original file line numberDiff line numberDiff line change
@@ -2469,26 +2469,11 @@ impl<'a> Parser<'a> {
24692469
}
24702470
}
24712471
Keyword::AT => {
2472-
// if self.parse_keyword(Keyword::TIME) {
2473-
// self.expect_keyword(Keyword::ZONE)?;
2474-
if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) {
2475-
let time_zone = self.next_token();
2476-
match time_zone.token {
2477-
Token::SingleQuotedString(time_zone) => {
2478-
log::trace!("Peek token: {:?}", self.peek_token());
2479-
Ok(Expr::AtTimeZone {
2480-
timestamp: Box::new(expr),
2481-
time_zone,
2482-
})
2483-
}
2484-
_ => self.expected(
2485-
"Expected Token::SingleQuotedString after AT TIME ZONE",
2486-
time_zone,
2487-
),
2488-
}
2489-
} else {
2490-
self.expected("Expected Token::Word after AT", tok)
2491-
}
2472+
self.expect_keywords(&[Keyword::TIME, Keyword::ZONE])?;
2473+
Ok(Expr::AtTimeZone {
2474+
timestamp: Box::new(expr),
2475+
time_zone: Box::new(self.parse_subexpr(precedence)?),
2476+
})
24922477
}
24932478
Keyword::NOT
24942479
| Keyword::IN
@@ -2545,35 +2530,12 @@ impl<'a> Parser<'a> {
25452530
),
25462531
}
25472532
} else if Token::DoubleColon == tok {
2548-
let data_type = self.parse_data_type()?;
2549-
2550-
let cast_expr = Expr::Cast {
2533+
Ok(Expr::Cast {
25512534
kind: CastKind::DoubleColon,
25522535
expr: Box::new(expr),
2553-
data_type: data_type.clone(),
2536+
data_type: self.parse_data_type()?,
25542537
format: None,
2555-
};
2556-
2557-
match data_type {
2558-
DataType::Date
2559-
| DataType::Datetime(_)
2560-
| DataType::Timestamp(_, _)
2561-
| DataType::Time(_, _) => {
2562-
let value = self.parse_optional_time_zone()?;
2563-
match value {
2564-
Some(Value::SingleQuotedString(tz)) => Ok(Expr::AtTimeZone {
2565-
timestamp: Box::new(cast_expr),
2566-
time_zone: tz,
2567-
}),
2568-
None => Ok(cast_expr),
2569-
_ => Err(ParserError::ParserError(format!(
2570-
"Expected Token::SingleQuotedString after AT TIME ZONE, but found: {}",
2571-
value.unwrap()
2572-
))),
2573-
}
2574-
}
2575-
_ => Ok(cast_expr),
2576-
}
2538+
})
25772539
} else if Token::ExclamationMark == tok {
25782540
// PostgreSQL factorial operation
25792541
Ok(Expr::UnaryOp {
@@ -2784,10 +2746,14 @@ impl<'a> Parser<'a> {
27842746

27852747
// use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference
27862748
// higher number = higher precedence
2749+
//
2750+
// NOTE: The pg documentation is incomplete, e.g. the AT TIME ZONE operator
2751+
// actually has higher precedence than addition.
2752+
// See https://postgrespro.com/list/thread-id/2673331.
2753+
const AT_TZ_PREC: u8 = 41;
27872754
const MUL_DIV_MOD_OP_PREC: u8 = 40;
27882755
const PLUS_MINUS_PREC: u8 = 30;
27892756
const XOR_PREC: u8 = 24;
2790-
const TIME_ZONE_PREC: u8 = 20;
27912757
const BETWEEN_PREC: u8 = 20;
27922758
const LIKE_PREC: u8 = 19;
27932759
const IS_PREC: u8 = 17;
@@ -2817,7 +2783,7 @@ impl<'a> Parser<'a> {
28172783
(Token::Word(w), Token::Word(w2))
28182784
if w.keyword == Keyword::TIME && w2.keyword == Keyword::ZONE =>
28192785
{
2820-
Ok(Self::TIME_ZONE_PREC)
2786+
Ok(Self::AT_TZ_PREC)
28212787
}
28222788
_ => Ok(0),
28232789
}

tests/sqlparser_common.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -4995,7 +4995,9 @@ fn parse_at_timezone() {
49954995
assert_eq!(
49964996
&Expr::AtTimeZone {
49974997
timestamp: Box::new(call("FROM_UNIXTIME", [zero.clone()])),
4998-
time_zone: "UTC-06:00".to_string(),
4998+
time_zone: Box::new(Expr::Value(Value::SingleQuotedString(
4999+
"UTC-06:00".to_string()
5000+
))),
49995001
},
50005002
expr_from_projection(only(&select.projection)),
50015003
);
@@ -5009,7 +5011,9 @@ fn parse_at_timezone() {
50095011
[
50105012
Expr::AtTimeZone {
50115013
timestamp: Box::new(call("FROM_UNIXTIME", [zero])),
5012-
time_zone: "UTC-06:00".to_string(),
5014+
time_zone: Box::new(Expr::Value(Value::SingleQuotedString(
5015+
"UTC-06:00".to_string()
5016+
))),
50135017
},
50145018
Expr::Value(Value::SingleQuotedString("%Y-%m-%dT%H".to_string()),)
50155019
]
@@ -7037,7 +7041,9 @@ fn parse_double_colon_cast_at_timezone() {
70377041
data_type: DataType::Timestamp(None, TimezoneInfo::None),
70387042
format: None
70397043
}),
7040-
time_zone: "Europe/Brussels".to_string()
7044+
time_zone: Box::new(Expr::Value(Value::SingleQuotedString(
7045+
"Europe/Brussels".to_string()
7046+
))),
70417047
},
70427048
expr_from_projection(only(&select.projection)),
70437049
);

tests/sqlparser_postgres.rs

+40
Original file line numberDiff line numberDiff line change
@@ -3882,3 +3882,43 @@ fn parse_mat_cte() {
38823882
let sql2 = r#"WITH cte AS NOT MATERIALIZED (SELECT id FROM accounts) SELECT id FROM cte"#;
38833883
pg().verified_stmt(sql2);
38843884
}
3885+
3886+
#[test]
3887+
fn parse_at_time_zone() {
3888+
pg_and_generic().verified_expr("CURRENT_TIMESTAMP AT TIME ZONE tz");
3889+
pg_and_generic().verified_expr("CURRENT_TIMESTAMP AT TIME ZONE ('America/' || 'Los_Angeles')");
3890+
3891+
// check precedence
3892+
let expr = Expr::BinaryOp {
3893+
left: Box::new(Expr::AtTimeZone {
3894+
timestamp: Box::new(Expr::TypedString {
3895+
data_type: DataType::Timestamp(None, TimezoneInfo::None),
3896+
value: "2001-09-28 01:00".to_owned(),
3897+
}),
3898+
time_zone: Box::new(Expr::Cast {
3899+
kind: CastKind::DoubleColon,
3900+
expr: Box::new(Expr::Value(Value::SingleQuotedString(
3901+
"America/Los_Angeles".to_owned(),
3902+
))),
3903+
data_type: DataType::Text,
3904+
format: None,
3905+
}),
3906+
}),
3907+
op: BinaryOperator::Plus,
3908+
right: Box::new(Expr::Interval(Interval {
3909+
value: Box::new(Expr::Value(Value::SingleQuotedString(
3910+
"23 hours".to_owned(),
3911+
))),
3912+
leading_field: None,
3913+
leading_precision: None,
3914+
last_field: None,
3915+
fractional_seconds_precision: None,
3916+
})),
3917+
};
3918+
pretty_assertions::assert_eq!(
3919+
pg_and_generic().verified_expr(
3920+
"TIMESTAMP '2001-09-28 01:00' AT TIME ZONE 'America/Los_Angeles'::TEXT + INTERVAL '23 hours'",
3921+
),
3922+
expr
3923+
);
3924+
}

0 commit comments

Comments
 (0)