@@ -1157,6 +1157,13 @@ fn try_push_into_inputs(
11571157 return Ok ( None ) ;
11581158 }
11591159
1160+ // Unnest may output a column with the same name but different value/type
1161+ // than its input column. Name-based routing cannot distinguish those.
1162+ // On top of that Unnest can't go through the `node.with_new_exprs(node.expressions(), new_inputs)` rebuild
1163+ if matches ! ( node, LogicalPlan :: Unnest ( _) ) {
1164+ return Ok ( None ) ;
1165+ }
1166+
11601167 // SubqueryAlias remaps qualifiers between input and output.
11611168 // Rewrite pairs/columns from alias-space to input-space before routing.
11621169 let remapped = if let LogicalPlan :: SubqueryAlias ( sa) = node {
@@ -3050,4 +3057,48 @@ mod tests {
30503057
30513058 Ok ( ( ) )
30523059 }
3060+
3061+ /// Regression test for the `Assertion failed: expr.is_empty(): Unnest`
3062+ /// internal error.
3063+ ///
3064+ /// `try_push_into_inputs` rebuilds the parent node via
3065+ /// `node.with_new_exprs(node.expressions(), new_inputs)`. For `Unnest`,
3066+ /// `apply_expressions` exposes the `exec_columns` as `Expr::Column`s
3067+ /// (so `expressions()` is **non-empty**), but `with_new_exprs` for
3068+ /// `Unnest` immediately calls `assert_no_expressions(expr)?` and errors
3069+ /// out. The optimizer should treat `Unnest` as a barrier and bail
3070+ /// instead of attempting to push through it.
3071+ #[ test]
3072+ fn test_no_push_through_unnest ( ) -> Result < ( ) > {
3073+ use arrow:: datatypes:: { DataType , Field , Schema } ;
3074+
3075+ let schema = Schema :: new ( vec ! [
3076+ Field :: new( "list_col" , DataType :: new_list( DataType :: Int32 , true ) , true ) ,
3077+ Field :: new( "other_col" , DataType :: Int32 , true ) ,
3078+ ] ) ;
3079+ let table_scan =
3080+ datafusion_expr:: logical_plan:: table_scan ( Some ( "t" ) , & schema, None ) ?
3081+ . build ( ) ?;
3082+ let plan = LogicalPlanBuilder :: from ( table_scan)
3083+ . unnest_column ( "list_col" ) ?
3084+ . filter ( leaf_udf ( col ( "list_col" ) , "x" ) . eq ( lit ( 1i32 ) ) ) ?
3085+ . build ( ) ?;
3086+
3087+ let ctx = OptimizerContext :: new ( ) . with_max_passes ( 1 ) ;
3088+ let optimizer = Optimizer :: with_rules ( vec ! [
3089+ Arc :: new( ExtractLeafExpressions :: new( ) ) ,
3090+ Arc :: new( PushDownLeafProjections :: new( ) ) ,
3091+ ] ) ;
3092+ let optimized = optimizer. optimize ( plan, & ctx, |_, _| { } ) ?;
3093+
3094+ insta:: assert_snapshot!( format!( "{optimized}" ) , @r#"
3095+ Projection: list_col, t.other_col
3096+ Filter: __datafusion_extracted_1 = Int32(1)
3097+ Projection: leaf_udf(list_col, Utf8("x")) AS __datafusion_extracted_1, list_col, t.other_col
3098+ Unnest: lists[t.list_col|depth=1] structs[]
3099+ TableScan: t
3100+ "# ) ;
3101+
3102+ Ok ( ( ) )
3103+ }
30533104}
0 commit comments