This is an automated email from the ASF dual-hosted git repository.

andygrove pushed a commit to branch branch-1.0
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/branch-1.0 by this push:
     new 8407d475c8 perf: evaluate posexplode array expressions once per batch 
(#5737) (#5820)
8407d475c8 is described below

commit 8407d475c87d2be21ed9ad749737f0b1040a074d
Author: Andy Grove <[email protected]>
AuthorDate: Thu Sep 10 11:20:08 2026 -0600

    perf: evaluate posexplode array expressions once per batch (#5737) (#5820)
    
    (cherry picked from commit cbe3b39035a5918fc65496ebfe08bcf8e7adfce2)
    
    Adapted for branch-1.0:
    - Added the `AtomicUsize`/`Ordering`, `ArrayRef`, and `MemorySourceConfig`
      imports the new planner test needs. On main these were already present
      from tests added after branch-1.0 was cut.
    - Dropped `element_field_id` from the test's `ListInfo` literal; that proto
      field does not exist on branch-1.0.
    
    Co-authored-by: KUAN-HAO HUANG <[email protected]>
---
 native/core/src/execution/planner.rs               | 247 +++++++++++++++++----
 .../apache/comet/exec/CometGenerateExecSuite.scala |  14 ++
 2 files changed, 215 insertions(+), 46 deletions(-)

diff --git a/native/core/src/execution/planner.rs 
b/native/core/src/execution/planner.rs
index 5cb305b4c6..2baf3baa3c 100644
--- a/native/core/src/execution/planner.rs
+++ b/native/core/src/execution/planner.rs
@@ -1925,54 +1925,46 @@ impl PhysicalPlanner {
                 // unnesting. See 
https://github.com/apache/datafusion/issues/19053. Once
                 // Comet moves to a DataFusion release carrying
                 // https://github.com/apache/datafusion/pull/22100, 
`ListEmptyToNullExpr`
-                // and the pre-projection below can be removed in favor of
-                // `NullHandling::PreserveAndExpandEmpty`. See
+                // can be removed in favor of 
`NullHandling::PreserveAndExpandEmpty`. See
                 // https://github.com/apache/datafusion-comet/issues/5210.
-                //
-                // For `posexplode_outer` the wrapped array is materialized in 
a
-                // pre-projection so `ListPositionsExpr` and the array 
passthrough share
-                // a single evaluation of `ListEmptyToNullExpr` instead of 
re-running it
-                // per branch. Plain `explode_outer` references the wrapped 
array exactly
-                // once, so no pre-projection is needed there.
-                let (child_expr, child_native_plan): (Arc<dyn PhysicalExpr>, 
_) =
-                    match (explode.outer, explode.position) {
-                        (true, true) => {
-                            let wrapped: Arc<dyn PhysicalExpr> =
-                                
Arc::new(ListEmptyToNullExpr::new(raw_child_expr));
-                            let reserved_name =
-                                format!("__comet_explode_outer_{}", 
child_field_name);
-
-                            let mut pre_exprs: Vec<(Arc<dyn PhysicalExpr>, 
String)> = child_schema
-                                .fields()
-                                .iter()
-                                .enumerate()
-                                .map(|(i, f)| {
-                                    (
-                                        Arc::new(Column::new(f.name(), i)) as 
Arc<dyn PhysicalExpr>,
-                                        f.name().to_string(),
-                                    )
-                                })
-                                .collect();
-                            let wrapped_idx = pre_exprs.len();
-                            pre_exprs.push((wrapped, reserved_name.clone()));
+                let child_expr: Arc<dyn PhysicalExpr> = if explode.outer {
+                    Arc::new(ListEmptyToNullExpr::new(raw_child_expr))
+                } else {
+                    raw_child_expr
+                };
 
-                            let pre_exec = Arc::new(ProjectionExec::try_new(
-                                pre_exprs,
-                                Arc::clone(&child.native_plan),
-                            )?);
+                // Both posexplode variants reference the array twice: once 
for positions
+                // and once for values. Materialize computed arrays so both 
references
+                // share one evaluation. A plain Column is already 
materialized.
+                let (child_expr, child_native_plan) = if explode.position
+                    && !child_expr.is::<Column>()
+                {
+                    let reserved_name = format!("__comet_explode_{}", 
child_field_name);
+                    let mut pre_exprs: Vec<(Arc<dyn PhysicalExpr>, String)> = 
child_schema
+                        .fields()
+                        .iter()
+                        .enumerate()
+                        .map(|(i, f)| {
                             (
-                                Arc::new(Column::new(&reserved_name, 
wrapped_idx))
-                                    as Arc<dyn PhysicalExpr>,
-                                pre_exec as Arc<dyn ExecutionPlan>,
+                                Arc::new(Column::new(f.name(), i)) as Arc<dyn 
PhysicalExpr>,
+                                f.name().to_string(),
                             )
-                        }
-                        (true, false) => (
-                            Arc::new(ListEmptyToNullExpr::new(raw_child_expr))
-                                as Arc<dyn PhysicalExpr>,
-                            Arc::clone(&child.native_plan),
-                        ),
-                        (false, _) => (raw_child_expr, 
Arc::clone(&child.native_plan)),
-                    };
+                        })
+                        .collect();
+                    let child_idx = pre_exprs.len();
+                    pre_exprs.push((child_expr, reserved_name.clone()));
+
+                    let pre_exec = Arc::new(ProjectionExec::try_new(
+                        pre_exprs,
+                        Arc::clone(&child.native_plan),
+                    )?);
+                    (
+                        Arc::new(Column::new(&reserved_name, child_idx)) as 
Arc<dyn PhysicalExpr>,
+                        pre_exec as Arc<dyn ExecutionPlan>,
+                    )
+                } else {
+                    (child_expr, Arc::clone(&child.native_plan))
+                };
 
                 // Create projection expressions for other columns
                 let projections: Vec<Arc<dyn PhysicalExpr>> = explode
@@ -4540,15 +4532,23 @@ fn needs_fields_coercion(sig: &TypeSignature) -> bool {
 #[cfg(test)]
 mod tests {
     use futures::{poll, StreamExt};
-    use std::{sync::Arc, task::Poll};
+    use std::{
+        sync::{
+            atomic::{AtomicUsize, Ordering},
+            Arc,
+        },
+        task::Poll,
+    };
 
     use arrow::array::{
-        Array, DictionaryArray, Int32Array, Int8Array, ListArray, RecordBatch, 
StringArray,
+        Array, ArrayRef, DictionaryArray, Int32Array, Int8Array, ListArray, 
RecordBatch,
+        StringArray,
     };
     use arrow::datatypes::{DataType, Field, FieldRef, Fields, Schema};
     use datafusion::catalog::memory::DataSourceExec;
     use datafusion::config::TableParquetOptions;
     use datafusion::datasource::listing::PartitionedFile;
+    use datafusion::datasource::memory::MemorySourceConfig;
     use datafusion::datasource::object_store::ObjectStoreUrl;
     use datafusion::datasource::physical_plan::{
         FileGroup, FileScanConfigBuilder, FileSource, ParquetSource,
@@ -4880,6 +4880,161 @@ mod tests {
         }
     }
 
+    #[tokio::test]
+    async fn explode_evaluates_array_once_per_batch() {
+        use arrow::datatypes::Int32Type;
+        use datafusion::common::tree_node::{Transformed, TreeNode};
+        use datafusion::logical_expr::{create_udf, Volatility};
+        use datafusion::physical_plan::projection::ProjectionExec;
+        use spark_expression::data_type::{data_type_info::DatatypeStruct, 
DataTypeInfo, ListInfo};
+
+        let array_type = spark_expression::DataType {
+            type_id: 14,
+            type_info: Some(Box::new(DataTypeInfo {
+                datatype_struct: Some(DatatypeStruct::List(Box::new(ListInfo {
+                    element_type: Some(Box::new(create_proto_datatype())),
+                    contains_null: true,
+                }))),
+            })),
+        };
+        let arrays = Arc::new(ListArray::from_iter_primitive::<Int32Type, _, 
_>(vec![
+            Some(vec![Some(10), None]),
+            Some(vec![]),
+            None,
+            Some(vec![Some(20)]),
+        ])) as ArrayRef;
+
+        for outer in [false, true] {
+            for position in [false, true] {
+                for computed in [false, true] {
+                    let calls = Arc::new(AtomicUsize::new(0));
+                    let counter = Arc::clone(&calls);
+                    let ctx = SessionContext::new();
+                    ctx.register_udf(create_udf(
+                        "counted_array",
+                        vec![arrays.data_type().clone()],
+                        arrays.data_type().clone(),
+                        Volatility::Immutable,
+                        Arc::new(move |args| {
+                            counter.fetch_add(1, Ordering::Relaxed);
+                            Ok(args[0].clone())
+                        }),
+                    ));
+                    let task_ctx = ctx.task_ctx();
+                    let planner = PhysicalPlanner::new(Arc::new(ctx), 0);
+                    let bound = Expr {
+                        expr_struct: 
Some(Bound(spark_expression::BoundReference {
+                            index: 0,
+                            datatype: Some(array_type.clone()),
+                        })),
+                        ..Default::default()
+                    };
+                    let child_expr = if computed {
+                        Expr {
+                            expr_struct: 
Some(ScalarFunc(spark_expression::ScalarFunc {
+                                func: "counted_array".to_string(),
+                                args: vec![bound.clone()],
+                                ..Default::default()
+                            })),
+                            ..Default::default()
+                        }
+                    } else {
+                        bound.clone()
+                    };
+                    let op = Operator {
+                        children: vec![Operator {
+                            op_struct: 
Some(OpStruct::Scan(spark_operator::Scan {
+                                fields: vec![array_type.clone()],
+                                source: String::new(),
+                            })),
+                            ..Default::default()
+                        }],
+                        op_struct: 
Some(OpStruct::Explode(spark_operator::Explode {
+                            child: Some(child_expr),
+                            outer,
+                            position,
+                            project_list: vec![bound],
+                        })),
+                        ..Default::default()
+                    };
+                    let (_, _, plan) = planner.create_plan(&op, &mut vec![], 
1).unwrap();
+                    let schema = plan.children[0].schema();
+                    let batch = RecordBatch::try_new(schema, 
vec![Arc::clone(&arrays)]).unwrap();
+                    let input: Arc<dyn ExecutionPlan> = 
MemorySourceConfig::try_new_exec(
+                        &[vec![batch.clone(), batch.clone()]],
+                        batch.schema(),
+                        None,
+                    )
+                    .unwrap();
+                    let mut projections = 0;
+                    // Replace only the JNI-fed scan, keeping the planner's 
generated operators.
+                    let native_plan = Arc::clone(&plan.native_plan)
+                        .transform_up(|node| {
+                            projections += 
usize::from(node.is::<ProjectionExec>());
+                            Ok(if node.name() == "ScanExec" {
+                                Transformed::yes(Arc::clone(&input))
+                            } else {
+                                Transformed::no(node)
+                            })
+                        })
+                        .unwrap()
+                        .data;
+                    let results = collect(native_plan.execute(0, 
task_ctx).unwrap())
+                        .await
+                        .unwrap();
+                    let context =
+                        format!("outer={outer}, position={position}, 
computed={computed}");
+                    assert_eq!(
+                        calls.load(Ordering::Relaxed),
+                        if computed { 2 } else { 0 },
+                        "{context}"
+                    );
+                    assert_eq!(
+                        projections,
+                        1 + usize::from(position && (outer || computed)),
+                        "{context}"
+                    );
+                    let expected_values = if outer {
+                        vec![Some(10), None, None, None, Some(20)]
+                    } else {
+                        vec![Some(10), None, Some(20)]
+                    };
+                    let values: Vec<_> = results
+                        .iter()
+                        .flat_map(|batch| {
+                            batch
+                                .column(batch.num_columns() - 1)
+                                .as_any()
+                                .downcast_ref::<Int32Array>()
+                                .unwrap()
+                                .iter()
+                        })
+                        .collect();
+                    assert_eq!(values, expected_values.repeat(2), "{context}");
+                    if position {
+                        let expected_positions = if outer {
+                            vec![Some(0), Some(1), None, None, Some(0)]
+                        } else {
+                            vec![Some(0), Some(1), Some(0)]
+                        };
+                        let positions: Vec<_> = results
+                            .iter()
+                            .flat_map(|batch| {
+                                batch
+                                    .column(1)
+                                    .as_any()
+                                    .downcast_ref::<Int32Array>()
+                                    .unwrap()
+                                    .iter()
+                            })
+                            .collect();
+                        assert_eq!(positions, expected_positions.repeat(2), 
"{context}");
+                    }
+                }
+            }
+        }
+    }
+
     #[test]
     fn test_create_array() {
         let session_ctx = SessionContext::new();
diff --git 
a/spark/src/test/scala/org/apache/comet/exec/CometGenerateExecSuite.scala 
b/spark/src/test/scala/org/apache/comet/exec/CometGenerateExecSuite.scala
index 7fe827fa40..ac0ca6ed22 100644
--- a/spark/src/test/scala/org/apache/comet/exec/CometGenerateExecSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/exec/CometGenerateExecSuite.scala
@@ -28,6 +28,20 @@ class CometGenerateExecSuite extends CometTestBase {
 
   import testImplicits._
 
+  test("posexplode with a computed array from Parquet") {
+    withSQLConf(CometConf.COMET_EXEC_EXPLODE_ENABLED.key -> "true") {
+      val input = Seq((1, "axb"), (2, ""), (3, null), (4, "xxc"))
+      withParquetDataFrame(input) { parquet =>
+        withParquetTable(parquet.toDF("id", "s"), "t") {
+          for (generator <- Seq("posexplode", "posexplode_outer")) {
+            val df = sql(s"SELECT id, $generator(split(s, 'x')) AS (pos, 
value) FROM t")
+            checkSparkAnswerAndOperator(df)
+          }
+        }
+      }
+    }
+  }
+
   test("explode with simple array") {
     withSQLConf(
       CometConf.COMET_EXEC_LOCAL_TABLE_SCAN_ENABLED.key -> "true",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to