You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by mb...@apache.org on 2026/06/25 20:03:10 UTC
(datafusion-comet) branch main updated: perf: unwrap identity casts in schema adapter to enable Parquet stats pruning (#4730)
This is an automated email from the ASF dual-hosted git repository.
mbutrovich pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git
The following commit(s) were added to refs/heads/main by this push:
new 2473aaa9be perf: unwrap identity casts in schema adapter to enable Parquet stats pruning (#4730)
2473aaa9be is described below
commit 2473aaa9bed30ad1d5266f02ff31dd3a62a4b380
Author: Matt Butrovich <mb...@users.noreply.github.com>
AuthorDate: Thu Jun 25 16:03:05 2026 -0400
perf: unwrap identity casts in schema adapter to enable Parquet stats pruning (#4730)
---
native/core/src/parquet/schema_adapter.rs | 15 +++++++
.../apache/comet/exec/CometNativeReaderSuite.scala | 47 +++++++++++++++++++++-
2 files changed, 61 insertions(+), 1 deletion(-)
diff --git a/native/core/src/parquet/schema_adapter.rs b/native/core/src/parquet/schema_adapter.rs
index 38bcff91a1..c6586b4681 100644
--- a/native/core/src/parquet/schema_adapter.rs
+++ b/native/core/src/parquet/schema_adapter.rs
@@ -645,6 +645,21 @@ impl SparkPhysicalExprAdapter {
};
let physical_type = input_field.data_type();
+ // Identity cast: DataFusion's default adapter inserts a CastExpr
+ // whenever the logical and physical Arrow Fields differ in any
+ // attribute (data type, nullability, or metadata), so with identical
+ // data types but mismatched nullability or metadata, we receive a
+ // no-op cast. Unwrapping is safe because Spark `Cast` with equal
+ // source and target types is value-level identity (it does not
+ // null-strip or enforce non-null), and field nullability/metadata is
+ // informational rather than computational. Leaving the wrapper in
+ // place blocks DataFusion's pruning-predicate analyzer from
+ // recognizing the column reference, defeating row-group / page-index
+ // stats pruning.
+ if physical_type == target_type {
+ return Ok(Transformed::yes(child));
+ }
+
// Reject reading a string/binary Parquet column as anything else. Spark's
// `ParquetVectorUpdaterFactory.getUpdater` BINARY case allows StringType /
// BinaryType, or DecimalType only when the column carries a
diff --git a/spark/src/test/scala/org/apache/comet/exec/CometNativeReaderSuite.scala b/spark/src/test/scala/org/apache/comet/exec/CometNativeReaderSuite.scala
index fc2adcbe6d..5b7b9c8e6a 100644
--- a/spark/src/test/scala/org/apache/comet/exec/CometNativeReaderSuite.scala
+++ b/spark/src/test/scala/org/apache/comet/exec/CometNativeReaderSuite.scala
@@ -24,12 +24,13 @@ import org.scalatest.Tag
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.Path
-import org.apache.parquet.hadoop.ParquetWriter
+import org.apache.parquet.hadoop.{ParquetFileReader, ParquetWriter}
import org.apache.parquet.hadoop.api.WriteSupport
import org.apache.parquet.hadoop.api.WriteSupport.WriteContext
import org.apache.parquet.io.api.RecordConsumer
import org.apache.parquet.schema.MessageTypeParser
import org.apache.spark.sql.{CometTestBase, Row}
+import org.apache.spark.sql.comet.CometNativeScanExec
import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
import org.apache.spark.sql.functions.{array, col}
import org.apache.spark.sql.internal.SQLConf
@@ -827,6 +828,50 @@ class CometNativeReaderSuite extends CometTestBase with AdaptiveSparkPlanHelper
}
}
}
+
+ test("row-group statistics pruning fires for native Parquet scan dataFilters") {
+ // Regression test for the identity-cast pruning gap. DataFusion's default
+ // PhysicalExprAdapter inserts a CastExpr around Column refs whenever the logical
+ // and physical Arrow Fields differ at all, including metadata or nullability with
+ // identical data types. Comet then translates that CastExpr into a Spark Cast,
+ // which DataFusion's pruning-predicate analyzer cannot peel back to a column,
+ // so build_pruning_predicates returns None and no row groups are pruned. The
+ // fix skips the wrap when source and target types are equal.
+ withTempPath { dir =>
+ withSQLConf(SQLConf.LEAF_NODE_DEFAULT_PARALLELISM.key -> "1") {
+ spark
+ .range(0, 1000)
+ .toDF("c1")
+ .repartition(1)
+ .write
+ .option("parquet.block.size", "1024")
+ .format("parquet")
+ .save(dir.toString)
+
+ val parquetFile = dir
+ .listFiles()
+ .find(_.getName.endsWith(".parquet"))
+ .getOrElse(fail("No parquet file was written"))
+ val reader = ParquetFileReader.open(
+ org.apache.parquet.hadoop.util.HadoopInputFile
+ .fromPath(new Path(parquetFile.getAbsolutePath), spark.sessionState.newHadoopConf()))
+ val numRowGroups =
+ try reader.getRowGroups.size()
+ finally reader.close()
+ assert(numRowGroups > 1, s"Test setup needs >1 row groups, got $numRowGroups")
+
+ val df = spark.read.parquet(dir.toString).where("c1 > 500")
+ val (_, cometPlan) = checkSparkAnswerAndOperator(df)
+ val nativeScans = cometPlan.collect { case n: CometNativeScanExec => n }
+ assert(nativeScans.nonEmpty, "Expected a CometNativeScanExec")
+ val scanRows = nativeScans.head.metrics("numOutputRows").value
+ assert(
+ scanRows < 1000,
+ s"Row-group statistics pruning did not fire (scan read $scanRows of 1000 rows " +
+ s"across $numRowGroups row groups)")
+ }
+ }
+ }
}
private class DirectWriteSupport(schema: org.apache.parquet.schema.MessageType)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org