Skip to content

Commit 22f5c7f

Browse files
authored
Merge pull request #26 from Embucket/snowflake-directed-join
feat: parse Snowflake directed joins
2 parents ff0378d + 707457f commit 22f5c7f

13 files changed

Lines changed: 114 additions & 11 deletions

‎src/ast/query.rs‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2655,6 +2655,8 @@ pub struct Join {
26552655
/// ClickHouse supports the optional `GLOBAL` keyword before the join operator.
26562656
/// See [ClickHouse](https://clickhouse.com/docs/en/sql-reference/statements/select/join)
26572657
pub global: bool,
2658+
/// Snowflake `DIRECTED` join-order directive.
2659+
pub directed: bool,
26582660
/// The join operator and its constraint (INNER/LEFT/RIGHT/CROSS/ASOF/etc.).
26592661
pub join_operator: JoinOperator,
26602662
}
@@ -2685,52 +2687,53 @@ impl fmt::Display for Join {
26852687
if self.global {
26862688
write!(f, "GLOBAL ")?;
26872689
}
2690+
let directed = if self.directed { "DIRECTED " } else { "" };
26882691

26892692
match &self.join_operator {
26902693
JoinOperator::Join(constraint) => f.write_fmt(format_args!(
2691-
"{}JOIN {}{}",
2694+
"{}{directed}JOIN {}{}",
26922695
prefix(constraint),
26932696
self.relation,
26942697
suffix(constraint)
26952698
)),
26962699
JoinOperator::Inner(constraint) => f.write_fmt(format_args!(
2697-
"{}INNER JOIN {}{}",
2700+
"{}INNER {directed}JOIN {}{}",
26982701
prefix(constraint),
26992702
self.relation,
27002703
suffix(constraint)
27012704
)),
27022705
JoinOperator::Left(constraint) => f.write_fmt(format_args!(
2703-
"{}LEFT JOIN {}{}",
2706+
"{}LEFT {directed}JOIN {}{}",
27042707
prefix(constraint),
27052708
self.relation,
27062709
suffix(constraint)
27072710
)),
27082711
JoinOperator::LeftOuter(constraint) => f.write_fmt(format_args!(
2709-
"{}LEFT OUTER JOIN {}{}",
2712+
"{}LEFT OUTER {directed}JOIN {}{}",
27102713
prefix(constraint),
27112714
self.relation,
27122715
suffix(constraint)
27132716
)),
27142717
JoinOperator::Right(constraint) => f.write_fmt(format_args!(
2715-
"{}RIGHT JOIN {}{}",
2718+
"{}RIGHT {directed}JOIN {}{}",
27162719
prefix(constraint),
27172720
self.relation,
27182721
suffix(constraint)
27192722
)),
27202723
JoinOperator::RightOuter(constraint) => f.write_fmt(format_args!(
2721-
"{}RIGHT OUTER JOIN {}{}",
2724+
"{}RIGHT OUTER {directed}JOIN {}{}",
27222725
prefix(constraint),
27232726
self.relation,
27242727
suffix(constraint)
27252728
)),
27262729
JoinOperator::FullOuter(constraint) => f.write_fmt(format_args!(
2727-
"{}FULL JOIN {}{}",
2730+
"{}FULL {directed}JOIN {}{}",
27282731
prefix(constraint),
27292732
self.relation,
27302733
suffix(constraint)
27312734
)),
27322735
JoinOperator::CrossJoin(constraint) => f.write_fmt(format_args!(
2733-
"CROSS JOIN {}{}",
2736+
"CROSS {directed}JOIN {}{}",
27342737
self.relation,
27352738
suffix(constraint)
27362739
)),

‎src/ast/spans.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2220,7 +2220,8 @@ impl Spanned for Join {
22202220
fn span(&self) -> Span {
22212221
let Join {
22222222
relation,
2223-
global: _, // bool
2223+
global: _, // bool
2224+
directed: _, // bool
22242225
join_operator,
22252226
} = self;
22262227

‎src/dialect/mod.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,11 @@ pub trait Dialect: Debug + Any {
433433
false
434434
}
435435

436+
/// Returns true if the dialect supports Snowflake-style `DIRECTED` joins.
437+
fn supports_directed_join(&self) -> bool {
438+
false
439+
}
440+
436441
/// Returns true if the dialect supports CONNECT BY.
437442
fn supports_connect_by(&self) -> bool {
438443
false

‎src/dialect/snowflake.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@ impl Dialect for SnowflakeDialect {
483483
false
484484
}
485485

486+
fn supports_directed_join(&self) -> bool {
487+
true
488+
}
489+
486490
fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
487491
// Unreserve some keywords that Snowflake accepts as identifiers
488492
// See: https://docs.snowflake.com/en/sql-reference/reserved-keywords
@@ -589,6 +593,8 @@ impl Dialect for SnowflakeDialect {
589593

590594
Keyword::GLOBAL if parser.peek_keyword(Keyword::FULL) => false,
591595

596+
Keyword::DIRECTED if parser.peek_keyword(Keyword::JOIN) => false,
597+
592598
// Reserved keywords by the Snowflake dialect, which seem to be less strictive
593599
// than what is listed in `keywords::RESERVED_FOR_TABLE_ALIAS`. The following
594600
// keywords were tested with the this statement: `SELECT <KW>.* FROM tbl <KW>`.

‎src/keywords.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ define_keywords!(
336336
DETAIL,
337337
DETERMINISTIC,
338338
DIMENSIONS,
339+
DIRECTED,
339340
DIRECTORY,
340341
DISABLE,
341342
DISCARD,

‎src/parser/mod.rs‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15784,9 +15784,11 @@ impl<'a> Parser<'a> {
1578415784
loop {
1578515785
let global = self.parse_keyword(Keyword::GLOBAL);
1578615786
let join = if self.parse_keyword(Keyword::CROSS) {
15787+
let directed =
15788+
self.dialect.supports_directed_join() && self.parse_keyword(Keyword::DIRECTED);
1578715789
let join_operator = if self.parse_keyword(Keyword::JOIN) {
1578815790
JoinOperator::CrossJoin(JoinConstraint::None)
15789-
} else if self.parse_keyword(Keyword::APPLY) {
15791+
} else if !directed && self.parse_keyword(Keyword::APPLY) {
1579015792
// MSSQL extension, similar to CROSS JOIN LATERAL
1579115793
JoinOperator::CrossApply
1579215794
} else {
@@ -15804,6 +15806,7 @@ impl<'a> Parser<'a> {
1580415806
Join {
1580515807
relation,
1580615808
global,
15809+
directed,
1580715810
join_operator,
1580815811
}
1580915812
} else if self.parse_keyword(Keyword::OUTER) {
@@ -15812,6 +15815,7 @@ impl<'a> Parser<'a> {
1581215815
Join {
1581315816
relation: self.parse_table_factor()?,
1581415817
global,
15818+
directed: false,
1581515819
join_operator: JoinOperator::OuterApply,
1581615820
}
1581715821
} else if self.parse_keyword(Keyword::ASOF) {
@@ -15822,6 +15826,7 @@ impl<'a> Parser<'a> {
1582215826
Join {
1582315827
relation,
1582415828
global,
15829+
directed: false,
1582515830
join_operator: JoinOperator::AsOf {
1582615831
match_condition,
1582715832
constraint: self.parse_join_constraint(false)?,
@@ -15834,6 +15839,7 @@ impl<'a> Parser<'a> {
1583415839
Join {
1583515840
relation: self.parse_table_factor()?,
1583615841
global,
15842+
directed: false,
1583715843
join_operator: JoinOperator::InnerArrayJoin,
1583815844
}
1583915845
} else if self.dialect.supports_array_join_syntax()
@@ -15843,6 +15849,7 @@ impl<'a> Parser<'a> {
1584315849
Join {
1584415850
relation: self.parse_table_factor()?,
1584515851
global,
15852+
directed: false,
1584615853
join_operator: JoinOperator::LeftArrayJoin,
1584715854
}
1584815855
} else if self.dialect.supports_array_join_syntax()
@@ -15852,10 +15859,12 @@ impl<'a> Parser<'a> {
1585215859
Join {
1585315860
relation: self.parse_table_factor()?,
1585415861
global,
15862+
directed: false,
1585515863
join_operator: JoinOperator::ArrayJoin,
1585615864
}
1585715865
} else {
1585815866
let natural = self.parse_keyword(Keyword::NATURAL);
15867+
let mut directed = false;
1585915868
let peek_keyword = if let Token::Word(w) = &self.peek_token_ref().token {
1586015869
w.keyword
1586115870
} else {
@@ -15865,6 +15874,10 @@ impl<'a> Parser<'a> {
1586515874
let join_operator_type = match peek_keyword {
1586615875
Keyword::INNER | Keyword::JOIN => {
1586715876
let inner = self.parse_keyword(Keyword::INNER); // [ INNER ]
15877+
if inner {
15878+
directed = self.dialect.supports_directed_join()
15879+
&& self.parse_keyword(Keyword::DIRECTED);
15880+
}
1586815881
self.expect_keyword_is(Keyword::JOIN)?;
1586915882
if inner {
1587015883
JoinOperator::Inner
@@ -15879,10 +15892,13 @@ impl<'a> Parser<'a> {
1587915892
Keyword::OUTER,
1588015893
Keyword::SEMI,
1588115894
Keyword::ANTI,
15895+
Keyword::DIRECTED,
1588215896
Keyword::JOIN,
1588315897
]);
1588415898
match join_type {
1588515899
Some(Keyword::OUTER) => {
15900+
directed = self.dialect.supports_directed_join()
15901+
&& self.parse_keyword(Keyword::DIRECTED);
1588615902
self.expect_keyword_is(Keyword::JOIN)?;
1588715903
if is_left {
1588815904
JoinOperator::LeftOuter
@@ -15906,6 +15922,15 @@ impl<'a> Parser<'a> {
1590615922
JoinOperator::RightAnti
1590715923
}
1590815924
}
15925+
Some(Keyword::DIRECTED) if self.dialect.supports_directed_join() => {
15926+
directed = true;
15927+
self.expect_keyword_is(Keyword::JOIN)?;
15928+
if is_left {
15929+
JoinOperator::Left
15930+
} else {
15931+
JoinOperator::Right
15932+
}
15933+
}
1590915934
Some(Keyword::JOIN) => {
1591015935
if is_left {
1591115936
JoinOperator::Left
@@ -15915,7 +15940,7 @@ impl<'a> Parser<'a> {
1591515940
}
1591615941
_ => {
1591715942
return Err(ParserError::ParserError(format!(
15918-
"expected OUTER, SEMI, ANTI or JOIN after {kw:?}"
15943+
"expected OUTER, SEMI, ANTI, DIRECTED or JOIN after {kw:?}"
1591915944
)))
1592015945
}
1592115946
}
@@ -15933,6 +15958,8 @@ impl<'a> Parser<'a> {
1593315958
Keyword::FULL => {
1593415959
let _ = self.next_token(); // consume FULL
1593515960
let _ = self.parse_keyword(Keyword::OUTER); // [ OUTER ]
15961+
directed = self.dialect.supports_directed_join()
15962+
&& self.parse_keyword(Keyword::DIRECTED);
1593615963
self.expect_keyword_is(Keyword::JOIN)?;
1593715964
JoinOperator::FullOuter
1593815965
}
@@ -15967,6 +15994,7 @@ impl<'a> Parser<'a> {
1596715994
Join {
1596815995
relation,
1596915996
global,
15997+
directed,
1597015998
join_operator: join_operator_type(join_constraint),
1597115999
}
1597216000
};

‎src/test_utils.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ pub fn join(relation: TableFactor) -> Join {
432432
Join {
433433
relation,
434434
global: false,
435+
directed: false,
435436
join_operator: JoinOperator::Join(JoinConstraint::Natural),
436437
}
437438
}

‎tests/sqlparser_bigquery.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,7 @@ fn parse_join_constraint_unnest_alias() {
17861786
with_ordinality: false,
17871787
},
17881788
global: false,
1789+
directed: false,
17891790
join_operator: JoinOperator::Join(JoinConstraint::On(Expr::BinaryOp {
17901791
left: Box::new(Expr::Identifier("c1".into())),
17911792
op: BinaryOperator::Eq,

‎tests/sqlparser_common.rs‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7326,6 +7326,7 @@ fn parse_implicit_join() {
73267326
joins: vec![Join {
73277327
relation: table_from_name(ObjectName::from(vec!["t1b".into()])),
73287328
global: false,
7329+
directed: false,
73297330
join_operator: JoinOperator::Join(JoinConstraint::Natural),
73307331
}],
73317332
},
@@ -7334,6 +7335,7 @@ fn parse_implicit_join() {
73347335
joins: vec![Join {
73357336
relation: table_from_name(ObjectName::from(vec!["t2b".into()])),
73367337
global: false,
7338+
directed: false,
73377339
join_operator: JoinOperator::Join(JoinConstraint::Natural),
73387340
}],
73397341
},
@@ -7350,6 +7352,7 @@ fn parse_cross_join() {
73507352
Join {
73517353
relation: table_from_name(ObjectName::from(vec![Ident::new("t2")])),
73527354
global: false,
7355+
directed: false,
73537356
join_operator: JoinOperator::CrossJoin(JoinConstraint::None),
73547357
},
73557358
only(only(select.from).joins),
@@ -7362,6 +7365,7 @@ fn parse_cross_join_constraint() {
73627365
Join {
73637366
relation: table_from_name(ObjectName::from(vec![Ident::new("t2")])),
73647367
global: false,
7368+
directed: false,
73657369
join_operator: JoinOperator::CrossJoin(constraint),
73667370
}
73677371
}
@@ -7411,6 +7415,7 @@ fn parse_joins_on() {
74117415
index_hints: vec![],
74127416
},
74137417
global,
7418+
directed: false,
74147419
join_operator: f(JoinConstraint::On(Expr::BinaryOp {
74157420
left: Box::new(Expr::Identifier("c1".into())),
74167421
op: BinaryOperator::Eq,
@@ -7553,6 +7558,7 @@ fn parse_joins_using() {
75537558
index_hints: vec![],
75547559
},
75557560
global: false,
7561+
directed: false,
75567562
join_operator: f(JoinConstraint::Using(vec![ObjectName::from(vec![
75577563
"c1".into()
75587564
])])),
@@ -7648,6 +7654,7 @@ fn parse_natural_join() {
76487654
index_hints: vec![],
76497655
},
76507656
global: false,
7657+
directed: false,
76517658
join_operator: f(JoinConstraint::Natural),
76527659
}
76537660
}
@@ -7953,6 +7960,7 @@ fn parse_derived_tables() {
79537960
joins: vec![Join {
79547961
relation: table_from_name(ObjectName::from(vec!["t2".into()])),
79557962
global: false,
7963+
directed: false,
79567964
join_operator: JoinOperator::Join(JoinConstraint::Natural),
79577965
}],
79587966
}),
@@ -9055,6 +9063,7 @@ fn lateral_function() {
90559063
alias: None,
90569064
},
90579065
global: false,
9066+
directed: false,
90589067
join_operator: JoinOperator::Left(JoinConstraint::None),
90599068
}],
90609069
}],

‎tests/sqlparser_mssql.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ fn parse_mssql_openjson() {
522522
alias: table_alias(true, "B")
523523
},
524524
global: false,
525+
directed: false,
525526
join_operator: JoinOperator::CrossApply
526527
}]
527528
}],
@@ -576,6 +577,7 @@ fn parse_mssql_openjson() {
576577
alias: table_alias(true, "B")
577578
},
578579
global: false,
580+
directed: false,
579581
join_operator: JoinOperator::CrossApply
580582
}]
581583
}],
@@ -630,6 +632,7 @@ fn parse_mssql_openjson() {
630632
alias: table_alias(true, "B")
631633
},
632634
global: false,
635+
directed: false,
633636
join_operator: JoinOperator::CrossApply
634637
}]
635638
}],
@@ -664,6 +667,7 @@ fn parse_mssql_openjson() {
664667
alias: table_alias(true, "B")
665668
},
666669
global: false,
670+
directed: false,
667671
join_operator: JoinOperator::CrossApply
668672
}]
669673
}],
@@ -698,6 +702,7 @@ fn parse_mssql_openjson() {
698702
alias: table_alias(true, "B")
699703
},
700704
global: false,
705+
directed: false,
701706
join_operator: JoinOperator::CrossApply
702707
}]
703708
}],

0 commit comments

Comments
 (0)