Skip to content

Commit 7cbbd91

Browse files
bitemyappChris Allen
andauthored
Add support for AT TIME ZONE (apache#539)
* Support for empty array literals * Added support for AT TIME ZONE Co-authored-by: Chris Allen <chrisa@indeed.com>
1 parent 4706d8b commit 7cbbd91

File tree

4 files changed

+130
-3
lines changed

4 files changed

+130
-3
lines changed

src/ast/mod.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@ pub enum Expr {
285285
expr: Box<Expr>,
286286
data_type: DataType,
287287
},
288+
/// AT a timestamp to a different timezone e.g. `FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00'`
289+
AtTimeZone {
290+
timestamp: Box<Expr>,
291+
time_zone: String,
292+
},
288293
/// EXTRACT(DateTimeField FROM <expr>)
289294
Extract {
290295
field: DateTimeField,
@@ -562,6 +567,12 @@ impl fmt::Display for Expr {
562567
Expr::CompositeAccess { expr, key } => {
563568
write!(f, "{}.{}", expr, key)
564569
}
570+
Expr::AtTimeZone {
571+
timestamp,
572+
time_zone,
573+
} => {
574+
write!(f, "{} AT TIME ZONE '{}'", timestamp, time_zone)
575+
}
565576
}
566577
}
567578
}

src/parser.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -883,9 +883,17 @@ impl<'a> Parser<'a> {
883883
/// Parses an array expression `[ex1, ex2, ..]`
884884
/// if `named` is `true`, came from an expression like `ARRAY[ex1, ex2]`
885885
pub fn parse_array_expr(&mut self, named: bool) -> Result<Expr, ParserError> {
886-
let exprs = self.parse_comma_separated(Parser::parse_expr)?;
887-
self.expect_token(&Token::RBracket)?;
888-
Ok(Expr::Array(Array { elem: exprs, named }))
886+
if self.peek_token() == Token::RBracket {
887+
let _ = self.next_token();
888+
Ok(Expr::Array(Array {
889+
elem: vec![],
890+
named,
891+
}))
892+
} else {
893+
let exprs = self.parse_comma_separated(Parser::parse_expr)?;
894+
self.expect_token(&Token::RBracket)?;
895+
Ok(Expr::Array(Array { elem: exprs, named }))
896+
}
889897
}
890898

891899
/// Parse a SQL LISTAGG expression, e.g. `LISTAGG(...) WITHIN GROUP (ORDER BY ...)`.
@@ -1205,6 +1213,28 @@ impl<'a> Parser<'a> {
12051213
)
12061214
}
12071215
}
1216+
Keyword::AT => {
1217+
// if self.parse_keyword(Keyword::TIME) {
1218+
// self.expect_keyword(Keyword::ZONE)?;
1219+
if self.parse_keywords(&[Keyword::TIME, Keyword::ZONE]) {
1220+
let time_zone = self.next_token();
1221+
match time_zone {
1222+
Token::SingleQuotedString(time_zone) => {
1223+
log::trace!("Peek token: {:?}", self.peek_token());
1224+
Ok(Expr::AtTimeZone {
1225+
timestamp: Box::new(expr),
1226+
time_zone,
1227+
})
1228+
}
1229+
tok => self.expected(
1230+
"Expected Token::SingleQuotedString after AT TIME ZONE",
1231+
tok,
1232+
),
1233+
}
1234+
} else {
1235+
self.expected("Expected Token::Word after AT", tok)
1236+
}
1237+
}
12081238
Keyword::NOT | Keyword::IN | Keyword::BETWEEN => {
12091239
self.prev_token();
12101240
let negated = self.parse_keyword(Keyword::NOT);
@@ -1350,15 +1380,32 @@ impl<'a> Parser<'a> {
13501380
const UNARY_NOT_PREC: u8 = 15;
13511381
const BETWEEN_PREC: u8 = 20;
13521382
const PLUS_MINUS_PREC: u8 = 30;
1383+
const TIME_ZONE_PREC: u8 = 20;
13531384

13541385
/// Get the precedence of the next token
13551386
pub fn get_next_precedence(&self) -> Result<u8, ParserError> {
13561387
let token = self.peek_token();
13571388
debug!("get_next_precedence() {:?}", token);
1389+
let token_0 = self.peek_nth_token(0);
1390+
let token_1 = self.peek_nth_token(1);
1391+
let token_2 = self.peek_nth_token(2);
1392+
debug!("0: {token_0} 1: {token_1} 2: {token_2}");
13581393
match token {
13591394
Token::Word(w) if w.keyword == Keyword::OR => Ok(5),
13601395
Token::Word(w) if w.keyword == Keyword::AND => Ok(10),
13611396
Token::Word(w) if w.keyword == Keyword::XOR => Ok(24),
1397+
1398+
Token::Word(w) if w.keyword == Keyword::AT => {
1399+
match (self.peek_nth_token(1), self.peek_nth_token(2)) {
1400+
(Token::Word(w), Token::Word(w2))
1401+
if w.keyword == Keyword::TIME && w2.keyword == Keyword::ZONE =>
1402+
{
1403+
Ok(Self::TIME_ZONE_PREC)
1404+
}
1405+
_ => Ok(0),
1406+
}
1407+
}
1408+
13621409
Token::Word(w) if w.keyword == Keyword::NOT => match self.peek_nth_token(1) {
13631410
// The precedence of NOT varies depending on keyword that
13641411
// follows it. If it is followed by IN, BETWEEN, or LIKE,

tests/sqlparser_common.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2871,6 +2871,65 @@ fn parse_literal_interval() {
28712871
);
28722872
}
28732873

2874+
#[test]
2875+
fn parse_at_timezone() {
2876+
let zero = Expr::Value(number("0"));
2877+
let sql = "SELECT FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00' FROM t";
2878+
let select = verified_only_select(sql);
2879+
assert_eq!(
2880+
&Expr::AtTimeZone {
2881+
timestamp: Box::new(Expr::Function(Function {
2882+
name: ObjectName(vec![Ident {
2883+
value: "FROM_UNIXTIME".to_string(),
2884+
quote_style: None
2885+
}]),
2886+
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero.clone()))],
2887+
over: None,
2888+
distinct: false
2889+
})),
2890+
time_zone: "UTC-06:00".to_string()
2891+
},
2892+
expr_from_projection(only(&select.projection)),
2893+
);
2894+
2895+
let sql = r#"SELECT DATE_FORMAT(FROM_UNIXTIME(0) AT TIME ZONE 'UTC-06:00', '%Y-%m-%dT%H') AS "hour" FROM t"#;
2896+
let select = verified_only_select(sql);
2897+
assert_eq!(
2898+
&SelectItem::ExprWithAlias {
2899+
expr: Expr::Function(Function {
2900+
name: ObjectName(vec![Ident {
2901+
value: "DATE_FORMAT".to_string(),
2902+
quote_style: None,
2903+
},],),
2904+
args: vec![
2905+
FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::AtTimeZone {
2906+
timestamp: Box::new(Expr::Function(Function {
2907+
name: ObjectName(vec![Ident {
2908+
value: "FROM_UNIXTIME".to_string(),
2909+
quote_style: None,
2910+
},],),
2911+
args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(zero,),),],
2912+
over: None,
2913+
distinct: false,
2914+
},)),
2915+
time_zone: "UTC-06:00".to_string(),
2916+
},),),
2917+
FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value(
2918+
Value::SingleQuotedString("%Y-%m-%dT%H".to_string(),),
2919+
),),),
2920+
],
2921+
over: None,
2922+
distinct: false,
2923+
},),
2924+
alias: Ident {
2925+
value: "hour".to_string(),
2926+
quote_style: Some('"',),
2927+
},
2928+
},
2929+
only(&select.projection),
2930+
);
2931+
}
2932+
28742933
#[test]
28752934
fn parse_simple_math_expr_plus() {
28762935
let sql = "SELECT a + b, 2 + a, 2.5 + a, a_f + b_f, 2 + a_f, 2.5 + a_f FROM c";

tests/sqlparser_postgres.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,16 @@ fn parse_array_index_expr() {
12281228
},
12291229
expr_from_projection(only(&select.projection)),
12301230
);
1231+
1232+
let sql = "SELECT ARRAY[]";
1233+
let select = pg_and_generic().verified_only_select(sql);
1234+
assert_eq!(
1235+
&Expr::Array(sqlparser::ast::Array {
1236+
elem: vec![],
1237+
named: true
1238+
}),
1239+
expr_from_projection(only(&select.projection)),
1240+
);
12311241
}
12321242

12331243
#[test]

0 commit comments

Comments
 (0)