-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Fix unparse union all nested distinct #24455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1591,10 +1591,43 @@ impl Unparser<'_> { | |
| ); | ||
| } | ||
|
|
||
| // Each UNION branch is unparsed in its own isolated query | ||
| // context. A branch is inlined only when it is a plain SELECT | ||
| // with no query-scoped clauses; otherwise it is wrapped in a | ||
| // parenthesized subquery. | ||
| // | ||
| // Sharing this statement's `QueryBuilder` across branches is | ||
| // unsound: its single `distinct_union` flag leaks a nested | ||
| // distinct `UNION` up to the enclosing `UNION ALL`, and a | ||
| // branch's own `ORDER BY`/`LIMIT`/`OFFSET` would bind to the | ||
| // whole set operation. Combined with sqlparser rendering nested | ||
| // `SetExpr::SetOperation`s without parentheses, this silently | ||
| // rewrites e.g. `a UNION ALL (b UNION c LIMIT 1)` into | ||
| // `a UNION b UNION c LIMIT 1`. Isolating each branch and | ||
| // parenthesizing non-trivial ones preserves precedence, the set | ||
| // quantifier, and operand-scoped clauses. | ||
| let input_exprs: Vec<SetExpr> = union | ||
| .inputs | ||
| .iter() | ||
| .map(|input| self.select_to_sql_expr(input, query)) | ||
| .map(|input| { | ||
| let mut branch_query = Some(QueryBuilder::default()); | ||
| let body = self.select_to_sql_expr(input, &mut branch_query)?; | ||
|
|
||
| // Inline a branch only when it is a plain SELECT with no | ||
| // query-scoped clauses; otherwise wrap it in a | ||
| // parenthesized subquery so its set quantifier and | ||
| // clauses stay bound to the branch. | ||
| match branch_query { | ||
| Some(mut branch_query) | ||
| if !matches!(body, SetExpr::Select(_)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this wrapping condition is a little too broad. It wraps any non- For example, DataFusion plans a flat query like Could we wrap only when the parentheses are actually needed? Now that each branch has its own let associative_with_parent = matches!(
&body,
SetExpr::SetOperation {
op: ast::SetOperator::Union,
set_quantifier: sq,
..
} if *sq == set_quantifier
);
let needs_wrap = !(matches!(body, SetExpr::Select(_)) || associative_with_parent)
|| branch_query
.as_ref()
.is_some_and(|q| q.has_operand_scoped_clauses());That should keep the cases this PR fixes parenthesized, such as |
||
| || branch_query.has_operand_scoped_clauses() => | ||
| { | ||
| let query = branch_query.body(Box::new(body)).build()?; | ||
| Ok(SetExpr::Query(Box::new(query))) | ||
| } | ||
| _ => Ok(body), | ||
| } | ||
| }) | ||
| .collect::<Result<Vec<_>>>()?; | ||
|
|
||
| assert_or_internal_err!( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -170,6 +170,10 @@ fn roundtrip_statement() -> Result<()> { | |
| SELECT j2_string as string FROM j2 | ||
| ORDER BY string DESC | ||
| LIMIT 10"#, | ||
| r#"SELECT j1_string FROM j1 UNION ALL (SELECT j2_string FROM j2 UNION SELECT j1_string FROM j1)"#, | ||
| r#"SELECT j1_string FROM j1 UNION SELECT j2_string FROM j2 UNION ALL SELECT j1_string FROM j1"#, | ||
| r#"SELECT j1_string FROM j1 UNION ALL (SELECT j2_string FROM j2 UNION SELECT j1_string FROM j1 LIMIT 5)"#, | ||
| r#"SELECT j1_string FROM j1 UNION ALL (SELECT j2_string FROM j2 UNION SELECT j1_string FROM j1 ORDER BY 1)"#, | ||
| r#"SELECT col1, id FROM ( | ||
| SELECT j1_string AS col1, j1_id AS id FROM j1 | ||
| UNION ALL | ||
|
|
@@ -359,6 +363,34 @@ fn roundtrip_statement_with_dialect_2() -> Result<(), DataFusionError> { | |
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn roundtrip_statement_union_all_with_nested_distinct_union() | ||
| -> Result<(), DataFusionError> { | ||
| // Outer `UNION ALL` whose operand is a distinct `UNION`: the outer ALL must | ||
| // survive, and the nested distinct UNION must be parenthesized. | ||
| roundtrip_statement_with_dialect_helper!( | ||
| sql: "SELECT j1_string FROM j1 UNION ALL (SELECT j2_string FROM j2 UNION SELECT j1_string FROM j1)", | ||
| parser_dialect: GenericDialect {}, | ||
| unparser_dialect: UnparserDefaultDialect {}, | ||
| expected: @"SELECT j1.j1_string FROM j1 UNION ALL (SELECT j2.j2_string FROM j2 UNION SELECT j1.j1_string FROM j1)", | ||
| ); | ||
| // The same shape written flat: `a UNION b UNION ALL a`. | ||
| roundtrip_statement_with_dialect_helper!( | ||
| sql: "SELECT j1_string FROM j1 UNION SELECT j2_string FROM j2 UNION ALL SELECT j1_string FROM j1", | ||
| parser_dialect: GenericDialect {}, | ||
| unparser_dialect: UnparserDefaultDialect {}, | ||
| expected: @"(SELECT j1.j1_string FROM j1 UNION SELECT j2.j2_string FROM j2) UNION ALL SELECT j1.j1_string FROM j1", | ||
| ); | ||
| // A branch's own LIMIT must remain bound to that branch, inside the parens. | ||
| roundtrip_statement_with_dialect_helper!( | ||
| sql: "SELECT j1_string FROM j1 UNION ALL (SELECT j2_string FROM j2 UNION SELECT j1_string FROM j1 LIMIT 5)", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional test hardening: we already test the branch-scoped |
||
| parser_dialect: GenericDialect {}, | ||
| unparser_dialect: UnparserDefaultDialect {}, | ||
| expected: @"SELECT j1.j1_string FROM j1 UNION ALL (SELECT j2.j2_string FROM j2 UNION SELECT j1.j1_string FROM j1 LIMIT 5)", | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn roundtrip_statement_with_dialect_3() -> Result<(), DataFusionError> { | ||
| roundtrip_statement_with_dialect_helper!( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor suggestion: I think this comment repeats quite a bit of the larger comment just above it, as well as part of the
has_operand_scoped_clausesdoc comment. Could we drop this shorter comment and keep the explanation in one place? The part explaining why sharing the builder is unsound is especially useful and worth keeping.