1616 */
1717package 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
2123import org .apache .spark .sql .QueryTest
24+ import org .apache .spark .sql .delta .sources .DeltaSQLConf
2225import org .apache .spark .sql .delta .test .{DeltaSQLCommandTest , DeltaSQLTestUtils }
26+ import org .apache .spark .sql .execution .{FileSourceScanExec , FilterExec , ProjectExec , SparkPlan }
2327import org .apache .spark .sql .test .SharedSparkSession
2428import org .apache .spark .tags .ExtendedSQLTest
29+ import org .apache .spark .util .SparkVersionUtil
2530
2631import org .apache .hadoop .fs .Path
2732
33+ import java .io .File
34+
2835@ ExtendedSQLTest
2936class 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