Skip to content

Commit 0c9c64d

Browse files
committed
test(unparser): share a fetch-parameterized fixture between the bounded and unbounded EXISTS cases
The bounded/unbounded pairs exist to pin that the refusal is gated on the row bound rather than on the shape of the correlation. That only holds while each pair's two plans are identical apart from the bound, which copy-paste made a convention rather than a property — an edit to one side would silently leave the pair comparing two different shapes. Parameterize by `fetch`, matching `qualified_build_side_semi_join` alongside it. The limit is applied only when asked, since `limit(0, None)` still inserts a `Limit` node. Also drops a stale claim on the fully-qualified-dialect test: it described the other two shapes as declining to SQL that "does not bind, which a database rejects", which is the premise the refusals overturn.
1 parent 4cb060c commit 0c9c64d

2 files changed

Lines changed: 70 additions & 85 deletions

File tree

datafusion/sql/src/unparser/plan.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2732,9 +2732,8 @@ impl Unparser<'_> {
27322732
/// is a bound this unparser cannot place — and leaving the bound beside the
27332733
/// correlation emits SQL that *binds and runs* while answering from rows
27342734
/// outside the bound. A wrong answer that executes is worse than one that
2735-
/// does not, so these cost the pushdown instead: the trade
2736-
/// `derive_row_limited_scope` makes for the limit it scopes, and the one the
2737-
/// fully-qualified-dialect arm below already made.
2735+
/// does not, so these cost the pushdown instead — the trade
2736+
/// `derive_row_limited_scope` makes for the limit it scopes.
27382737
///
27392738
/// Refusing is not the same as repairing. Each shape below is emitted
27402739
/// correctly only once the correlation's own qualifiers are rewritten to the

datafusion/sql/tests/cases/plan_to_sql.rs

Lines changed: 68 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3201,11 +3201,10 @@ fn qualified_build_side_semi_join(fetch: Option<usize>) -> Result<LogicalPlan> {
32013201
/// name in the correlated predicate, but a derived table's alias is a single
32023202
/// identifier and can only carry the last one, so the bound cannot be scoped.
32033203
///
3204-
/// This one is refused rather than declined. The other declines in
3205-
/// `exists_scope_name` fall back to SQL that does not bind, which a database
3206-
/// rejects; leaving the bound unscoped *here* would bind and run, silently
3207-
/// answering from rows outside the bound. Losing the pushdown is the cheaper
3208-
/// failure, and it is the trade `derive_row_limited_scope` already makes.
3204+
/// Every shape `exists_scope_name` cannot name is refused, this one included:
3205+
/// leaving the bound unscoped binds and runs, silently answering from rows
3206+
/// outside the bound. Losing the pushdown is the cheaper failure, and it is the
3207+
/// trade `derive_row_limited_scope` already makes.
32093208
#[test]
32103209
fn test_unparse_semi_join_build_side_fetch_refuses_qualified_full_column_dialect()
32113210
-> Result<()> {
@@ -3305,6 +3304,60 @@ fn test_unparse_left_semi_join_scopes_bounded_set_operation_build_side() -> Resu
33053304
Ok(())
33063305
}
33073306

3307+
/// Builds `LeftSemi Join: t1.c = t2.c AND t1.d = t3.d` over a `t2 INNER JOIN t3`
3308+
/// build side, optionally bounded, so the bounded and unbounded cases below
3309+
/// differ only in the bound.
3310+
fn multi_relation_build_side_semi_join(fetch: Option<usize>) -> Result<LogicalPlan> {
3311+
let schema = exists_fetch_schema();
3312+
let probe = table_scan(Some("t1"), &schema, Some(vec![0, 1]))?.build()?;
3313+
let build = table_scan(Some("t2"), &schema, Some(vec![0]))?.join_on(
3314+
table_scan(Some("t3"), &schema, Some(vec![0, 1]))?.build()?,
3315+
datafusion_expr::JoinType::Inner,
3316+
vec![col("t2.c").eq(col("t3.c"))],
3317+
)?;
3318+
// Applied only when asked: `limit(0, None)` still inserts a `Limit` node,
3319+
// and the unbounded cases are about a build side that carries no bound.
3320+
let build = match fetch {
3321+
Some(fetch) => build.limit(0, Some(fetch))?,
3322+
None => build,
3323+
}
3324+
.build()?;
3325+
3326+
LogicalPlanBuilder::from(probe)
3327+
.project(vec![col("t1.d")])?
3328+
.join_on(
3329+
build,
3330+
datafusion_expr::JoinType::LeftSemi,
3331+
vec![col("t1.c").eq(col("t2.c")), col("t1.d").eq(col("t3.d"))],
3332+
)?
3333+
.build()
3334+
}
3335+
3336+
/// Builds `LeftSemi Join: t.c = t.c` where the build side is the same relation as
3337+
/// the probe, optionally bounded — the self-join whose correlation carries only a
3338+
/// qualifier the probe also answers to.
3339+
fn probe_qualified_self_join(fetch: Option<usize>) -> Result<LogicalPlan> {
3340+
let schema = exists_fetch_schema();
3341+
let probe = table_scan(Some("t"), &schema, Some(vec![0, 1]))?.build()?;
3342+
let build = table_scan_with_filter_and_fetch(
3343+
Some("t"),
3344+
&schema,
3345+
Some(vec![0]),
3346+
vec![],
3347+
fetch,
3348+
)?
3349+
.build()?;
3350+
3351+
LogicalPlanBuilder::from(probe)
3352+
.project(vec![col("t.d")])?
3353+
.join_on(
3354+
build,
3355+
datafusion_expr::JoinType::LeftSemi,
3356+
vec![col("t.c").eq(col("t.c"))],
3357+
)?
3358+
.build()
3359+
}
3360+
33083361
/// A correlation naming more than one of the build side's own inputs cannot be
33093362
/// scoped: one derived table can answer to only one of those names, so no name
33103363
/// keeps every reference bound to the relation it came from.
@@ -3321,25 +3374,7 @@ fn test_unparse_left_semi_join_scopes_bounded_set_operation_build_side() -> Resu
33213374
/// refusal to become a scope.
33223375
#[test]
33233376
fn test_unparse_left_semi_join_refuses_multi_relation_build_side() -> Result<()> {
3324-
let schema = exists_fetch_schema();
3325-
let probe = table_scan(Some("t1"), &schema, Some(vec![0, 1]))?.build()?;
3326-
let build = table_scan(Some("t2"), &schema, Some(vec![0]))?
3327-
.join_on(
3328-
table_scan(Some("t3"), &schema, Some(vec![0, 1]))?.build()?,
3329-
datafusion_expr::JoinType::Inner,
3330-
vec![col("t2.c").eq(col("t3.c"))],
3331-
)?
3332-
.limit(0, Some(5))?
3333-
.build()?;
3334-
3335-
let plan = LogicalPlanBuilder::from(probe)
3336-
.project(vec![col("t1.d")])?
3337-
.join_on(
3338-
build,
3339-
datafusion_expr::JoinType::LeftSemi,
3340-
vec![col("t1.c").eq(col("t2.c")), col("t1.d").eq(col("t3.d"))],
3341-
)?
3342-
.build()?;
3377+
let plan = multi_relation_build_side_semi_join(Some(5))?;
33433378

33443379
let unparser = Unparser::new(&UnparserPostgreSqlDialect {});
33453380
let err = unparser
@@ -3369,25 +3404,7 @@ fn test_unparse_left_semi_join_refuses_multi_relation_build_side() -> Result<()>
33693404
/// still emitted and still wrong.
33703405
#[test]
33713406
fn test_unparse_left_semi_join_refuses_probe_qualified_correlation() -> Result<()> {
3372-
let schema = exists_fetch_schema();
3373-
let probe = table_scan(Some("t"), &schema, Some(vec![0, 1]))?.build()?;
3374-
let build = table_scan_with_filter_and_fetch(
3375-
Some("t"),
3376-
&schema,
3377-
Some(vec![0]),
3378-
vec![],
3379-
Some(5),
3380-
)?
3381-
.build()?;
3382-
3383-
let plan = LogicalPlanBuilder::from(probe)
3384-
.project(vec![col("t.d")])?
3385-
.join_on(
3386-
build,
3387-
datafusion_expr::JoinType::LeftSemi,
3388-
vec![col("t.c").eq(col("t.c"))],
3389-
)?
3390-
.build()?;
3407+
let plan = probe_qualified_self_join(Some(5))?;
33913408

33923409
let unparser = Unparser::new(&UnparserPostgreSqlDialect {});
33933410
let err = unparser
@@ -3410,24 +3427,7 @@ fn test_unparse_left_semi_join_refuses_probe_qualified_correlation() -> Result<(
34103427
#[test]
34113428
fn test_unparse_left_semi_join_without_fetch_keeps_multi_relation_correlation()
34123429
-> Result<()> {
3413-
let schema = exists_fetch_schema();
3414-
let probe = table_scan(Some("t1"), &schema, Some(vec![0, 1]))?.build()?;
3415-
let build = table_scan(Some("t2"), &schema, Some(vec![0]))?
3416-
.join_on(
3417-
table_scan(Some("t3"), &schema, Some(vec![0, 1]))?.build()?,
3418-
datafusion_expr::JoinType::Inner,
3419-
vec![col("t2.c").eq(col("t3.c"))],
3420-
)?
3421-
.build()?;
3422-
3423-
let plan = LogicalPlanBuilder::from(probe)
3424-
.project(vec![col("t1.d")])?
3425-
.join_on(
3426-
build,
3427-
datafusion_expr::JoinType::LeftSemi,
3428-
vec![col("t1.c").eq(col("t2.c")), col("t1.d").eq(col("t3.d"))],
3429-
)?
3430-
.build()?;
3430+
let plan = multi_relation_build_side_semi_join(None)?;
34313431

34323432
let unparser = Unparser::new(&UnparserPostgreSqlDialect {});
34333433
assert_snapshot!(
@@ -3437,30 +3437,16 @@ fn test_unparse_left_semi_join_without_fetch_keeps_multi_relation_correlation()
34373437
Ok(())
34383438
}
34393439

3440-
/// The probe-qualified self-join, unbounded. Still emitted, and still wrong: the
3441-
/// inner `FROM "t"` shadows the outer `"t"`, so the correlation is lost. That is
3442-
/// the pre-existing defect spiceai/spiceai#12840 tracks, independent of any
3443-
/// bound.
3444-
///
3445-
/// Recorded so the boundary of the refusal above is explicit — it covers the
3446-
/// bounded build side, which is where a scope is demanded, and nothing else. Not
3447-
/// an endorsement of this output; whoever lands the qualifier rewrite should
3448-
/// expect this snapshot to change too.
3440+
/// Pins that the refusal above is gated on the bound: unbounded, the same
3441+
/// correlation is still emitted. This output is **known-incorrect** — the inner
3442+
/// `FROM "t"` shadows the outer `"t"`, so the correlation is lost — and it is not
3443+
/// an endorsement. That shadowing is the pre-existing defect independent of any
3444+
/// bound, so whoever lands the qualifier rewrite should expect this snapshot to
3445+
/// change too.
34493446
#[test]
34503447
fn test_unparse_left_semi_join_without_fetch_still_shadows_probe_qualifier() -> Result<()>
34513448
{
3452-
let schema = exists_fetch_schema();
3453-
let probe = table_scan(Some("t"), &schema, Some(vec![0, 1]))?.build()?;
3454-
let build = table_scan(Some("t"), &schema, Some(vec![0]))?.build()?;
3455-
3456-
let plan = LogicalPlanBuilder::from(probe)
3457-
.project(vec![col("t.d")])?
3458-
.join_on(
3459-
build,
3460-
datafusion_expr::JoinType::LeftSemi,
3461-
vec![col("t.c").eq(col("t.c"))],
3462-
)?
3463-
.build()?;
3449+
let plan = probe_qualified_self_join(None)?;
34643450

34653451
let unparser = Unparser::new(&UnparserPostgreSqlDialect {});
34663452
assert_snapshot!(

0 commit comments

Comments
 (0)