peterxcli commented on code in PR #5699:
URL: https://github.com/apache/datafusion-comet/pull/5699#discussion_r3942940080


##########
native/core/src/execution/operators/dynamic_filter.rs:
##########
@@ -0,0 +1,572 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Connect a hash join's completed build domain to its probe input.
+//!
+//! Comet does not run DataFusion's physical optimizer, which normally connects
+//! dynamic-filter producers and consumers. This targeted wiring filters probe
+//! batches and lets a direct Parquet reader use the same live predicate for
+//! pruning. The original join verifies matches, including hash collisions.
+//! This leaves Spark's operator tree and partitioning intact and does
+//! not cross Spark exchanges or JVM/Arrow boundaries.
+
+use std::fmt::Formatter;
+use std::sync::Arc;
+
+use arrow::compute::filter_record_batch;
+use arrow::datatypes::DataType;
+use datafusion::common::cast::as_boolean_array;
+use datafusion::common::config::ConfigOptions;
+use datafusion::common::tree_node::TreeNodeRecursion;
+use datafusion::common::{internal_err, JoinType, NullEquality, Result, 
ScalarValue, Statistics};
+use datafusion::datasource::physical_plan::ParquetSource;
+use datafusion::datasource::source::DataSourceExec;
+use datafusion::execution::TaskContext;
+use datafusion::logical_expr::{ColumnarValue, Operator};
+use datafusion::physical_expr::expressions::{
+    lit, BinaryExpr, Column, DynamicFilterPhysicalExpr, IsNotNullExpr,
+};
+use datafusion::physical_expr::PhysicalExpr;
+use 
datafusion::physical_plan::distribution_requirements::InputDistributionRequirements;
+use datafusion::physical_plan::execution_plan::CardinalityEffect;
+use datafusion::physical_plan::joins::{HashJoinExec, PartitionMode};
+use datafusion::physical_plan::metrics::{ExecutionPlanMetricsSet, 
MetricBuilder, MetricsSet};
+use datafusion::physical_plan::statistics::{ChildStats, StatisticsArgs};
+use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
+use datafusion::physical_plan::{
+    apply_expression_roots, ChildrenPropertiesMode, DisplayAs, 
DisplayFormatType, ExecutionPlan,
+    ExecutionPlanProperties, PlanProperties, ReplaceChildrenOptions, 
SendableRecordBatchStream,
+};
+use futures::StreamExt;
+
+use super::CometFilterExec;
+
+/// A task-local consumer of DataFusion's build-side runtime filter.
+#[derive(Debug)]
+pub(crate) struct DynamicFilterExec {
+    input: Arc<dyn ExecutionPlan>,
+    predicate: Arc<DynamicFilterPhysicalExpr>,
+    metrics: ExecutionPlanMetricsSet,
+}
+
+impl DynamicFilterExec {
+    fn new(input: Arc<dyn ExecutionPlan>, predicate: 
Arc<DynamicFilterPhysicalExpr>) -> Self {
+        Self {
+            input,
+            predicate,
+            metrics: ExecutionPlanMetricsSet::new(),
+        }
+    }
+}
+
+impl DisplayAs for DynamicFilterExec {
+    fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> 
std::fmt::Result {
+        write!(f, "CometDynamicFilterExec")
+    }
+}
+
+impl ExecutionPlan for DynamicFilterExec {
+    fn name(&self) -> &str {
+        "CometDynamicFilterExec"
+    }
+
+    fn properties(&self) -> &Arc<PlanProperties> {
+        // Removing rows preserves the input's schema, ordering and 
partitioning.
+        self.input.properties()
+    }
+
+    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
+        vec![&self.input]
+    }
+
+    fn apply_expressions(
+        &self,
+        f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
+    ) -> Result<TreeNodeRecursion> {
+        apply_expression_roots([Arc::clone(&self.predicate) as Arc<dyn 
PhysicalExpr>], f)
+    }
+
+    fn maintains_input_order(&self) -> Vec<bool> {
+        vec![true]
+    }
+
+    fn cardinality_effect(&self) -> CardinalityEffect {
+        CardinalityEffect::LowerEqual
+    }
+
+    fn with_new_children(
+        self: Arc<Self>,
+        children: Vec<Arc<dyn ExecutionPlan>>,
+    ) -> Result<Arc<dyn ExecutionPlan>> {
+        self.replace_children(
+            children,
+            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
+        )
+    }
+
+    fn replace_children(
+        self: Arc<Self>,
+        mut children: Vec<Arc<dyn ExecutionPlan>>,
+        _options: ReplaceChildrenOptions,
+    ) -> Result<Arc<dyn ExecutionPlan>> {
+        if children.len() != 1 {
+            return internal_err!("CometDynamicFilterExec requires one child");
+        }
+        Ok(Arc::new(Self::new(
+            children.remove(0),
+            Arc::clone(&self.predicate),
+        )))
+    }
+
+    fn reset_state(self: Arc<Self>) -> Result<Arc<dyn ExecutionPlan>> {
+        // HashJoinExec resets its producer on reexecution. Never retain a 
previous
+        // build's domain in the consumer. A reset plan safely bypasses 
filtering;
+        // ordinary Spark task attempts each construct a fresh, connected plan.
+        let predicate = Arc::new(DynamicFilterPhysicalExpr::new(
+            self.predicate.children().into_iter().cloned().collect(),
+            lit(true),
+        ));
+        Ok(Arc::new(Self::new(Arc::clone(&self.input), predicate)))
+    }
+
+    fn execute(
+        &self,
+        partition: usize,
+        context: Arc<TaskContext>,
+    ) -> Result<SendableRecordBatchStream> {
+        let children = self.predicate.children();
+        let [key] = children.as_slice() else {
+            return internal_err!("CometDynamicFilterExec requires one join-key 
column");
+        };
+        let Some(key) = key.downcast_ref::<Column>() else {
+            return internal_err!("CometDynamicFilterExec requires a direct 
join-key column");
+        };
+        let key_index = key.index();
+        let predicate = Arc::clone(&self.predicate)
+            .with_new_children(vec![Arc::new(Column::new(key.name(), 0))])?;
+        let input = self.input.execute(partition, context)?;
+        let evaluated =
+            
MetricBuilder::new(&self.metrics).counter("dynamic_filter_rows_evaluated", 
partition);
+        let pruned =
+            
MetricBuilder::new(&self.metrics).counter("dynamic_filter_rows_pruned", 
partition);
+        let bypassed =
+            
MetricBuilder::new(&self.metrics).counter("dynamic_filter_rows_bypassed", 
partition);
+        // Only dedicated metrics: merging this helper into the Spark join 
must not
+        // add its input/output counts or elapsed time to the join's existing 
metrics.
+        let eval_time =
+            
MetricBuilder::new(&self.metrics).subset_time("dynamic_filter_eval_time", 
partition);
+        let stream = input.map(move |batch| {
+            let batch = batch?;
+            let _timer = eval_time.timer();
+            // AND may prefilter its input before evaluating hash membership. A
+            // zero-copy key projection keeps payload columns out of that 
temporary
+            // batch. The remapped expression still observes live producer 
updates.
+            let key_batch = batch.project(&[key_index])?;
+            match predicate.evaluate(&key_batch)? {
+                // DataFusion leaves this placeholder unchanged until the 
complete
+                // build is available, or if it declines to populate the 
filter.
+                ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))) => {
+                    bypassed.add(batch.num_rows());
+                    Ok(batch)
+                }
+                ColumnarValue::Scalar(ScalarValue::Boolean(Some(false) | 
None)) => {
+                    evaluated.add(batch.num_rows());
+                    pruned.add(batch.num_rows());
+                    Ok(batch.slice(0, 0))
+                }
+                ColumnarValue::Array(mask) => {
+                    let filtered = filter_record_batch(&batch, 
as_boolean_array(&mask)?)?;
+                    evaluated.add(batch.num_rows());
+                    pruned.add(batch.num_rows() - filtered.num_rows());
+                    Ok(filtered)
+                }
+                _ => internal_err!("Join dynamic filter must evaluate to a 
Boolean"),
+            }
+        });
+        // Return even empty batches. Each poll consumes at most one input 
batch,
+        // so a selective filter cannot drain a ready input in an unbounded 
loop.
+        Ok(Box::pin(RecordBatchStreamAdapter::new(
+            self.schema(),
+            stream,
+        )))
+    }
+
+    fn metrics(&self) -> Option<MetricsSet> {
+        Some(self.metrics.clone_inner())
+    }
+}
+
+/// A permanent plan must not own a completed join's filter or build 
accumulator:
+/// those can retain the hash map after its stream-owned reservation is 
released.
+/// Keep an unexecuted template here and create the producer and consumer 
together
+/// for each stream. Only their metric handles are retained by the Spark plan.
+#[derive(Debug)]
+pub(crate) struct DynamicFilterJoinExec {
+    template: HashJoinExec,
+    config: ConfigOptions,
+    metrics: ExecutionPlanMetricsSet,
+}
+
+/// Per-execution join state. The permanent plan keeps no live filter; this 
value
+/// records whether this execution also connected its filter to the Parquet 
reader.
+struct RuntimeDynamicFilterJoin {
+    join: HashJoinExec,
+    reader_filter_attached: bool,
+}
+
+/// Recognize only direct-column null checks joined by AND, without evaluating
+/// or changing the predicate. Every accepted leaf is deterministic, 
infallible,
+/// and only discards rows, so reader pruning cannot suppress expression errors
+/// or alter stateful evaluation. All other expressions remain a boundary.
+fn is_direct_column_null_checks(predicate: &Arc<dyn PhysicalExpr>) -> bool {
+    if let Some(binary) = predicate.downcast_ref::<BinaryExpr>() {
+        return binary.op() == &Operator::And
+            && is_direct_column_null_checks(binary.left())
+            && is_direct_column_null_checks(binary.right());
+    }
+    predicate
+        .downcast_ref::<IsNotNullExpr>()
+        .is_some_and(|is_not_null| is_not_null.arg().is::<Column>())
+}
+
+fn try_attach_parquet_reader_filter(

Review Comment:
   sorry I didnt see this before, but I think this could be move to planner, 
too.
   and maybe is time to split the planner.rs into multiple rule files.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to