Skip to content

Commit d7c071f

Browse files
committed
asof joins
1 parent d5faf3c commit d7c071f

File tree

5 files changed

+123
-3
lines changed

5 files changed

+123
-3
lines changed

src/ast/query.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,15 @@ impl fmt::Display for Join {
15231523
),
15241524
JoinOperator::CrossApply => write!(f, " CROSS APPLY {}", self.relation),
15251525
JoinOperator::OuterApply => write!(f, " OUTER APPLY {}", self.relation),
1526+
JoinOperator::AsOf {
1527+
match_condition,
1528+
constraint,
1529+
} => write!(
1530+
f,
1531+
" ASOF JOIN {} MATCH_CONDITION ({match_condition}){}",
1532+
self.relation,
1533+
suffix(constraint)
1534+
),
15261535
}
15271536
}
15281537
}
@@ -1548,6 +1557,14 @@ pub enum JoinOperator {
15481557
CrossApply,
15491558
/// OUTER APPLY (non-standard)
15501559
OuterApply,
1560+
/// `ASOF` joins are used for joining tables containing time-series data
1561+
/// whose timestamp columns do not match exactly.
1562+
///
1563+
/// See <https://docs.snowflake.com/en/sql-reference/constructs/asof-join>.
1564+
AsOf {
1565+
match_condition: Expr,
1566+
constraint: JoinConstraint,
1567+
},
15511568
}
15521569

15531570
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]

src/keywords.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ define_keywords!(
9191
AS,
9292
ASC,
9393
ASENSITIVE,
94+
ASOF,
9495
ASSERT,
9596
ASYMMETRIC,
9697
AT,
@@ -418,6 +419,7 @@ define_keywords!(
418419
MATCH,
419420
MATCHED,
420421
MATCHES,
422+
MATCH_CONDITION,
421423
MATCH_RECOGNIZE,
422424
MATERIALIZED,
423425
MAX,

src/parser/mod.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3196,6 +3196,16 @@ impl<'a> Parser<'a> {
31963196
Ok(values)
31973197
}
31983198

3199+
pub fn parse_parenthesized<T, F>(&mut self, mut f: F) -> Result<T, ParserError>
3200+
where
3201+
F: FnMut(&mut Parser<'a>) -> Result<T, ParserError>,
3202+
{
3203+
self.expect_token(&Token::LParen)?;
3204+
let res = f(self)?;
3205+
self.expect_token(&Token::RParen)?;
3206+
Ok(res)
3207+
}
3208+
31993209
/// Parse a comma-separated list of 0+ items accepted by `F`
32003210
pub fn parse_comma_separated0<T, F>(&mut self, f: F) -> Result<Vec<T>, ParserError>
32013211
where
@@ -8336,6 +8346,18 @@ impl<'a> Parser<'a> {
83368346
relation: self.parse_table_factor()?,
83378347
join_operator: JoinOperator::OuterApply,
83388348
}
8349+
} else if self.parse_keyword(Keyword::ASOF) {
8350+
self.expect_keyword(Keyword::JOIN)?;
8351+
let relation = self.parse_table_factor()?;
8352+
self.expect_keyword(Keyword::MATCH_CONDITION)?;
8353+
let match_condition = self.parse_parenthesized(Self::parse_expr)?;
8354+
Join {
8355+
relation,
8356+
join_operator: JoinOperator::AsOf {
8357+
match_condition,
8358+
constraint: self.parse_join_constraint(false)?,
8359+
},
8360+
}
83398361
} else {
83408362
let natural = self.parse_keyword(Keyword::NATURAL);
83418363
let peek_keyword = if let Token::Word(w) = self.peek_token().token {
@@ -8782,9 +8804,7 @@ impl<'a> Parser<'a> {
87828804
};
87838805

87848806
self.expect_keyword(Keyword::PATTERN)?;
8785-
self.expect_token(&Token::LParen)?;
8786-
let pattern = self.parse_pattern()?;
8787-
self.expect_token(&Token::RParen)?;
8807+
let pattern = self.parse_parenthesized(Self::parse_pattern)?;
87888808

87898809
self.expect_keyword(Keyword::DEFINE)?;
87908810

src/test_utils.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,20 @@ pub fn table(name: impl Into<String>) -> TableFactor {
312312
}
313313
}
314314

315+
pub fn table_with_alias(name: impl Into<String>, alias: impl Into<String>) -> TableFactor {
316+
TableFactor::Table {
317+
name: ObjectName(vec![Ident::new(name)]),
318+
alias: Some(TableAlias {
319+
name: Ident::new(alias),
320+
columns: vec![],
321+
}),
322+
args: None,
323+
with_hints: vec![],
324+
version: None,
325+
partitions: vec![],
326+
}
327+
}
328+
315329
pub fn join(relation: TableFactor) -> Join {
316330
Join {
317331
relation,

tests/sqlparser_snowflake.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,3 +1585,70 @@ fn first_value_ignore_nulls() {
15851585
"FROM some_table"
15861586
));
15871587
}
1588+
1589+
#[test]
1590+
fn asof_joins() {
1591+
#[rustfmt::skip]
1592+
let query = snowflake_and_generic().verified_only_select(concat!(
1593+
"SELECT * ",
1594+
"FROM trades_unixtime AS tu ",
1595+
"ASOF JOIN quotes_unixtime AS qu ",
1596+
"MATCH_CONDITION (tu.trade_time >= qu.quote_time)",
1597+
));
1598+
1599+
assert_eq!(
1600+
query.from[0],
1601+
TableWithJoins {
1602+
relation: table_with_alias("trades_unixtime", "tu"),
1603+
joins: vec![Join {
1604+
relation: table_with_alias("quotes_unixtime", "qu"),
1605+
join_operator: JoinOperator::AsOf {
1606+
match_condition: Expr::BinaryOp {
1607+
left: Box::new(Expr::CompoundIdentifier(vec![
1608+
Ident::new("tu"),
1609+
Ident::new("trade_time"),
1610+
])),
1611+
op: BinaryOperator::GtEq,
1612+
right: Box::new(Expr::CompoundIdentifier(vec![
1613+
Ident::new("qu"),
1614+
Ident::new("quote_time"),
1615+
])),
1616+
},
1617+
constraint: JoinConstraint::None,
1618+
},
1619+
}],
1620+
}
1621+
);
1622+
1623+
#[rustfmt::skip]
1624+
snowflake_and_generic().verified_query(concat!(
1625+
"SELECT t.stock_symbol, t.trade_time, t.quantity, q.quote_time, q.price ",
1626+
"FROM trades AS t ASOF JOIN quotes AS q ",
1627+
"MATCH_CONDITION (t.trade_time >= quote_time) ",
1628+
"ON t.stock_symbol = q.stock_symbol ",
1629+
"ORDER BY t.stock_symbol",
1630+
));
1631+
1632+
#[rustfmt::skip]
1633+
snowflake_and_generic().verified_query(concat!(
1634+
"SELECT t.stock_symbol, c.company_name, t.trade_time, t.quantity, q.quote_time, q.price ",
1635+
"FROM trades AS t ASOF JOIN quotes AS q ",
1636+
"MATCH_CONDITION (t.trade_time <= quote_time) ",
1637+
"USING(stock_symbol) ",
1638+
"JOIN companies AS c ON c.stock_symbol = t.stock_symbol ",
1639+
"ORDER BY t.stock_symbol",
1640+
));
1641+
1642+
#[rustfmt::skip]
1643+
snowflake_and_generic().verified_query(concat!(
1644+
"SELECT * ",
1645+
"FROM snowtime AS s ",
1646+
"ASOF JOIN raintime AS r ",
1647+
"MATCH_CONDITION (s.observed >= r.observed) ",
1648+
"ON s.state = r.state ",
1649+
"ASOF JOIN preciptime AS p ",
1650+
"MATCH_CONDITION (s.observed >= p.observed) ",
1651+
"ON s.state = p.state ",
1652+
"ORDER BY s.observed",
1653+
));
1654+
}

0 commit comments

Comments
 (0)