Skip to content

Commit 604e6f0

Browse files
committed
asof joins
1 parent 375742d commit 604e6f0

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
@@ -1562,6 +1562,15 @@ impl fmt::Display for Join {
15621562
),
15631563
JoinOperator::CrossApply => write!(f, " CROSS APPLY {}", self.relation),
15641564
JoinOperator::OuterApply => write!(f, " OUTER APPLY {}", self.relation),
1565+
JoinOperator::AsOf {
1566+
match_condition,
1567+
constraint,
1568+
} => write!(
1569+
f,
1570+
" ASOF JOIN {} MATCH_CONDITION ({match_condition}){}",
1571+
self.relation,
1572+
suffix(constraint)
1573+
),
15651574
}
15661575
}
15671576
}
@@ -1587,6 +1596,14 @@ pub enum JoinOperator {
15871596
CrossApply,
15881597
/// OUTER APPLY (non-standard)
15891598
OuterApply,
1599+
/// `ASOF` joins are used for joining tables containing time-series data
1600+
/// whose timestamp columns do not match exactly.
1601+
///
1602+
/// See <https://docs.snowflake.com/en/sql-reference/constructs/asof-join>.
1603+
AsOf {
1604+
match_condition: Expr,
1605+
constraint: JoinConstraint,
1606+
},
15901607
}
15911608

15921609
#[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
@@ -8505,6 +8515,18 @@ impl<'a> Parser<'a> {
85058515
relation: self.parse_table_factor()?,
85068516
join_operator: JoinOperator::OuterApply,
85078517
}
8518+
} else if self.parse_keyword(Keyword::ASOF) {
8519+
self.expect_keyword(Keyword::JOIN)?;
8520+
let relation = self.parse_table_factor()?;
8521+
self.expect_keyword(Keyword::MATCH_CONDITION)?;
8522+
let match_condition = self.parse_parenthesized(Self::parse_expr)?;
8523+
Join {
8524+
relation,
8525+
join_operator: JoinOperator::AsOf {
8526+
match_condition,
8527+
constraint: self.parse_join_constraint(false)?,
8528+
},
8529+
}
85088530
} else {
85098531
let natural = self.parse_keyword(Keyword::NATURAL);
85108532
let peek_keyword = if let Token::Word(w) = self.peek_token().token {
@@ -8951,9 +8973,7 @@ impl<'a> Parser<'a> {
89518973
};
89528974

89538975
self.expect_keyword(Keyword::PATTERN)?;
8954-
self.expect_token(&Token::LParen)?;
8955-
let pattern = self.parse_pattern()?;
8956-
self.expect_token(&Token::RParen)?;
8976+
let pattern = self.parse_parenthesized(Self::parse_pattern)?;
89578977

89588978
self.expect_keyword(Keyword::DEFINE)?;
89598979

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
@@ -1688,3 +1688,70 @@ fn test_pivot() {
16881688
"ORDER BY region",
16891689
));
16901690
}
1691+
1692+
#[test]
1693+
fn asof_joins() {
1694+
#[rustfmt::skip]
1695+
let query = snowflake_and_generic().verified_only_select(concat!(
1696+
"SELECT * ",
1697+
"FROM trades_unixtime AS tu ",
1698+
"ASOF JOIN quotes_unixtime AS qu ",
1699+
"MATCH_CONDITION (tu.trade_time >= qu.quote_time)",
1700+
));
1701+
1702+
assert_eq!(
1703+
query.from[0],
1704+
TableWithJoins {
1705+
relation: table_with_alias("trades_unixtime", "tu"),
1706+
joins: vec![Join {
1707+
relation: table_with_alias("quotes_unixtime", "qu"),
1708+
join_operator: JoinOperator::AsOf {
1709+
match_condition: Expr::BinaryOp {
1710+
left: Box::new(Expr::CompoundIdentifier(vec![
1711+
Ident::new("tu"),
1712+
Ident::new("trade_time"),
1713+
])),
1714+
op: BinaryOperator::GtEq,
1715+
right: Box::new(Expr::CompoundIdentifier(vec![
1716+
Ident::new("qu"),
1717+
Ident::new("quote_time"),
1718+
])),
1719+
},
1720+
constraint: JoinConstraint::None,
1721+
},
1722+
}],
1723+
}
1724+
);
1725+
1726+
#[rustfmt::skip]
1727+
snowflake_and_generic().verified_query(concat!(
1728+
"SELECT t.stock_symbol, t.trade_time, t.quantity, q.quote_time, q.price ",
1729+
"FROM trades AS t ASOF JOIN quotes AS q ",
1730+
"MATCH_CONDITION (t.trade_time >= quote_time) ",
1731+
"ON t.stock_symbol = q.stock_symbol ",
1732+
"ORDER BY t.stock_symbol",
1733+
));
1734+
1735+
#[rustfmt::skip]
1736+
snowflake_and_generic().verified_query(concat!(
1737+
"SELECT t.stock_symbol, c.company_name, t.trade_time, t.quantity, q.quote_time, q.price ",
1738+
"FROM trades AS t ASOF JOIN quotes AS q ",
1739+
"MATCH_CONDITION (t.trade_time <= quote_time) ",
1740+
"USING(stock_symbol) ",
1741+
"JOIN companies AS c ON c.stock_symbol = t.stock_symbol ",
1742+
"ORDER BY t.stock_symbol",
1743+
));
1744+
1745+
#[rustfmt::skip]
1746+
snowflake_and_generic().verified_query(concat!(
1747+
"SELECT * ",
1748+
"FROM snowtime AS s ",
1749+
"ASOF JOIN raintime AS r ",
1750+
"MATCH_CONDITION (s.observed >= r.observed) ",
1751+
"ON s.state = r.state ",
1752+
"ASOF JOIN preciptime AS p ",
1753+
"MATCH_CONDITION (s.observed >= p.observed) ",
1754+
"ON s.state = p.state ",
1755+
"ORDER BY s.observed",
1756+
));
1757+
}

0 commit comments

Comments
 (0)