Skip to content

Commit 6d70f4b

Browse files
committed
[VL][Delta] Guard DV DML row-index scans
Native DELETE/UPDATE/MERGE deletion-vector support deliberately keeps the target row-index scan on Spark until native row-index execution is proven for the required Delta table shapes. Detect those DML row-index scan shapes and keep the small scan subtree (and its parent filter/project) off the native path, so the offload path stays correctness-first while the write side is built out. - add DeltaDeletionVectorDmlUtils to detect and tag Delta DV DML row-index scans (row-index / file-path / bitmap-aggregator references) - gate OffloadDeltaScan to fall back on DML row-index scans unless native Delta write and native DML row-index scan are both enabled - add keepDmlRowIndexFallbackSubtreeOnSpark post-transform rule to keep the filter/project above a fallen-back DML scan on Spark, avoiding row<->columnar transitions right before Delta's JVM bitmap path - cover the fallback shape with DeltaDeletionVectorHandoffSuite cases and a DeltaDmlRowIndexScanBenchmark for Spark 3.5 / Delta 3.3 and Spark 4.0
1 parent 2dcee79 commit 6d70f4b

8 files changed

Lines changed: 1484 additions & 18 deletions

File tree

backends-velox/src-delta/main/scala/org/apache/gluten/component/VeloxDeltaComponent.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package org.apache.gluten.component
1818

1919
import org.apache.gluten.backendsapi.velox.VeloxBackend
2020
import org.apache.gluten.config.GlutenConfig
21-
import org.apache.gluten.extension.{DeltaPostTransformRules, OffloadDeltaFilter, OffloadDeltaProject, OffloadDeltaScan}
21+
import org.apache.gluten.extension.{DeltaDeletionVectorDmlUtils, DeltaPostTransformRules, OffloadDeltaFilter, OffloadDeltaProject, OffloadDeltaScan}
2222
import org.apache.gluten.extension.columnar.heuristic.HeuristicTransform
2323
import org.apache.gluten.extension.columnar.validator.Validators
2424
import org.apache.gluten.extension.injector.Injector
@@ -40,6 +40,11 @@ class VeloxDeltaComponent extends Component {
4040
// PreprocessTableWithDVsStrategy injects the skip-row column and filter during physical
4141
// planning, DeltaPostTransformRules.nativeDeletionVectorRule strips them when the scan
4242
// offloads, and DeltaScanTransformer materializes the per-file DV payloads for Velox.
43+
//
44+
// For native DELETE/UPDATE/MERGE, the DML target row-index scan is deliberately kept on Spark
45+
// until native row-index execution is proven; tag those scans here so the post-transform rules
46+
// can keep the small subtree off the native path.
47+
legacy.injectPreTransform(_ => DeltaDeletionVectorDmlUtils.tagDmlRowIndexScans)
4348
legacy.injectTransform {
4449
c =>
4550
val offload = Seq(OffloadDeltaScan(), OffloadDeltaProject(), OffloadDeltaFilter())

backends-velox/src-delta33/test/scala/org/apache/spark/sql/delta/DeltaDeletionVectorHandoffSuite.scala

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,22 @@
1616
*/
1717
package org.apache.spark.sql.delta
1818

19-
import org.apache.gluten.execution.DeltaScanTransformer
19+
import org.apache.gluten.execution.{DeltaScanTransformer, FilterExecTransformerBase, ProjectExecTransformerBase}
20+
import org.apache.gluten.extension.DeltaDeletionVectorDmlUtils
21+
import org.apache.gluten.extension.columnar.FallbackTags
2022

2123
import org.apache.spark.sql.QueryTest
24+
import org.apache.spark.sql.delta.sources.DeltaSQLConf
2225
import org.apache.spark.sql.delta.test.{DeltaSQLCommandTest, DeltaSQLTestUtils}
26+
import org.apache.spark.sql.execution.{FileSourceScanExec, FilterExec, ProjectExec, SparkPlan}
2327
import org.apache.spark.sql.test.SharedSparkSession
2428
import org.apache.spark.tags.ExtendedSQLTest
29+
import org.apache.spark.util.SparkVersionUtil
2530

2631
import org.apache.hadoop.fs.Path
2732

33+
import java.io.File
34+
2835
@ExtendedSQLTest
2936
class DeltaDeletionVectorHandoffSuite
3037
extends QueryTest
@@ -34,6 +41,93 @@ class DeltaDeletionVectorHandoffSuite
3441

3542
import testImplicits._
3643

44+
private val DmlFallbackReason = "fallback Delta DV DML row-index scan"
45+
private val DmlRowIndexColumnNames =
46+
Seq("__delta_internal_row_index", "_tmp_metadata_row_index", "rowIndexCol")
47+
48+
private def containsDmlFallbackScan(plan: SparkPlan): Boolean = {
49+
plan.exists {
50+
case scan: FileSourceScanExec =>
51+
DeltaDeletionVectorDmlUtils.isDeletionVectorDmlRowIndexScan(scan) &&
52+
FallbackTags.getOption(scan).exists(_.reason().contains(DmlFallbackReason))
53+
case _ => false
54+
}
55+
}
56+
57+
private def hasSparkParentOverDmlFallbackScan(plan: SparkPlan): Boolean = {
58+
plan.exists {
59+
case ProjectExec(_, child) if containsDmlFallbackScan(child) => true
60+
case FilterExec(_, child) if containsDmlFallbackScan(child) => true
61+
case _ => false
62+
}
63+
}
64+
65+
private def hasNativeParentOverDmlFallbackScan(plan: SparkPlan): Boolean = {
66+
plan.exists {
67+
case project: ProjectExecTransformerBase if containsDmlFallbackScan(project.child) => true
68+
case filter: FilterExecTransformerBase if containsDmlFallbackScan(filter.child) => true
69+
case _ => false
70+
}
71+
}
72+
73+
private def containsDmlRowIndexTargetScanText(plan: SparkPlan): Boolean = {
74+
val planText = plan.treeString
75+
planText.contains("FileScan parquet") &&
76+
planText.contains("file_path") &&
77+
DmlRowIndexColumnNames.exists(planText.contains) &&
78+
(planText.contains("TahoeBatchFileIndex") || planText.contains("PreparedDeltaFileIndex"))
79+
}
80+
81+
private def captureDeletePlans(
82+
path: String,
83+
predicate: String,
84+
useMetadataRowIndex: Boolean): Seq[SparkPlan] = {
85+
var executedPlans: Seq[SparkPlan] = Seq.empty
86+
withSQLConf(
87+
DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key ->
88+
useMetadataRowIndex.toString,
89+
"spark.gluten.sql.columnar.backend.velox.delta.enableNativeWrite" -> "false",
90+
"spark.gluten.sql.delta.enableNativeDmlRowIndexScan" -> "false"
91+
) {
92+
executedPlans = DeltaTestUtils.withAllPlansCaptured(spark) {
93+
spark.sql(s"DELETE FROM delta.`$path` WHERE $predicate").collect()
94+
}.map(_.executedPlan)
95+
}
96+
executedPlans
97+
}
98+
99+
private def assertSparkDmlFallback(executedPlans: Seq[SparkPlan]): Unit = {
100+
val planText = executedPlans.map(_.treeString).mkString("\n\n")
101+
if (executedPlans.exists(containsDmlFallbackScan)) {
102+
assert(executedPlans.exists(hasSparkParentOverDmlFallbackScan), planText)
103+
assert(!executedPlans.exists(hasNativeParentOverDmlFallbackScan), planText)
104+
} else {
105+
assert(executedPlans.exists(containsDmlRowIndexTargetScanText), planText)
106+
}
107+
}
108+
109+
private def assertReadPlanAfterDmlFallback(path: String, useMetadataRowIndex: Boolean): Unit = {
110+
withSQLConf(
111+
DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key -> useMetadataRowIndex.toString) {
112+
val df = spark.read.format("delta").load(path)
113+
val executedPlan = df.queryExecution.executedPlan
114+
val planText = executedPlan.treeString
115+
if (useMetadataRowIndex) {
116+
assert(executedPlan.collect { case _: DeltaScanTransformer => true }.nonEmpty, planText)
117+
assert(!planText.contains(DmlFallbackReason), planText)
118+
} else {
119+
assert(executedPlan.collect { case _: DeltaScanTransformer => true }.isEmpty, planText)
120+
}
121+
checkAnswer(df, Seq((1, "a"), (2, "b")).toDF())
122+
}
123+
}
124+
125+
private def activeDvCardinality(path: String): Long = {
126+
val log = DeltaLog.forTable(spark, new Path(path))
127+
log.update().allFiles.collect().flatMap(
128+
file => Option(file.deletionVector).map(_.cardinality)).sum
129+
}
130+
37131
test("Spark 3.5 Delta DV scan handoff should filter deleted rows") {
38132
withTempDir {
39133
tempDir =>
@@ -65,4 +159,70 @@ class DeltaDeletionVectorHandoffSuite
65159
checkAnswer(df, Seq((1, "a"), (2, "b")).toDF())
66160
}
67161
}
162+
163+
Seq(true, false).foreach {
164+
useMetadataRowIndex =>
165+
test(
166+
"Delta DV DML row-index scan should fall back with Spark project/filter, " +
167+
s"metadata row index=$useMetadataRowIndex") {
168+
assume(SparkVersionUtil.gteSpark35, "DML row-index scan fallback is Spark 3.5+ coverage")
169+
withTempDir {
170+
tempDir =>
171+
val path = tempDir.getCanonicalPath
172+
Seq((1, "a"), (2, "b"), (3, "c"), (4, "d"))
173+
.toDF("id", "value")
174+
.coalesce(1)
175+
.write
176+
.format("delta")
177+
.save(path)
178+
179+
spark.sql(
180+
s"ALTER TABLE delta.`$path` SET TBLPROPERTIES " +
181+
"('delta.enableDeletionVectors' = true)")
182+
183+
var executedPlans: Seq[SparkPlan] = Seq.empty
184+
withSQLConf(
185+
DeltaSQLConf.DELETION_VECTORS_USE_METADATA_ROW_INDEX.key ->
186+
useMetadataRowIndex.toString,
187+
"spark.gluten.sql.columnar.backend.velox.delta.enableNativeWrite" -> "false",
188+
"spark.gluten.sql.delta.enableNativeDmlRowIndexScan" -> "false"
189+
) {
190+
executedPlans = DeltaTestUtils.withAllPlansCaptured(spark) {
191+
spark.sql(s"DELETE FROM delta.`$path` WHERE id IN (3, 4)").collect()
192+
}.map(_.executedPlan)
193+
}
194+
assertSparkDmlFallback(executedPlans)
195+
196+
val log = DeltaLog.forTable(spark, new Path(path))
197+
assert(log.update().allFiles.collect().exists(_.deletionVector != null))
198+
assertReadPlanAfterDmlFallback(path, useMetadataRowIndex)
199+
}
200+
}
201+
}
202+
203+
test("Delta DV DML row-index scan should fall back when updating an existing DV") {
204+
assume(SparkVersionUtil.gteSpark35, "DML row-index scan fallback is Spark 3.5+ coverage")
205+
withTempDir {
206+
tempDir =>
207+
val path = new File(tempDir, "delta table with spaces").getCanonicalPath
208+
Seq((1, "a"), (2, "b"), (3, "c"), (4, "d"), (5, "e"), (6, "f"))
209+
.toDF("id", "value")
210+
.coalesce(1)
211+
.write
212+
.format("delta")
213+
.save(path)
214+
215+
spark.sql(
216+
s"ALTER TABLE delta.`$path` SET TBLPROPERTIES " +
217+
"('delta.enableDeletionVectors' = true)")
218+
219+
assertSparkDmlFallback(captureDeletePlans(path, "id IN (5, 6)", useMetadataRowIndex = true))
220+
assert(activeDvCardinality(path) === 2L)
221+
222+
assertSparkDmlFallback(captureDeletePlans(path, "id IN (3, 4)", useMetadataRowIndex = true))
223+
assert(activeDvCardinality(path) === 4L)
224+
225+
assertReadPlanAfterDmlFallback(path, useMetadataRowIndex = true)
226+
}
227+
}
68228
}

0 commit comments

Comments
 (0)