felipepessoto commented on code in PR #12215:
URL: https://github.com/apache/gluten/pull/12215#discussion_r3899041447


##########
gluten-delta/src/main/scala/org/apache/gluten/extension/OffloadDeltaScan.scala:
##########
@@ -20,47 +20,80 @@ import org.apache.gluten.execution.DeltaScanTransformer
 import org.apache.gluten.extension.columnar.FallbackTags
 import org.apache.gluten.extension.columnar.offload.OffloadSingleNode
 
-import org.apache.spark.sql.delta.DeltaParquetFileFormat
-import org.apache.spark.sql.delta.SnapshotDescriptor
+import org.apache.spark.sql.delta.{DeltaParquetFileFormat, SnapshotDescriptor}
 import 
org.apache.spark.sql.delta.commands.DeletionVectorUtils.deletionVectorsReadable
-import org.apache.spark.sql.delta.files.{CdcAddFileIndex, TahoeFileIndex, 
TahoeRemoveFileIndex}
+import org.apache.spark.sql.delta.files.{CdcAddFileIndex, TahoeBatchFileIndex, 
TahoeFileIndex, TahoeRemoveFileIndex}
 import org.apache.spark.sql.delta.stats.PreparedDeltaFileIndex
 import org.apache.spark.sql.execution.{FileSourceScanExec, SparkPlan}
+import org.apache.spark.sql.types.{DataType, StructType}
 import org.apache.spark.util.SparkVersionUtil
 
-case class OffloadDeltaScan() extends OffloadSingleNode {
+case class OffloadDeltaScan(enableNativeDmlRowIndexScan: Boolean) extends 
OffloadSingleNode {
   private val DeletionVectorsUseMetadataRowIndexKey =
     "spark.databricks.delta.deletionVectors.useMetadataRowIndex"
 
+  // Spark 3.5+ exposes this as 
ParquetFileFormat.ROW_INDEX_TEMPORARY_COLUMN_NAME.
+  private val parquetTemporaryRowIndexColumnName = "_tmp_metadata_row_index"
+  private val rowIndexColumnNames =
+    Set(
+      "__delta_internal_row_index",
+      DeltaParquetFileFormat.ROW_INDEX_COLUMN_NAME,
+      parquetTemporaryRowIndexColumnName,
+      "row_index")
+  // TahoeBatchFileIndex.actionType as set by Delta's DELETE, UPDATE and MERGE 
commands.
+  private val dmlActionTypes = Set("delete", "update", "merge")
+
   override def offload(plan: SparkPlan): SparkPlan = plan match {
     case scan: FileSourceScanExec if isDeltaLogScan(scan) =>
       FallbackTags.add(scan, "fallback Delta _delta_log scan")
       scan
+    case scan: FileSourceScanExec if shouldFallbackDeletionVectorDmlScan(scan) 
=>
+      FallbackTags.add(scan, "fallback Delta DV DML row-index scan by 
configuration")
+      scan
     case scan: FileSourceScanExec if 
shouldFallbackSpark34DeletionVectorScan(scan) =>
       FallbackTags.add(scan, "fallback Spark 3.4 Delta DV scan")
       scan
     case scan: FileSourceScanExec
         if shouldFallbackDeletionVectorScanWithoutMetadataRowIndex(scan) =>
       FallbackTags.add(scan, "fallback Delta DV scan without metadata row 
index")
       scan
-    case scan: FileSourceScanExec if isDeltaScan(scan) =>
+    case scan: FileSourceScanExec if DeltaScanUtils.isDeltaScan(scan) =>
       DeltaScanTransformer(scan)
     case other => other
   }
 
-  private def isDeltaScan(scan: FileSourceScanExec): Boolean = {
-    isDeltaFileIndex(scan) || isDeltaParquetScan(scan)
+  /**
+   * The scoped escape hatch: with the config off, the DELETE/UPDATE/MERGE 
target scan that produces
+   * file paths and row indexes for deletion-vector writes stays on Spark, 
while every other scan
+   * keeps offloading. The whole check lives here, on the scan alone: Delta 
builds every DML target
+   * relation over a [[TahoeBatchFileIndex]] carrying the command name, which 
survives AQE stage
+   * splits and arbitrary join placement, and only DV-writing DML reads a 
row-index column from that
+   * relation; DML that rewrites whole files does not, and remains eligible 
for native execution.
+   */
+  private def shouldFallbackDeletionVectorDmlScan(scan: FileSourceScanExec): 
Boolean = {
+    !enableNativeDmlRowIndexScan && isDmlTargetScan(scan) && 
scanReadsRowIndexColumn(scan)
   }
 
-  private def isDeltaParquetScan(scan: FileSourceScanExec): Boolean = {
-    val fileFormatClass = scan.relation.fileFormat.getClass
-    fileFormatClass == classOf[DeltaParquetFileFormat] ||
-    fileFormatClass.getSimpleName == "GlutenDeltaParquetFileFormat"
+  private def isDmlTargetScan(scan: FileSourceScanExec): Boolean = {
+    scan.relation.location match {
+      case index: TahoeBatchFileIndex => 
dmlActionTypes.contains(index.actionType)
+      case _ => false
+    }
   }
 
-  private def isDeltaFileIndex(scan: FileSourceScanExec): Boolean = {
-    scan.relation.location.isInstanceOf[TahoeFileIndex] ||
-    scan.relation.location.isInstanceOf[PreparedDeltaFileIndex]
+  private def scanReadsRowIndexColumn(scan: FileSourceScanExec): Boolean = {
+    def nestedFieldNames(dataType: DataType): Seq[String] = dataType match {
+      case struct: StructType =>
+        struct.fields.flatMap(field => field.name +: 
nestedFieldNames(field.dataType)).toSeq
+      case _ => Seq.empty
+    }
+
+    val outputColumnNames =
+      scan.output.flatMap(attribute => attribute.name +: 
nestedFieldNames(attribute.dataType))
+    val requiredColumnNames = scan.requiredSchema.fields.flatMap {
+      field => field.name +: nestedFieldNames(field.dataType)
+    }
+    (outputColumnNames ++ 
requiredColumnNames).exists(rowIndexColumnNames.contains)

Review Comment:
   Non-blocking: `scanReadsRowIndexColumn` recursively flattens every struct 
and treats any field named `row_index` as Delta metadata. Could we narrow that 
match to the actual `_metadata.row_index` field? A regular Delta table may have 
a top-level or nested user column with that name, and non-DV DELETE/UPDATE 
paths still use a `TahoeBatchFileIndex` with action type `delete`/`update`. 
With this config disabled, those ordinary scans would therefore fall back to 
Spark even though the config promises that other scans are unaffected.
   
   For example, the check could retain exact matching for generated top-level 
columns while inspecting `row_index` only under `_metadata`:
   
   ```scala
   private val generatedRowIndexColumnNames = Set(
     "__delta_internal_row_index",
     DeltaParquetFileFormat.ROW_INDEX_COLUMN_NAME,
     parquetTemporaryRowIndexColumnName)
   
   private def isRowIndexColumn(name: String, dataType: DataType): Boolean = {
     generatedRowIndexColumnNames.contains(name) ||
     (name == "_metadata" && (dataType match {
       case struct: StructType => struct.fieldNames.contains("row_index")
       case _ => false
     }))
   }
   
   private def scanReadsRowIndexColumn(scan: FileSourceScanExec): Boolean = {
     val outputFields = scan.output.iterator.map(attr => attr.name -> 
attr.dataType)
     val requiredFields = scan.requiredSchema.fields.iterator.map(field => 
field.name -> field.dataType)
     (outputFields ++ requiredFields).exists { case (name, dataType) =>
       isRowIndexColumn(name, dataType)
     }
   }
   ```
   
   A non-DV DML regression test using a user-defined `row_index` column would 
also lock in the intended scope.



-- 
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