Skip to content

Commit 14d02dd

Browse files
committed
Fix join precedence for non-snowflake queries (apache#1905)
1 parent 2bbaaeb commit 14d02dd

File tree

5 files changed

+64
-1
lines changed

5 files changed

+64
-1
lines changed

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ impl Dialect for GenericDialect {
5252
true
5353
}
5454

55+
fn supports_left_associative_joins_without_parens(&self) -> bool {
56+
true
57+
}
58+
5559
fn supports_connect_by(&self) -> bool {
5660
true
5761
}

src/dialect/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,34 @@ pub trait Dialect: Debug + Any {
278278
false
279279
}
280280

281+
/// Indicates whether the dialect supports left-associative join parsing
282+
/// by default when parentheses are omitted in nested joins.
283+
///
284+
/// Most dialects (like MySQL or Postgres) assume **left-associative** precedence,
285+
/// so a query like:
286+
///
287+
/// ```sql
288+
/// SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON ...
289+
/// ```
290+
/// is interpreted as:
291+
/// ```sql
292+
/// ((t1 NATURAL JOIN t5) INNER JOIN t0 ON ...)
293+
/// ```
294+
/// and internally represented as a **flat list** of joins.
295+
///
296+
/// In contrast, some dialects (e.g. **Snowflake**) assume **right-associative**
297+
/// precedence and interpret the same query as:
298+
/// ```sql
299+
/// (t1 NATURAL JOIN (t5 INNER JOIN t0 ON ...))
300+
/// ```
301+
/// which results in a **nested join** structure in the AST.
302+
///
303+
/// If this method returns `false`, the parser must build nested join trees
304+
/// even in the absence of parentheses to reflect the correct associativity
305+
fn supports_left_associative_joins_without_parens(&self) -> bool {
306+
true
307+
}
308+
281309
/// Returns true if the dialect supports the `(+)` syntax for OUTER JOIN.
282310
fn supports_outer_join_operator(&self) -> bool {
283311
false

src/dialect/snowflake.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ impl Dialect for SnowflakeDialect {
279279
true
280280
}
281281

282+
fn supports_left_associative_joins_without_parens(&self) -> bool {
283+
false
284+
}
285+
282286
fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
283287
// Unreserve some keywords that Snowflake accepts as identifiers
284288
// See: https://docs.snowflake.com/en/sql-reference/reserved-keywords

src/parser/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11922,7 +11922,11 @@ impl<'a> Parser<'a> {
1192211922
};
1192311923
let mut relation = self.parse_table_factor()?;
1192411924

11925-
if self.peek_parens_less_nested_join() {
11925+
if !self
11926+
.dialect
11927+
.supports_left_associative_joins_without_parens()
11928+
&& self.peek_parens_less_nested_join()
11929+
{
1192611930
let joins = self.parse_joins()?;
1192711931
relation = TableFactor::NestedJoin {
1192811932
table_with_joins: Box::new(TableWithJoins { relation, joins }),

tests/sqlparser_common.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15110,3 +15110,26 @@ fn parse_return() {
1511015110
fn parse_subquery_limit() {
1511115111
let _ = all_dialects().verified_stmt("SELECT t1_id, t1_name FROM t1 WHERE t1_id IN (SELECT t2_id FROM t2 WHERE t1_name = t2_name LIMIT 10)");
1511215112
}
15113+
15114+
#[test]
15115+
fn join_precedence() {
15116+
all_dialects_except(|d| !d.supports_left_associative_joins_without_parens())
15117+
.verified_query_with_canonical(
15118+
"SELECT *
15119+
FROM t1
15120+
NATURAL JOIN t5
15121+
INNER JOIN t0 ON (t0.v1 + t5.v0) > 0
15122+
WHERE t0.v1 = t1.v0",
15123+
// canonical string without parentheses
15124+
"SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0",
15125+
);
15126+
all_dialects_except(|d| d.supports_left_associative_joins_without_parens()).verified_query_with_canonical(
15127+
"SELECT *
15128+
FROM t1
15129+
NATURAL JOIN t5
15130+
INNER JOIN t0 ON (t0.v1 + t5.v0) > 0
15131+
WHERE t0.v1 = t1.v0",
15132+
// canonical string with parentheses
15133+
"SELECT * FROM t1 NATURAL JOIN (t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0) WHERE t0.v1 = t1.v0",
15134+
);
15135+
}

0 commit comments

Comments
 (0)