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

zhouyuan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 9f6dcb599d [GLUTEN-12273][CORE] Support Iceberg staged scans (#12962)
9f6dcb599d is described below

commit 9f6dcb599d3ebb8ef9f55450163aae4f025300e9
Author: inf <[email protected]>
AuthorDate: Mon Sep 14 13:41:55 2026 +0000

    [GLUTEN-12273][CORE] Support Iceberg staged scans (#12962)
---
 .../gluten/execution/IcebergScanTransformer.scala  |   2 +-
 .../spark/source/GlutenIcebergSourceUtil.scala     | 150 ++++++++++-----------
 .../org/apache/gluten/execution/IcebergSuite.scala |  70 ++++++++++
 3 files changed, 146 insertions(+), 76 deletions(-)

diff --git 
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
 
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
index 6735c92d85..6ac22b956a 100644
--- 
a/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
+++ 
b/gluten-iceberg/src/main/scala/org/apache/gluten/execution/IcebergScanTransformer.scala
@@ -363,7 +363,7 @@ object IcebergScanTransformer {
   }
 
   def supportsBatchScan(scan: Scan): Boolean = {
-    scan.getClass == GlutenIcebergSourceUtil.getClassOfSparkBatchQueryScan
+    GlutenIcebergSourceUtil.isSupportedScan(scan)
   }
 
   private def containsUuidOrFixedType(dataType: Type): Boolean = {
diff --git 
a/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/GlutenIcebergSourceUtil.scala
 
b/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/GlutenIcebergSourceUtil.scala
index db1bb024af..233c06c30c 100644
--- 
a/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/GlutenIcebergSourceUtil.scala
+++ 
b/gluten-iceberg/src/main/scala/org/apache/iceberg/spark/source/GlutenIcebergSourceUtil.scala
@@ -31,7 +31,7 @@ import org.apache.spark.sql.types.StructType
 import org.apache.iceberg._
 import org.apache.iceberg.spark.SparkSchemaUtil
 
-import java.lang.{Class, Long => JLong}
+import java.lang.{Long => JLong}
 import java.util.{ArrayList => JArrayList, HashMap => JHashMap, List => JList, 
Map => JMap}
 import java.util.Locale
 
@@ -42,10 +42,13 @@ object GlutenIcebergSourceUtil {
   private val InputFileBlockStartCol = "input_file_block_start"
   private val InputFileBlockLengthCol = "input_file_block_length"
 
-  def getClassOfSparkBatchQueryScan(): Class[SparkBatchQueryScan] = {
-    classOf[SparkBatchQueryScan]
+  def isSupportedScan(sparkScan: Scan): Boolean = sparkScan match {
+    case _: SparkBatchQueryScan | _: SparkStagedScan => true
+    case _ => false
   }
 
+  def isSparkStagedScan(sparkScan: Scan): Boolean = 
sparkScan.isInstanceOf[SparkStagedScan]
+
   def deleteExists(p: SparkDataSourceRDDPartition): Boolean = {
     p.inputPartitions.exists {
       case ip: SparkInputPartition =>
@@ -114,32 +117,22 @@ object GlutenIcebergSourceUtil {
 
   def getFieldIds(sparkScan: Scan): JHashMap[String, Integer] = {
     val fieldIds = new JHashMap[String, Integer]()
-    sparkScan match {
-      case scan: SparkBatchQueryScan =>
-        scan.table().schema().columns().asScala.foreach {
-          field => fieldIds.put(field.name(), field.fieldId())
-        }
-      case _ =>
-        throw new GlutenNotSupportException("Only support iceberg 
SparkBatchQueryScan.")
+    getTable(sparkScan).schema().columns().asScala.foreach {
+      field => fieldIds.put(field.name(), field.fieldId())
     }
     fieldIds
   }
 
   def getInitialDefaults(sparkScan: Scan): JHashMap[String, String] = {
     val initialDefaults = new JHashMap[String, String]()
-    sparkScan match {
-      case scan: SparkBatchQueryScan =>
-        scan.table().schema().columns().asScala.foreach {
-          field =>
-            val defaultValue = IcebergDefaultValueUtil.getInitialDefault(field)
-            if (defaultValue != null) {
-              initialDefaults.put(
-                field.name(),
-                TypeUtil.getPartitionValueString(field.`type`(), defaultValue))
-            }
+    getTable(sparkScan).schema().columns().asScala.foreach {
+      field =>
+        val defaultValue = IcebergDefaultValueUtil.getInitialDefault(field)
+        if (defaultValue != null) {
+          initialDefaults.put(
+            field.name(),
+            TypeUtil.getPartitionValueString(field.`type`(), defaultValue))
         }
-      case _ =>
-        throw new GlutenNotSupportException("Only support iceberg 
SparkBatchQueryScan.")
     }
     initialDefaults
   }
@@ -162,65 +155,72 @@ object GlutenIcebergSourceUtil {
     metadataColumns
   }
 
-  def getFileFormat(sparkScan: Scan): ReadFileFormat = sparkScan match {
-    case scan: SparkBatchQueryScan =>
-      val tasks = scan.tasks().asScala
-      asFileScanTask(tasks.toList).foreach {
-        task =>
-          task.file().format() match {
-            case FileFormat.PARQUET => return ReadFileFormat.ParquetReadFormat
-            case FileFormat.ORC => return ReadFileFormat.OrcReadFormat
-            case _ =>
-          }
-      }
-      throw new GlutenNotSupportException("Iceberg Only support parquet and 
orc file format.")
-    case _ =>
-      throw new GlutenNotSupportException("Only support iceberg 
SparkBatchQueryScan.")
+  def getFileFormat(sparkScan: Scan): ReadFileFormat = {
+    asFileScanTask(getScanTasks(sparkScan)).foreach {
+      task =>
+        task.file().format() match {
+          case FileFormat.PARQUET => return ReadFileFormat.ParquetReadFormat
+          case FileFormat.ORC => return ReadFileFormat.OrcReadFormat
+          case _ =>
+        }
+    }
+    throw new GlutenNotSupportException("Iceberg Only support parquet and orc 
file format.")
   }
 
-  def getReadPartitionSchema(sparkScan: Scan): StructType = sparkScan match {
-    case scan: SparkBatchQueryScan =>
-      val tasks = scan.tasks().asScala
-      asFileScanTask(tasks.toList).foreach {
-        task =>
-          val spec = task.spec()
-          if (spec.isPartitioned) {
-            val readFields = scan.readSchema().fields.map(_.name).toSet
-            // Iceberg will generate some non-table fields as partition 
fields, such as x_bucket,
-            // which will not appear in readFields, they also cannot be 
filtered.
-            val tableFields = 
spec.schema().columns().asScala.map(_.name()).toSet
-            val voidTransformFields = scan
-              .table()
-              .spec()
+  def getReadPartitionSchema(sparkScan: Scan): StructType = {
+    asFileScanTask(getScanTasks(sparkScan)).foreach {
+      task =>
+        val spec = task.spec()
+        if (spec.isPartitioned) {
+          val readFields = sparkScan.readSchema().fields.map(_.name).toSet
+          // Iceberg will generate some non-table fields as partition fields, 
such as x_bucket,
+          // which will not appear in readFields, they also cannot be filtered.
+          val tableFields = spec.schema().columns().asScala.map(_.name()).toSet
+          val voidTransformFields = getTable(sparkScan)
+            .spec()
+            .fields()
+            .asScala
+            .filter(
+              f => {
+                f.transform().isVoid
+              })
+            .map(_.name())
+            .toSet
+          val partitionFields =
+            spec
+              .partitionType()
               .fields()
               .asScala
-              .filter(
-                f => {
-                  f.transform().isVoid
-                })
-              .map(_.name())
-              .toSet
-            val partitionFields =
-              spec
-                .partitionType()
-                .fields()
-                .asScala
-                .filter(f => !tableFields.contains(f.name) || 
readFields.contains(f.name()))
-                .filter(f => !voidTransformFields.contains(f.name()))
-            partitionFields.foreach {
-              field => 
TypeUtil.validatePartitionColumnType(field.`type`().typeId())
-            }
-
-            val icebergSchema = new Schema(partitionFields.toList.asJava)
-            return SparkSchemaUtil.convert(icebergSchema)
-          } else {
-            return new StructType()
+              .filter(f => !tableFields.contains(f.name) || 
readFields.contains(f.name()))
+              .filter(f => !voidTransformFields.contains(f.name()))
+          partitionFields.foreach {
+            field => 
TypeUtil.validatePartitionColumnType(field.`type`().typeId())
           }
-      }
-      throw new UnsupportedOperationException(
-        "Failed to get partition schema from iceberg SparkBatchQueryScan.")
+
+          val icebergSchema = new Schema(partitionFields.toList.asJava)
+          return SparkSchemaUtil.convert(icebergSchema)
+        } else {
+          return new StructType()
+        }
+    }
+    throw new UnsupportedOperationException("Failed to get partition schema 
from iceberg scan.")
+  }
+
+  private def getTable(sparkScan: Scan): Table = sparkScan match {
+    case scan: SparkBatchQueryScan => scan.table()
+    case scan: SparkStagedScan => scan.table()
+    case _ =>
+      throw new GlutenNotSupportException(
+        s"Unsupported Iceberg scan: ${sparkScan.getClass.getName}.")
+  }
+
+  private def getScanTasks(sparkScan: Scan): List[ScanTask] = sparkScan match {
+    case scan: SparkBatchQueryScan => scan.tasks().asScala.toList
+    case scan: SparkStagedScan =>
+      scan.taskGroups().asScala.flatMap(_.tasks().asScala).toList
     case _ =>
-      throw new UnsupportedOperationException("Only support iceberg 
SparkBatchQueryScan.")
+      throw new GlutenNotSupportException(
+        s"Unsupported Iceberg scan: ${sparkScan.getClass.getName}.")
   }
 
   private def asFileScanTask(tasks: List[ScanTask]): List[FileScanTask] = {
diff --git 
a/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala 
b/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala
index 0ee193ce0f..5a194fc9a9 100644
--- 
a/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala
+++ 
b/gluten-iceberg/src/test/scala/org/apache/gluten/execution/IcebergSuite.scala
@@ -20,7 +20,13 @@ import org.apache.gluten.config.GlutenIcebergConfig
 
 import org.apache.spark.SparkConf
 import org.apache.spark.sql.Row
+import org.apache.spark.sql.execution.QueryExecution
 import org.apache.spark.sql.execution.datasources.v2.BatchScanExec
+import org.apache.spark.sql.util.QueryExecutionListener
+
+import org.apache.iceberg.spark.source.GlutenIcebergSourceUtil
+
+import java.util.concurrent.{CountDownLatch, TimeUnit}
 
 abstract class IcebergSuite extends WholeStageTransformerSuite {
   protected val rootPath: String = getClass.getResource("/").getPath
@@ -60,6 +66,70 @@ abstract class IcebergSuite extends 
WholeStageTransformerSuite {
     }
   }
 
+  test("rewrite_data_files uses an iceberg staged scan transformer") {
+    val tableName = "iceberg_rewrite_tb"
+    withTable(tableName) {
+      withSQLConf("spark.sql.adaptive.enabled" -> "false") {
+        spark.sql(s"CREATE TABLE $tableName (id INT, data STRING) USING 
iceberg")
+        (1 to 5).foreach {
+          id => spark.sql(s"INSERT INTO $tableName VALUES ($id, 'value-$id')")
+        }
+
+        def dataFileCount: Long =
+          spark.table(s"spark_catalog.default.$tableName.files").count()
+
+        assert(dataFileCount == 5)
+        val stagedScanSeen = new CountDownLatch(1)
+        val listener = new QueryExecutionListener {
+          override def onSuccess(
+              funcName: String,
+              qe: QueryExecution,
+              durationNs: Long): Unit = {
+            if (
+              qe.executedPlan.exists {
+                case scan: IcebergScanTransformer =>
+                  GlutenIcebergSourceUtil.isSparkStagedScan(scan.scan)
+                case _ => false
+              }
+            ) {
+              stagedScanSeen.countDown()
+            }
+          }
+
+          override def onFailure(
+              funcName: String,
+              qe: QueryExecution,
+              exception: Exception): Unit = {}
+        }
+
+        try {
+          spark.listenerManager.register(listener)
+          val result = spark
+            .sql(s"""
+                    |CALL spark_catalog.system.rewrite_data_files(
+                    |  table => 'default.$tableName',
+                    |  options => map('min-input-files', '2'))
+                    |""".stripMargin)
+            .collect()
+
+          assert(result.length == 1)
+          assert(result.head.getInt(0) == 5)
+          assert(result.head.getInt(1) == 1)
+          assert(
+            stagedScanSeen.await(10, TimeUnit.SECONDS),
+            "Rewrite read did not use IcebergScanTransformer with 
SparkStagedScan")
+        } finally {
+          spark.listenerManager.unregister(listener)
+        }
+
+        assert(dataFileCount == 1)
+        checkAnswer(
+          spark.sql(s"SELECT * FROM $tableName ORDER BY id"),
+          (1 to 5).map(id => Row(id, s"value-$id")))
+      }
+    }
+  }
+
   test("iceberg input_file_name") {
     withTable("iceberg_input_file_tb") {
       spark.sql("""


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

Reply via email to