@@ -627,14 +627,6 @@ mod tests {
627627 ) )
628628 }
629629
630- fn nullable_scalar_mark_scan ( name : & str ) -> Result < LogicalPlan > {
631- let schema = Schema :: new ( vec ! [
632- Field :: new( "id" , DataType :: Int32 , true ) ,
633- Field :: new( "grp" , DataType :: Int32 , true ) ,
634- ] ) ;
635- table_scan ( Some ( name) , & schema, None ) ?. build ( )
636- }
637-
638630 fn has_left_mark_join_with_null_aware (
639631 plan : & LogicalPlan ,
640632 expected_null_aware : bool ,
@@ -1287,81 +1279,33 @@ mod tests {
12871279 )
12881280 }
12891281
1290- #[ test]
1291- fn correlated_not_in_mark_join_is_null_aware_for_hashable_filter ( ) -> Result < ( ) > {
1292- let outer_scan = nullable_scalar_mark_scan ( "outer_t" ) ?;
1293- let inner_scan = nullable_scalar_mark_scan ( "inner_t" ) ?;
1294-
1295- let subquery = Arc :: new (
1296- LogicalPlanBuilder :: from ( inner_scan)
1297- . filter (
1298- out_ref_col ( DataType :: Int32 , "outer_t.grp" ) . eq ( col ( "inner_t.grp" ) ) ,
1299- ) ?
1300- . project ( vec ! [ col( "inner_t.id" ) ] ) ?
1301- . build ( ) ?,
1302- ) ;
1303-
1304- let plan = LogicalPlanBuilder :: from ( outer_scan)
1305- . filter ( not_in_subquery ( col ( "outer_t.id" ) , subquery) . is_null ( ) ) ?
1306- . build ( ) ?;
1307-
1308- let optimized = optimize_with_decorrelate ( plan) ?;
1309- assert ! (
1310- has_left_mark_join_with_null_aware( & optimized, true ) ,
1311- "{}" ,
1312- optimized. display_indent_schema( )
1313- ) ;
1314-
1315- Ok ( ( ) )
1316- }
1317-
1318- #[ test]
1319- fn correlated_not_in_mark_join_is_not_null_aware_for_residual_filter ( ) -> Result < ( ) > {
1320- let outer_scan = nullable_scalar_mark_scan ( "outer_t" ) ?;
1321- let inner_scan = nullable_scalar_mark_scan ( "inner_t" ) ?;
1322-
1323- let subquery = Arc :: new (
1324- LogicalPlanBuilder :: from ( inner_scan)
1325- . filter (
1326- out_ref_col ( DataType :: Int32 , "outer_t.grp" ) . lt ( col ( "inner_t.grp" ) ) ,
1327- ) ?
1328- . project ( vec ! [ col( "inner_t.id" ) ] ) ?
1329- . build ( ) ?,
1330- ) ;
1331-
1332- let plan = LogicalPlanBuilder :: from ( outer_scan)
1333- . filter ( not_in_subquery ( col ( "outer_t.id" ) , subquery) . is_null ( ) ) ?
1334- . build ( ) ?;
1335-
1336- let optimized = optimize_with_decorrelate ( plan) ?;
1337- assert ! (
1338- has_left_mark_join_with_null_aware( & optimized, false ) ,
1339- "{}" ,
1340- optimized. display_indent_schema( )
1341- ) ;
1342-
1343- Ok ( ( ) )
1344- }
1345-
1346- #[ test]
1347- fn correlated_not_in_mark_join_not_null_aware_for_non_nullable_value_key ( )
1348- -> Result < ( ) > {
1349- // The value key `id` is non-nullable on both sides; only the
1350- // correlation key `grp` is nullable. A NULL correlation key just makes
1351- // the correlated subquery empty, so NOT IN can never be UNKNOWN and
1352- // the mark join does not need null-aware semantics.
1282+ /// Builds `... WHERE (id NOT IN (SELECT id FROM inner WHERE <grp filter>))
1283+ /// IS NULL` over scans whose value key `id` has the given nullability (the
1284+ /// correlation key `grp` is always nullable), optimizes it, and asserts the
1285+ /// resulting `LeftMark` join's `null_aware` flag.
1286+ fn assert_correlated_not_in_mark_join_null_aware (
1287+ value_key_nullable : bool ,
1288+ hashable_filter : bool ,
1289+ expected_null_aware : bool ,
1290+ ) -> Result < ( ) > {
13531291 let schema = Schema :: new ( vec ! [
1354- Field :: new( "id" , DataType :: Int32 , false ) ,
1292+ Field :: new( "id" , DataType :: Int32 , value_key_nullable ) ,
13551293 Field :: new( "grp" , DataType :: Int32 , true ) ,
13561294 ] ) ;
13571295 let outer_scan = table_scan ( Some ( "outer_t" ) , & schema, None ) ?. build ( ) ?;
13581296 let inner_scan = table_scan ( Some ( "inner_t" ) , & schema, None ) ?. build ( ) ?;
13591297
1298+ let outer_grp = out_ref_col ( DataType :: Int32 , "outer_t.grp" ) ;
1299+ let inner_grp = col ( "inner_t.grp" ) ;
1300+ let grp_filter = if hashable_filter {
1301+ outer_grp. eq ( inner_grp)
1302+ } else {
1303+ outer_grp. lt ( inner_grp)
1304+ } ;
1305+
13601306 let subquery = Arc :: new (
13611307 LogicalPlanBuilder :: from ( inner_scan)
1362- . filter (
1363- out_ref_col ( DataType :: Int32 , "outer_t.grp" ) . eq ( col ( "inner_t.grp" ) ) ,
1364- ) ?
1308+ . filter ( grp_filter) ?
13651309 . project ( vec ! [ col( "inner_t.id" ) ] ) ?
13661310 . build ( ) ?,
13671311 ) ;
@@ -1372,14 +1316,29 @@ mod tests {
13721316
13731317 let optimized = optimize_with_decorrelate ( plan) ?;
13741318 assert ! (
1375- has_left_mark_join_with_null_aware( & optimized, false ) ,
1319+ has_left_mark_join_with_null_aware( & optimized, expected_null_aware ) ,
13761320 "{}" ,
13771321 optimized. display_indent_schema( )
13781322 ) ;
13791323
13801324 Ok ( ( ) )
13811325 }
13821326
1327+ #[ test]
1328+ fn correlated_not_in_mark_join_null_awareness ( ) -> Result < ( ) > {
1329+ // Hashable (`=`) correlation filter on a nullable value key: NOT IN can
1330+ // be UNKNOWN, so the mark join is null-aware.
1331+ assert_correlated_not_in_mark_join_null_aware ( true , true , true ) ?;
1332+ // Residual (`<`) correlation filter: the equijoin predicate can't be
1333+ // extracted, so the mark join is not null-aware.
1334+ assert_correlated_not_in_mark_join_null_aware ( true , false , false ) ?;
1335+ // Non-nullable value key `id`: a NULL correlation key just makes the
1336+ // subquery empty, so NOT IN can never be UNKNOWN and the join is not
1337+ // null-aware.
1338+ assert_correlated_not_in_mark_join_null_aware ( false , true , false ) ?;
1339+ Ok ( ( ) )
1340+ }
1341+
13831342 #[ test]
13841343 fn in_subquery_both_side_expr ( ) -> Result < ( ) > {
13851344 let table_scan = test_table_scan ( ) ?;
0 commit comments