Skip to content

Commit 01daed7

Browse files
Dimchikkkayman-sigma
authored andcommitted
Fix join precedence for non-snowflake queries (apache#1905)
1 parent 93b9f9c commit 01daed7

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
@@ -283,6 +283,10 @@ impl Dialect for SnowflakeDialect {
283283
true
284284
}
285285

286+
fn supports_left_associative_joins_without_parens(&self) -> bool {
287+
false
288+
}
289+
286290
fn is_reserved_for_identifier(&self, kw: Keyword) -> bool {
287291
// Unreserve some keywords that Snowflake accepts as identifiers
288292
// 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
@@ -12526,7 +12526,11 @@ impl<'a> Parser<'a> {
1252612526
};
1252712527
let mut relation = self.parse_table_factor()?;
1252812528

12529-
if self.peek_parens_less_nested_join() {
12529+
if !self
12530+
.dialect
12531+
.supports_left_associative_joins_without_parens()
12532+
&& self.peek_parens_less_nested_join()
12533+
{
1253012534
let joins = self.parse_joins()?;
1253112535
relation = TableFactor::NestedJoin {
1253212536
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
@@ -15385,6 +15385,29 @@ fn check_enforced() {
1538515385
);
1538615386
}
1538715387

15388+
#[test]
15389+
fn join_precedence() {
15390+
all_dialects_except(|d| !d.supports_left_associative_joins_without_parens())
15391+
.verified_query_with_canonical(
15392+
"SELECT *
15393+
FROM t1
15394+
NATURAL JOIN t5
15395+
INNER JOIN t0 ON (t0.v1 + t5.v0) > 0
15396+
WHERE t0.v1 = t1.v0",
15397+
// canonical string without parentheses
15398+
"SELECT * FROM t1 NATURAL JOIN t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0 WHERE t0.v1 = t1.v0",
15399+
);
15400+
all_dialects_except(|d| d.supports_left_associative_joins_without_parens()).verified_query_with_canonical(
15401+
"SELECT *
15402+
FROM t1
15403+
NATURAL JOIN t5
15404+
INNER JOIN t0 ON (t0.v1 + t5.v0) > 0
15405+
WHERE t0.v1 = t1.v0",
15406+
// canonical string with parentheses
15407+
"SELECT * FROM t1 NATURAL JOIN (t5 INNER JOIN t0 ON (t0.v1 + t5.v0) > 0) WHERE t0.v1 = t1.v0",
15408+
);
15409+
}
15410+
1538815411
#[test]
1538915412
fn parse_create_procedure_with_language() {
1539015413
let sql = r#"CREATE PROCEDURE test_proc LANGUAGE sql AS BEGIN SELECT 1; END"#;

0 commit comments

Comments
 (0)