Skip to content

Commit 63a4830

Browse files
committed
Support scoped aliases in MERGE subqueries
1 parent 3a76cd3 commit 63a4830

2 files changed

Lines changed: 223 additions & 41 deletions

File tree

datafusion/core/tests/sql/sql_api.rs

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -254,26 +254,68 @@ async fn merge_into_rejects_source_alias_colliding_with_target_name() {
254254
}
255255

256256
#[tokio::test]
257-
async fn merge_into_rejects_subqueries_correlated_to_target_alias() {
257+
async fn merge_into_handles_subquery_target_alias_scopes() {
258258
let ctx = merge_into_context().await;
259-
assert_merge_sql_error(
260-
&ctx,
259+
for sql in [
261260
"MERGE INTO target AS t USING source AS s \
262261
ON EXISTS (SELECT 1 FROM source AS x WHERE x.id = t.id) \
263262
WHEN MATCHED THEN DELETE",
264-
"MERGE subqueries correlated to target alias 't' are not supported",
265-
)
266-
.await;
263+
"MERGE INTO target AS t USING source AS s \
264+
ON EXISTS (SELECT 1 FROM source AS q \
265+
WHERE EXISTS (SELECT 1 FROM source AS x WHERE x.id = t.id)) \
266+
WHEN MATCHED THEN DELETE",
267+
"MERGE INTO target AS t USING source AS s \
268+
ON EXISTS (SELECT 1 FROM source AS q \
269+
CROSS JOIN LATERAL (SELECT t.id) AS l) \
270+
WHEN MATCHED THEN DELETE",
271+
"MERGE INTO target AS t USING source AS s \
272+
ON EXISTS (SELECT t.id FROM source AS t \
273+
LIMIT (SELECT t.id FROM source AS x LIMIT 1)) \
274+
WHEN MATCHED THEN DELETE",
275+
"MERGE INTO target AS t USING source AS s \
276+
ON EXISTS (SELECT 1 FROM source AS q CROSS JOIN (
277+
source AS x CROSS JOIN LATERAL (SELECT t.id) AS l
278+
)) WHEN MATCHED THEN DELETE",
279+
] {
280+
assert_merge_sql_error(
281+
&ctx,
282+
sql,
283+
"MERGE subqueries correlated to target alias 't' are not supported",
284+
)
285+
.await;
286+
}
267287

268-
// Source-correlated and uncorrelated subqueries remain supported through
269-
// logical optimization.
288+
// Source-correlated, uncorrelated, and locally shadowed target aliases
289+
// remain supported through logical optimization.
270290
for sql in [
271291
"MERGE INTO target AS t USING source AS s \
272292
ON EXISTS (SELECT 1 FROM source AS x WHERE x.id = s.id) \
273293
WHEN MATCHED THEN DELETE",
274294
"MERGE INTO target AS t USING source AS s \
275295
ON t.id = ANY (SELECT id FROM source) \
276296
WHEN MATCHED THEN DELETE",
297+
"MERGE INTO target AS t USING source AS s \
298+
ON EXISTS (SELECT 1 FROM source AS t \
299+
WHERE EXISTS (SELECT 1 FROM source AS x WHERE x.id = t.id)) \
300+
WHEN MATCHED THEN DELETE",
301+
"MERGE INTO target AS t USING source AS s \
302+
ON EXISTS (SELECT EXISTS (
303+
SELECT 1 FROM source AS x WHERE x.id = t.id
304+
) FROM source AS t) \
305+
WHEN MATCHED THEN DELETE",
306+
"MERGE INTO target AS t USING source AS s \
307+
ON EXISTS (SELECT 1 FROM source AS t \
308+
WHERE EXISTS (SELECT 1 FROM source AS q \
309+
WHERE EXISTS (SELECT 1 FROM source AS x WHERE x.id = t.id))) \
310+
WHEN MATCHED THEN DELETE",
311+
"MERGE INTO target AS t USING source AS s \
312+
ON EXISTS (SELECT 1 FROM source AS t \
313+
CROSS JOIN LATERAL (SELECT t.id) AS l) \
314+
WHEN MATCHED THEN DELETE",
315+
"MERGE INTO target AS t USING source AS s \
316+
ON EXISTS (SELECT 1 FROM source AS t CROSS JOIN (
317+
source AS x CROSS JOIN LATERAL (SELECT t.id) AS l
318+
)) WHEN MATCHED THEN DELETE",
277319
] {
278320
assert_merge_physical_error(&ctx, sql, "MERGE INTO not supported for Base table")
279321
.await;

datafusion/sql/src/statement.rs

Lines changed: 173 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ use datafusion_expr::dml::{
4646
use datafusion_expr::expr_rewriter::normalize_col_with_schemas_and_ambiguity_check;
4747
use datafusion_expr::logical_plan::DdlStatement;
4848
use datafusion_expr::logical_plan::builder::project;
49-
use datafusion_expr::utils::expr_to_columns;
49+
use datafusion_expr::utils::{expr_to_columns, merge_schema};
5050
use datafusion_expr::{
5151
Analyze, CreateCatalog, CreateCatalogSchema,
5252
CreateExternalTable as PlanCreateExternalTable, CreateFunction, CreateFunctionBody,
5353
CreateIndex as PlanCreateIndex, CreateMemoryTable, CreateView, Deallocate,
5454
DescribeTable, DmlStatement, DropCatalogSchema, DropFunction, DropTable, DropView,
5555
EmptyRelation, Execute, Explain, ExplainFormat, Expr, ExprSchemable, Filter,
5656
LogicalPlan, LogicalPlanBuilder, OperateFunctionArg, PlanType, Prepare,
57-
ResetVariable, SetVariable, SortExpr, Statement as PlanStatement, ToStringifiedPlan,
58-
TransactionAccessMode, TransactionConclusion, TransactionEnd,
57+
ResetVariable, SetVariable, SortExpr, Statement as PlanStatement, Subquery,
58+
ToStringifiedPlan, TransactionAccessMode, TransactionConclusion, TransactionEnd,
5959
TransactionIsolationLevel, TransactionStart, Volatility, WriteOp, cast, col,
6060
};
6161
use sqlparser::ast::{
@@ -2610,42 +2610,24 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
26102610
.map(|transformed| transformed.data)
26112611
}
26122612

2613-
/// Return true if an expression contains a subquery whose embedded plan
2614-
/// has an outer reference qualified by `qualifier`.
2613+
/// Return true if an expression contains a subquery correlated to
2614+
/// `qualifier`, accounting for aliases introduced by intervening subquery
2615+
/// scopes.
26152616
fn has_outer_reference_to_qualifier(
26162617
expr: &Expr,
26172618
qualifier: &TableReference,
26182619
) -> Result<bool> {
26192620
let mut found = false;
26202621
expr.apply(|expr| {
2621-
let subquery = match expr {
2622-
Expr::Exists(exists) => Some(&exists.subquery),
2623-
Expr::InSubquery(in_subquery) => Some(&in_subquery.subquery),
2624-
Expr::SetComparison(set_comparison) => Some(&set_comparison.subquery),
2625-
Expr::ScalarSubquery(subquery) => Some(subquery),
2626-
_ => None,
2627-
};
2628-
2629-
if let Some(subquery) = subquery {
2630-
subquery.subquery.apply_with_subqueries(|plan| {
2631-
plan.apply_expressions(|expr| {
2632-
expr.apply(|expr| {
2633-
if let Expr::OuterReferenceColumn(_, column) = expr
2634-
&& column.relation.as_ref() == Some(qualifier)
2635-
{
2636-
found = true;
2637-
Ok(TreeNodeRecursion::Stop)
2638-
} else {
2639-
Ok(TreeNodeRecursion::Continue)
2640-
}
2641-
})
2642-
})?;
2643-
Ok(if found {
2644-
TreeNodeRecursion::Stop
2645-
} else {
2646-
TreeNodeRecursion::Continue
2647-
})
2648-
})?;
2622+
if let Some(subquery) = Self::expr_subquery(expr)
2623+
&& Self::plan_has_outer_reference_to_qualifier(
2624+
&subquery.subquery,
2625+
qualifier,
2626+
&[],
2627+
None,
2628+
)?
2629+
{
2630+
found = true;
26492631
}
26502632

26512633
Ok(if found {
@@ -2657,6 +2639,164 @@ impl<S: ContextProvider> SqlToRel<'_, S> {
26572639
Ok(found)
26582640
}
26592641

2642+
fn expr_subquery(expr: &Expr) -> Option<&Subquery> {
2643+
match expr {
2644+
Expr::Exists(exists) => Some(&exists.subquery),
2645+
Expr::InSubquery(in_subquery) => Some(&in_subquery.subquery),
2646+
Expr::SetComparison(set_comparison) => Some(&set_comparison.subquery),
2647+
Expr::ScalarSubquery(subquery) => Some(subquery),
2648+
_ => None,
2649+
}
2650+
}
2651+
2652+
#[cfg_attr(feature = "recursive_protection", recursive::recursive)]
2653+
fn plan_has_outer_reference_to_qualifier(
2654+
plan: &LogicalPlan,
2655+
qualifier: &TableReference,
2656+
outer_scopes: &[DFSchema],
2657+
lateral_scope: Option<&DFSchema>,
2658+
) -> Result<bool> {
2659+
// Correlated lateral relations use an explicit Subquery plan node
2660+
// rather than an expression.
2661+
if let LogicalPlan::Subquery(subquery) = plan {
2662+
let mut subquery_outer_scopes = outer_scopes.to_vec();
2663+
if let Some(lateral_scope) = lateral_scope {
2664+
subquery_outer_scopes.push(lateral_scope.clone());
2665+
}
2666+
return Self::plan_has_outer_reference_to_qualifier(
2667+
&subquery.subquery,
2668+
qualifier,
2669+
&subquery_outer_scopes,
2670+
None,
2671+
);
2672+
}
2673+
2674+
let local_scope = Self::plan_expression_scope(plan);
2675+
2676+
let mut found = false;
2677+
plan.apply_expressions(|expr| {
2678+
expr.apply(|expr| {
2679+
if let Expr::OuterReferenceColumn(_, column) = expr
2680+
&& column.relation.as_ref() == Some(qualifier)
2681+
&& !outer_scopes
2682+
.iter()
2683+
.rev()
2684+
.any(|scope| scope.maybe_index_of_column(column).is_some())
2685+
{
2686+
found = true;
2687+
} else if let Some(subquery) = Self::expr_subquery(expr) {
2688+
let mut nested_outer_scopes = outer_scopes.to_vec();
2689+
if let Some(local_scope) = &local_scope {
2690+
nested_outer_scopes.push(local_scope.clone());
2691+
}
2692+
if Self::plan_has_outer_reference_to_qualifier(
2693+
&subquery.subquery,
2694+
qualifier,
2695+
&nested_outer_scopes,
2696+
None,
2697+
)? {
2698+
found = true;
2699+
}
2700+
}
2701+
2702+
Ok(if found {
2703+
TreeNodeRecursion::Stop
2704+
} else {
2705+
TreeNodeRecursion::Continue
2706+
})
2707+
})
2708+
})?;
2709+
if found {
2710+
return Ok(true);
2711+
}
2712+
2713+
if let LogicalPlan::Join(join) = plan {
2714+
if Self::plan_has_outer_reference_to_qualifier(
2715+
&join.left,
2716+
qualifier,
2717+
outer_scopes,
2718+
lateral_scope,
2719+
)? {
2720+
return Ok(true);
2721+
}
2722+
2723+
let mut right_lateral_scope =
2724+
lateral_scope.cloned().unwrap_or_else(DFSchema::empty);
2725+
right_lateral_scope.merge(join.left.schema());
2726+
if let Some(subquery) = Self::lateral_subquery(&join.right) {
2727+
let mut lateral_outer_scopes = outer_scopes.to_vec();
2728+
lateral_outer_scopes.push(right_lateral_scope);
2729+
return Self::plan_has_outer_reference_to_qualifier(
2730+
&subquery.subquery,
2731+
qualifier,
2732+
&lateral_outer_scopes,
2733+
None,
2734+
);
2735+
}
2736+
2737+
return Self::plan_has_outer_reference_to_qualifier(
2738+
&join.right,
2739+
qualifier,
2740+
outer_scopes,
2741+
Some(&right_lateral_scope),
2742+
);
2743+
}
2744+
2745+
for input in plan.inputs() {
2746+
if Self::plan_has_outer_reference_to_qualifier(
2747+
input,
2748+
qualifier,
2749+
outer_scopes,
2750+
lateral_scope,
2751+
)? {
2752+
return Ok(true);
2753+
}
2754+
}
2755+
2756+
Ok(false)
2757+
}
2758+
2759+
/// Return the schema used to resolve subqueries in this node's expressions.
2760+
///
2761+
/// Most relational expressions use their input schema. LIMIT/OFFSET
2762+
/// expressions are deliberately resolved against an empty schema, so they
2763+
/// must not inherit aliases from the limited input.
2764+
fn plan_expression_scope(plan: &LogicalPlan) -> Option<DFSchema> {
2765+
match plan {
2766+
LogicalPlan::Projection(_)
2767+
| LogicalPlan::Filter(_)
2768+
| LogicalPlan::Repartition(_)
2769+
| LogicalPlan::Window(_)
2770+
| LogicalPlan::Aggregate(_)
2771+
| LogicalPlan::Join(_)
2772+
| LogicalPlan::Sort(_)
2773+
| LogicalPlan::TableScan(_)
2774+
| LogicalPlan::Unnest(_)
2775+
| LogicalPlan::Distinct(_) => {
2776+
let inputs = plan.inputs();
2777+
Some(if inputs.is_empty() {
2778+
plan.schema().as_ref().clone()
2779+
} else {
2780+
merge_schema(&inputs)
2781+
})
2782+
}
2783+
_ => None,
2784+
}
2785+
}
2786+
2787+
/// Return the correlated subquery wrapped by a lateral relation, if this
2788+
/// plan has the shape produced by `create_relation_subquery`.
2789+
fn lateral_subquery(mut plan: &LogicalPlan) -> Option<&Subquery> {
2790+
loop {
2791+
match plan {
2792+
LogicalPlan::Subquery(subquery) => return Some(subquery),
2793+
LogicalPlan::SubqueryAlias(alias) => plan = &alias.input,
2794+
LogicalPlan::Projection(projection) => plan = &projection.input,
2795+
_ => return None,
2796+
}
2797+
}
2798+
}
2799+
26602800
fn merge_target_column_name(
26612801
&self,
26622802
name: &ObjectName,

0 commit comments

Comments
 (0)