@@ -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]
32103209fn 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]
33233376fn 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]
33713406fn 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]
34113428fn 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]
34503447fn 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