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

zhztheplayer 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 020211a4b4 [GLUTEN-12743][VL] Preserve transaction observers in native 
Delta commands (#13134)
020211a4b4 is described below

commit 020211a4b4c132f8133d0b3786b2d1928d28f0bf
Author: Felipe Pessoto <[email protected]>
AuthorDate: Thu Oct 15 08:31:57 2026 -0700

    [GLUTEN-12743][VL] Preserve transaction observers in native Delta commands 
(#13134)
    
    * [GLUTEN-12743][VL] Preserve transaction observers in native Delta commands
    
    Delegate native transaction creation and lifecycle callbacks to the 
existing Delta observer. Preserve next-observer advancement and nested command 
scopes, and add lifecycle and native DELETE regressions.
    
    Co-authored-by: Copilot <[email protected]>
    
    * [GLUTEN-12743][VL] Keep observer integration coverage in gluten-delta
    
    Export the native DELETE regression as an abstract gluten-delta suite and 
run it through the existing Velox Delta test-jar dependency. Remove the added 
gluten-ut dependency and use a concrete Velox suite instead.
    
    Co-authored-by: Copilot <[email protected]>
    
    * [GLUTEN-12743][VL] Share observer tests and refresh Delta baseline
    
    Remove the 64 observer-related cases confirmed passing in Delta CI run 
36155269247, retaining the remaining footer-metadata failure. Share the 
unchanged observer regression suites from src-delta33 through existing-style 
src-delta40 symlinks, with both Spark 3.5 and Spark 4.0 coverage.
    
    Co-authored-by: Copilot <[email protected]>
    
    * [GLUTEN-12743][VL] Address observer regression review comments
    
    Keep native DELETE coverage in the Velox suite with typed references and 
assert a data-file rewrite. Tag both suites for extended CI, use the supported 
transaction API, and document the Delta 4.3 observer getter upgrade requirement.
    
    Co-authored-by: Copilot <[email protected]>
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 .../util/delta-spark-ut/known-failures.txt         |  64 ------
 .../datasources/v2/DeltaWriteOperators.scala       |  83 +++++--
 .../sql/delta/DeltaTransactionObserverSuite.scala  | 249 +++++++++++++++++++++
 .../delta/VeloxDeltaTransactionObserverSuite.scala |  88 ++++++++
 .../sql/delta/DeltaTransactionObserverSuite.scala  |   1 +
 .../delta/VeloxDeltaTransactionObserverSuite.scala |   1 +
 6 files changed, 406 insertions(+), 80 deletions(-)

diff --git a/.github/workflows/util/delta-spark-ut/known-failures.txt 
b/.github/workflows/util/delta-spark-ut/known-failures.txt
index 51bbf2b9ed..a8b68d06be 100644
--- a/.github/workflows/util/delta-spark-ut/known-failures.txt
+++ b/.github/workflows/util/delta-spark-ut/known-failures.txt
@@ -65,7 +65,6 @@ org.apache.spark.sql.delta.ConvertToDeltaSQLSuite#external 
tables use correct pa
 org.apache.spark.sql.delta.ConvertToDeltaScalaSuite#external tables use 
correct path scheme
 org.apache.spark.sql.delta.DeleteMetricsSuite#delete-metrics: delete one row 
per file - Partitioned = false, cdfEnabled = false
 org.apache.spark.sql.delta.DeleteMetricsSuite#delete-metrics: delete one row 
per file - Partitioned = false, cdfEnabled = true
-org.apache.spark.sql.delta.DeltaAllFilesInCrcSuite#test all-files-in-crc 
verification failure also triggers and logs incremental-commit verification 
result
 org.apache.spark.sql.delta.DeltaCDCIdColumnMappingSuite#filters with special 
characters in name should be pushed down - column mapping id mode
 org.apache.spark.sql.delta.DeltaCDCNameColumnMappingSuite#filters with special 
characters in name should be pushed down - column mapping name mode
 org.apache.spark.sql.delta.DeltaCDCSQLIdColumnMappingSuite#filters with 
special characters in name should be pushed down - column mapping id mode
@@ -266,15 +265,7 @@ 
org.apache.spark.sql.delta.DescribeDeltaHistoryWithCatalogOwnedBatch100Suite#mer
 
org.apache.spark.sql.delta.DescribeDeltaHistoryWithCatalogOwnedBatch100Suite#merge-metrics:
 upsert and delete with conditions - Partitioned = true, CDF = false
 
org.apache.spark.sql.delta.DescribeDeltaHistoryWithCatalogOwnedBatch100Suite#merge-metrics:
 upsert and delete with conditions - Partitioned = true, CDF = true
 
org.apache.spark.sql.delta.DescribeDeltaHistoryWithCatalogOwnedBatch100Suite#operation
 metrics - merge
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Disable Deletion 
Vectors feature - withUnset: false
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Disable Deletion 
Vectors feature - withUnset: true
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Disable row 
tracking feature - withUnset: false
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Disable row 
tracking feature - withUnset: true
 org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Enable column 
mapping feature - txnInterleaved: true
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Enable deletion 
vectors feature
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Enable row 
tracking feature concurrent txn: delete
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Removing column 
mapping mode produces conflict - startMode: id
-org.apache.spark.sql.delta.FeatureEnablementConcurrencySuite#Removing column 
mapping mode produces conflict - startMode: name
 org.apache.spark.sql.delta.FileSizeHistogramSuite#check CommitStats with 
deletes
 org.apache.spark.sql.delta.FileSizeHistogramSuite#histogram is re-calculated 
when files are removed
 
org.apache.spark.sql.delta.GeneratedColumnSuite#update_generated_column_with_incorrect_value
@@ -390,49 +381,6 @@ 
org.apache.spark.sql.delta.SnapshotManagementWithCoordinatedCommitsBatch1Suite#s
 
org.apache.spark.sql.delta.SnapshotManagementWithCoordinatedCommitsBatch2Suite#recover
 from a corrupt checkpoint: previous checkpoint doesn't exist
 
org.apache.spark.sql.delta.SnapshotManagementWithCoordinatedCommitsBatch2Suite#should
 not recover when both the current and previous checkpoints are broken
 org.apache.spark.sql.delta.UpdateMetricsSuite#update-metrics: update one row 
per file - Partitioned = false, cdfEnabled = false
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#DELETE
 - Scenario 1
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#DELETE
 - Scenario 2
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#DELETE
 - Scenario 3
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#DELETE
 - Scenario 4
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#DELETE
 - Scenario 5
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#DELETE
 - Scenario 6
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#DELETE
 - Scenario 7
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#OPTIMIZE
 - Scenario 1
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#OPTIMIZE
 - Scenario 2
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#OPTIMIZE
 - Scenario 3
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#OPTIMIZE
 - Scenario 4
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#OPTIMIZE
 - Scenario 5
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#OPTIMIZE
 - Scenario 6
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#OPTIMIZE
 - Scenario 7
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#UPDATE
 - Scenario 1
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#UPDATE
 - Scenario 2
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#UPDATE
 - Scenario 3
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#UPDATE
 - Scenario 4
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#UPDATE
 - Scenario 5
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#UPDATE
 - Scenario 6
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsDVSuite#UPDATE
 - Scenario 7
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#DELETE
 - Scenario 1
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#DELETE
 - Scenario 2
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#DELETE
 - Scenario 3
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#DELETE
 - Scenario 4
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#DELETE
 - Scenario 5
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#DELETE
 - Scenario 6
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#DELETE
 - Scenario 7
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#OPTIMIZE
 - Scenario 1
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#OPTIMIZE
 - Scenario 2
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#OPTIMIZE
 - Scenario 3
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#OPTIMIZE
 - Scenario 4
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#OPTIMIZE
 - Scenario 5
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#OPTIMIZE
 - Scenario 6
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#OPTIMIZE
 - Scenario 7
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#UPDATE
 - Scenario 1
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#UPDATE
 - Scenario 2
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#UPDATE
 - Scenario 3
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#UPDATE
 - Scenario 4
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#UPDATE
 - Scenario 5
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#UPDATE
 - Scenario 6
-org.apache.spark.sql.delta.commands.backfill.RowTrackingBackfillConflictsSuite#UPDATE
 - Scenario 7
-org.apache.spark.sql.delta.concurrency.TransactionExecutionObserverSuite#Phase 
Locking - delete command
 
org.apache.spark.sql.delta.coordinatedcommits.CoordinatedCommitsSuite#Incomplete
 backfills are handled properly by next commit after CC to FS conversion
 org.apache.spark.sql.delta.deletionvectors.DeletionVectorsSuite#DELETE with 
DVs with column mapping mode=id
 org.apache.spark.sql.delta.deletionvectors.DeletionVectorsSuite#huge table: 
delete a small number of rows from tables of 2B rows with DVs
@@ -547,28 +495,16 @@ 
org.apache.spark.sql.delta.generatedsuites.UpdateBaseTempViewSQLPathBasedSuite#t
 
org.apache.spark.sql.delta.generatedsuites.UpdateBaseTempViewSQLPathBasedSuite#test
 update on temp view - nontrivial projection - SQL TempView
 org.apache.spark.sql.delta.optimize.OptimizeCompactionSQLSuite#optimize - 
multiple jobs start executing at once
 org.apache.spark.sql.delta.optimize.OptimizeCompactionScalaSuite#optimize - 
multiple jobs start executing at once
-org.apache.spark.sql.delta.optimize.OptimizeConflictSuite#conflict handling 
between Optimize and Business Txn
 org.apache.spark.sql.delta.optimize.OptimizeMetricsSuite#optimize ZOrderBy 
operation metrics in Delta table history
 org.apache.spark.sql.delta.optimize.OptimizeMetricsSuite#optimize metrics on 
idempotent operations
 org.apache.spark.sql.delta.perf.OptimizeGeneratedColumnSuite#end-to-end test 
of behaviors of write/read null on partition column
 org.apache.spark.sql.delta.perf.OptimizeGeneratedColumnSuite#five digits year 
in a date_format yyyy-MM partition column
 org.apache.spark.sql.delta.perf.OptimizeGeneratedColumnSuite#five digits year 
in a date_format yyyy-MM-dd-HH partition column
-org.apache.spark.sql.delta.rowid.ConflictCheckerRowIdSuite#Re-added files keep 
their row IDs after conflict with txn not updating high watermark
-org.apache.spark.sql.delta.rowid.ConflictCheckerRowIdSuite#concurrent 
transactions do not assign overlapping row IDs
-org.apache.spark.sql.delta.rowid.ConflictCheckerRowIdSuite#re-added files keep 
their row ids
 org.apache.spark.sql.delta.rowid.RowIdSuite#Filter by base Row IDs
 org.apache.spark.sql.delta.rowid.RowIdSuite#Filter by base Row IDs in subquery
 org.apache.spark.sql.delta.rowid.RowIdSuite#No dictionary filtering on 
_metadata.row_id
 org.apache.spark.sql.delta.rowid.RowIdSuite#missing base row ids and default 
row commit versions
 org.apache.spark.sql.delta.rowid.RowIdSuite#row ids can be read back
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencySuite#Interleaved
 delete right after protocol downgrade should abort due to protocol change
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencySuite#Interleaved
 update right after protocol downgrade should abort due to protocol change
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencySuite#Single 
Unbackfill batch interleaves delete
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencySuite#Single 
Unbackfill batch interleaves update
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencyWithoutDVsSuite#Interleaved
 delete right after protocol downgrade should abort due to protocol change
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencyWithoutDVsSuite#Interleaved
 update right after protocol downgrade should abort due to protocol change
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencyWithoutDVsSuite#Single
 Unbackfill batch interleaves delete
-org.apache.spark.sql.delta.rowid.RowTrackingRemovalConcurrencyWithoutDVsSuite#Single
 Unbackfill batch interleaves update
 org.apache.spark.sql.delta.rowtracking.RowTrackingReadWriteSuite#write and 
read table with all-null materialized columns
 org.apache.spark.sql.delta.stats.DataSkippingDeltaV1JsonCheckpointV2Suite#Data 
skipping handles aliasing for _metadata fields
 org.apache.spark.sql.delta.stats.DataSkippingDeltaV1JsonCheckpointV2Suite#Data 
skipping handles aliasing for _metadata fields - old behavior with DataFrame 
schema
diff --git 
a/backends-velox/src-delta33/main/scala/org/apache/spark/sql/execution/datasources/v2/DeltaWriteOperators.scala
 
b/backends-velox/src-delta33/main/scala/org/apache/spark/sql/execution/datasources/v2/DeltaWriteOperators.scala
index ebd9e7a3e4..e5b40e7182 100644
--- 
a/backends-velox/src-delta33/main/scala/org/apache/spark/sql/execution/datasources/v2/DeltaWriteOperators.scala
+++ 
b/backends-velox/src-delta33/main/scala/org/apache/spark/sql/execution/datasources/v2/DeltaWriteOperators.scala
@@ -28,8 +28,7 @@ case class GlutenDeltaLeafV2CommandExec(delegate: 
LeafV2CommandExec) extends Lea
   override def metrics: Map[String, SQLMetric] = delegate.metrics
 
   override protected def run(): Seq[InternalRow] = {
-    TransactionExecutionObserver.withObserver(
-      DeltaV2WriteOperators.UseColumnarDeltaTransactionLog) {
+    DeltaV2WriteOperators.withColumnarTransaction {
       delegate.executeCollect()
     }
   }
@@ -50,8 +49,7 @@ case class GlutenDeltaLeafRunnableCommand(delegate: 
LeafRunnableCommand)
   }
 
   override def run(sparkSession: SparkSession): Seq[Row] = {
-    TransactionExecutionObserver.withObserver(
-      DeltaV2WriteOperators.UseColumnarDeltaTransactionLog) {
+    DeltaV2WriteOperators.withColumnarTransaction {
       delegate.run(sparkSession)
     }
   }
@@ -67,8 +65,7 @@ case class GlutenDeltaRunnableCommand(delegate: 
RunnableCommand) extends LeafRun
   }
 
   override def run(sparkSession: SparkSession): Seq[Row] = {
-    TransactionExecutionObserver.withObserver(
-      DeltaV2WriteOperators.UseColumnarDeltaTransactionLog) {
+    DeltaV2WriteOperators.withColumnarTransaction {
       delegate.run(sparkSession)
     }
   }
@@ -77,26 +74,80 @@ case class GlutenDeltaRunnableCommand(delegate: 
RunnableCommand) extends LeafRun
 }
 
 object DeltaV2WriteOperators {
-  object UseColumnarDeltaTransactionLog extends TransactionExecutionObserver {
+  private[sql] def withColumnarTransaction[T](f: => T): T = {
+    TransactionExecutionObserver.getObserver match {
+      case _: UseColumnarDeltaTransactionLog => f
+      case observer =>
+        TransactionExecutionObserver.setObserver(wrap(observer))
+        try {
+          f
+        } finally {
+          // A completed transaction may have advanced to the next observer.
+          
TransactionExecutionObserver.setObserver(unwrap(TransactionExecutionObserver.getObserver))
+        }
+    }
+  }
+
+  private def wrap(observer: TransactionExecutionObserver): 
TransactionExecutionObserver =
+    observer match {
+      case _: UseColumnarDeltaTransactionLog => observer
+      case _ => new UseColumnarDeltaTransactionLog(observer)
+    }
+
+  private def unwrap(observer: TransactionExecutionObserver): 
TransactionExecutionObserver =
+    observer match {
+      case columnar: UseColumnarDeltaTransactionLog => columnar.underlying
+      case _ => observer
+    }
+
+  private class UseColumnarDeltaTransactionLog(val underlying: 
TransactionExecutionObserver)
+    extends TransactionExecutionObserver {
     override def startingTransaction(f: => OptimisticTransaction): 
OptimisticTransaction = {
-      val delegate = f
-      new GlutenOptimisticTransaction(delegate)
+      underlying.startingTransaction {
+        new GlutenOptimisticTransaction(f)
+      }
     }
 
-    override def preparingCommit[T](f: => T): T = f
+    override def preparingCommit[T](f: => T): T = underlying.preparingCommit(f)
 
-    override def beginDoCommit(): Unit = ()
+    override def beginDoCommit(): Unit = underlying.beginDoCommit()
 
-    override def beginBackfill(): Unit = ()
+    override def beginBackfill(): Unit = underlying.beginBackfill()
 
-    override def beginPostCommit(): Unit = ()
+    override def beginPostCommit(): Unit = underlying.beginPostCommit()
 
-    override def transactionCommitted(): Unit = ()
+    override def transactionCommitted(): Unit = withObserverAdvance {
+      underlying.transactionCommitted()
+    }
 
-    override def transactionAborted(): Unit = ()
+    override def transactionAborted(): Unit = withObserverAdvance {
+      underlying.transactionAborted()
+    }
 
     override def createChild(): TransactionExecutionObserver = {
-      TransactionExecutionObserver.getObserver
+      wrap(underlying.createChild())
+    }
+
+    override def setNextObserver(nextTxnObserver: 
TransactionExecutionObserver): Unit = {
+      // Delta 4.3 adds getNextObserver; also delegate that getter when 
upgrading the API.
+      underlying.setNextObserver(unwrap(nextTxnObserver))
+    }
+
+    override def advanceToNextThreadObserver(): Unit = withObserverAdvance {
+      underlying.advanceToNextThreadObserver()
+    }
+
+    private def withObserverAdvance(f: => Unit): Unit = {
+      val inColumnarScope =
+        
TransactionExecutionObserver.getObserver.isInstanceOf[UseColumnarDeltaTransactionLog]
+      try {
+        f
+      } finally {
+        if (inColumnarScope) {
+          // Delta's observer can replace itself from inside its commit/abort 
callbacks.
+          
TransactionExecutionObserver.setObserver(wrap(TransactionExecutionObserver.getObserver))
+        }
+      }
     }
   }
 }
diff --git 
a/backends-velox/src-delta33/test/scala/org/apache/spark/sql/delta/DeltaTransactionObserverSuite.scala
 
b/backends-velox/src-delta33/test/scala/org/apache/spark/sql/delta/DeltaTransactionObserverSuite.scala
new file mode 100644
index 0000000000..3290f2c858
--- /dev/null
+++ 
b/backends-velox/src-delta33/test/scala/org/apache/spark/sql/delta/DeltaTransactionObserverSuite.scala
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.delta
+
+import org.apache.spark.{SparkConf, SparkFunSuite}
+import org.apache.spark.sql.delta.catalog.DeltaCatalog
+import org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, 
PhaseLockingTransactionExecutionObserver}
+import 
org.apache.spark.sql.execution.datasources.v2.DeltaV2WriteOperators.withColumnarTransaction
+import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.tags.ExtendedSQLTest
+
+import io.delta.sql.DeltaSparkSessionExtension
+
+import scala.collection.mutable.ListBuffer
+
+@ExtendedSQLTest
+class DeltaTransactionObserverSuite extends SparkFunSuite with 
SharedSparkSession {
+
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf
+      .set("spark.sql.extensions", classOf[DeltaSparkSessionExtension].getName)
+      .set("spark.sql.catalog.spark_catalog", classOf[DeltaCatalog].getName)
+      .set("spark.ui.enabled", "false")
+  }
+
+  private class RecordingObserver extends TransactionExecutionObserver {
+    val events: ListBuffer[String] = ListBuffer.empty
+    var child: Option[TransactionExecutionObserver] = None
+
+    override def startingTransaction(f: => OptimisticTransaction): 
OptimisticTransaction = {
+      events += "starting"
+      val transaction = f
+      events += "started"
+      transaction
+    }
+
+    override def preparingCommit[T](f: => T): T = {
+      events += "preparing"
+      val result = f
+      events += "prepared"
+      result
+    }
+
+    override def beginDoCommit(): Unit = { events += "commit" }
+    override def beginBackfill(): Unit = { events += "backfill" }
+    override def beginPostCommit(): Unit = { events += "postCommit" }
+    override def transactionCommitted(): Unit = { events += "committed" }
+    override def transactionAborted(): Unit = { events += "aborted" }
+
+    override def createChild(): TransactionExecutionObserver = {
+      events += "child"
+      child.getOrElse(TransactionExecutionObserver.getObserver)
+    }
+  }
+
+  private def phaseObserver(name: String): 
PhaseLockingTransactionExecutionObserver = {
+    val observer =
+      new 
PhaseLockingTransactionExecutionObserver(OptimisticTransactionPhases.forName(name))
+    observer.phaseLocks.foreach(_.entryBarrier.unblock())
+    observer
+  }
+
+  private def finishCommit(observer: TransactionExecutionObserver): Unit = {
+    observer.preparingCommit(())
+    observer.beginDoCommit()
+    observer.beginBackfill()
+    observer.beginPostCommit()
+    observer.transactionCommitted()
+  }
+
+  test("native transactions preserve the existing observer and all lifecycle 
callbacks") {
+    withTempDir {
+      dir =>
+        val original = new RecordingObserver
+        val deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)
+        TransactionExecutionObserver.withObserver(original) {
+          withColumnarTransaction {
+            val transaction = deltaLog.startTransaction(catalogTableOpt = None)
+            assert(transaction.isInstanceOf[GlutenOptimisticTransaction])
+            val observer = TransactionExecutionObserver.getObserver
+            assert(transaction.executionObserver eq observer)
+            assert(original.events.toList == List("starting", "started"))
+            assert(observer.preparingCommit(42) == 42)
+            observer.beginDoCommit()
+            observer.beginBackfill()
+            observer.beginPostCommit()
+            observer.transactionCommitted()
+            observer.transactionAborted()
+          }
+          assert(TransactionExecutionObserver.getObserver eq original)
+        }
+        assert(
+          original.events.toList == List(
+            "starting",
+            "started",
+            "preparing",
+            "prepared",
+            "commit",
+            "backfill",
+            "postCommit",
+            "committed",
+            "aborted"))
+    }
+  }
+
+  test("creation, preparation, and command exceptions retain their identity 
and observer scope") {
+    val original = new RecordingObserver
+    val failure = new IllegalStateException("observer test")
+    TransactionExecutionObserver.withObserver(original) {
+      assert(intercept[IllegalStateException] {
+        withColumnarTransaction {
+          TransactionExecutionObserver.getObserver.startingTransaction(throw 
failure)
+        }
+      } eq failure)
+      assert(TransactionExecutionObserver.getObserver eq original)
+
+      assert(intercept[IllegalStateException] {
+        withColumnarTransaction {
+          TransactionExecutionObserver.getObserver.preparingCommit(throw 
failure)
+        }
+      } eq failure)
+      assert(TransactionExecutionObserver.getObserver eq original)
+
+      assert(intercept[IllegalStateException] {
+        withColumnarTransaction(throw failure)
+      } eq failure)
+      assert(TransactionExecutionObserver.getObserver eq original)
+    }
+  }
+
+  for (aborted <- Seq(false, true)) {
+    test(s"Delta's phase observer advances after completion: 
aborted=$aborted") {
+      withTempDir {
+        dir =>
+          val original = phaseObserver("first")
+          val next = phaseObserver("next")
+          original.setNextObserver(next, autoAdvance = true)
+          original.phases.postCommitPhase.exitBarrier.unblock()
+          val deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)
+
+          TransactionExecutionObserver.withObserver(original) {
+            withColumnarTransaction {
+              val first = deltaLog.startTransaction(catalogTableOpt = None)
+              assert(first.isInstanceOf[GlutenOptimisticTransaction])
+              val firstObserver = first.executionObserver
+              if (aborted) {
+                firstObserver.transactionAborted()
+              } else {
+                finishCommit(firstObserver)
+              }
+
+              assert(TransactionExecutionObserver.getObserver ne firstObserver)
+              val second = deltaLog.startTransaction(catalogTableOpt = None)
+              assert(second.isInstanceOf[GlutenOptimisticTransaction])
+              finishCommit(second.executionObserver)
+              assert(next.allPhasesHavePassed)
+            }
+            assert(TransactionExecutionObserver.getObserver eq next)
+          }
+      }
+    }
+  }
+
+  test("nested native commands reuse the observer and preserve advancement on 
scope exit") {
+    val original = new RecordingObserver
+    val next = new RecordingObserver
+    TransactionExecutionObserver.withObserver(original) {
+      withColumnarTransaction {
+        val observer = TransactionExecutionObserver.getObserver
+        withColumnarTransaction {
+          assert(TransactionExecutionObserver.getObserver eq observer)
+          observer.setNextObserver(next)
+          TransactionExecutionObserver.advanceToNextObserver()
+          TransactionExecutionObserver.getObserver.beginDoCommit()
+        }
+        assert(TransactionExecutionObserver.getObserver ne observer)
+        assert(TransactionExecutionObserver.getObserver ne next)
+        TransactionExecutionObserver.getObserver.beginBackfill()
+      }
+      assert(TransactionExecutionObserver.getObserver eq next)
+    }
+    assert(next.events.toList == List("commit", "backfill"))
+  }
+
+  test("native child observers preserve Delta's current-thread and 
custom-child behavior") {
+    val original = new RecordingObserver
+    val child = new RecordingObserver
+    TransactionExecutionObserver.withObserver(original) {
+      withColumnarTransaction {
+        val observer = TransactionExecutionObserver.getObserver
+        assert(observer.createChild() eq observer)
+        original.child = Some(child)
+        val childObserver = observer.createChild()
+        assert(childObserver ne child)
+        childObserver.beginDoCommit()
+        assert(child.events.toList == List("commit"))
+        assert(TransactionExecutionObserver.getObserver eq observer)
+      }
+      assert(TransactionExecutionObserver.getObserver eq original)
+    }
+  }
+
+  test("a callback exception does not discard an observer advancement") {
+    val next = new RecordingObserver
+    val failure = new IllegalStateException("commit callback")
+    val original = new RecordingObserver {
+      override def transactionCommitted(): Unit = {
+        advanceToNextThreadObserver()
+        throw failure
+      }
+    }
+    original.setNextObserver(next)
+    TransactionExecutionObserver.withObserver(original) {
+      assert(intercept[IllegalStateException] {
+        withColumnarTransaction {
+          TransactionExecutionObserver.getObserver.transactionCommitted()
+        }
+      } eq failure)
+      assert(TransactionExecutionObserver.getObserver eq next)
+    }
+  }
+
+  test("callbacks outside a native command do not install a native observer") {
+    val original = new RecordingObserver
+    TransactionExecutionObserver.withObserver(original) {
+      val observer = withColumnarTransaction {
+        TransactionExecutionObserver.getObserver
+      }
+      observer.transactionCommitted()
+      observer.transactionAborted()
+      assert(TransactionExecutionObserver.getObserver eq original)
+    }
+    assert(original.events.toList == List("committed", "aborted"))
+  }
+}
diff --git 
a/backends-velox/src-delta33/test/scala/org/apache/spark/sql/delta/VeloxDeltaTransactionObserverSuite.scala
 
b/backends-velox/src-delta33/test/scala/org/apache/spark/sql/delta/VeloxDeltaTransactionObserverSuite.scala
new file mode 100644
index 0000000000..d0817853c0
--- /dev/null
+++ 
b/backends-velox/src-delta33/test/scala/org/apache/spark/sql/delta/VeloxDeltaTransactionObserverSuite.scala
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.spark.sql.delta
+
+import org.apache.gluten.config.{GlutenConfig, VeloxDeltaConfig}
+
+import org.apache.spark.SparkConf
+import org.apache.spark.sql.{GlutenQueryTest, Row}
+import org.apache.spark.sql.delta.catalog.DeltaCatalog
+import org.apache.spark.sql.delta.fuzzer.{OptimisticTransactionPhases, 
PhaseLockingTransactionExecutionObserver}
+import org.apache.spark.sql.test.SharedSparkSession
+import org.apache.spark.tags.ExtendedSQLTest
+
+import io.delta.sql.DeltaSparkSessionExtension
+
+@ExtendedSQLTest
+class VeloxDeltaTransactionObserverSuite extends GlutenQueryTest with 
SharedSparkSession {
+
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf
+      .set("spark.sql.extensions", classOf[DeltaSparkSessionExtension].getName)
+      .set("spark.sql.catalog.spark_catalog", classOf[DeltaCatalog].getName)
+      .set("spark.plugins", "org.apache.gluten.GlutenPlugin")
+      .set("spark.shuffle.manager", 
"org.apache.spark.shuffle.sort.ColumnarShuffleManager")
+      .set("spark.memory.offHeap.enabled", "true")
+      .set("spark.memory.offHeap.size", "1024MB")
+      .set("spark.default.parallelism", "1")
+      .set("spark.sql.shuffle.partitions", "1")
+      .set("spark.databricks.delta.snapshotPartitions", "1")
+      .set("spark.sql.ansi.enabled", "false")
+      .set(GlutenConfig.GLUTEN_ANSI_FALLBACK_ENABLED.key, "false")
+      .set(VeloxDeltaConfig.ENABLE_NATIVE_WRITE.key, "true")
+  }
+
+  test("native Delta DELETE rewrites rows while preserving the phase-locking 
observer") {
+    withTempDir {
+      dir =>
+        withSQLConf(VeloxDeltaConfig.ENABLE_NATIVE_WRITE.key -> "false") {
+          spark
+            .range(4)
+            .coalesce(1)
+            .write
+            .format("delta")
+            .option(DeltaConfigs.ENABLE_DELETION_VECTORS_CREATION.key, "false")
+            .save(dir.getCanonicalPath)
+        }
+        val deltaLog = DeltaLog.forTable(spark, dir.getCanonicalPath)
+        val originalFiles = deltaLog.update().allFiles.collect()
+        assert(originalFiles.length == 1)
+        val observer = new PhaseLockingTransactionExecutionObserver(
+          OptimisticTransactionPhases.forName("native-delete")) {
+          override def startingTransaction(f: => OptimisticTransaction): 
OptimisticTransaction = {
+            val transaction = super.startingTransaction(f)
+            assert(transaction.isInstanceOf[GlutenOptimisticTransaction])
+            transaction
+          }
+        }
+        observer.phaseLocks.foreach(_.entryBarrier.unblock())
+
+        TransactionExecutionObserver.withObserver(observer) {
+          sql(s"DELETE FROM delta.`${dir.getCanonicalPath}` WHERE id < 
2").collect()
+          assert(TransactionExecutionObserver.getObserver eq observer)
+        }
+        assert(observer.allPhasesHavePassed)
+        val remainingFiles = deltaLog.update().allFiles.collect()
+        assert(remainingFiles.nonEmpty)
+        assert(remainingFiles.forall(_.path != originalFiles.head.path))
+        assert(remainingFiles.forall(_.deletionVector == null))
+        checkAnswer(
+          spark.read.format("delta").load(dir.getCanonicalPath),
+          Seq(Row(2L), Row(3L)))
+    }
+  }
+}
diff --git 
a/backends-velox/src-delta40/test/scala/org/apache/spark/sql/delta/DeltaTransactionObserverSuite.scala
 
b/backends-velox/src-delta40/test/scala/org/apache/spark/sql/delta/DeltaTransactionObserverSuite.scala
new file mode 120000
index 0000000000..b7864d0a58
--- /dev/null
+++ 
b/backends-velox/src-delta40/test/scala/org/apache/spark/sql/delta/DeltaTransactionObserverSuite.scala
@@ -0,0 +1 @@
+../../../../../../../../src-delta33/test/scala/org/apache/spark/sql/delta/DeltaTransactionObserverSuite.scala
\ No newline at end of file
diff --git 
a/backends-velox/src-delta40/test/scala/org/apache/spark/sql/delta/VeloxDeltaTransactionObserverSuite.scala
 
b/backends-velox/src-delta40/test/scala/org/apache/spark/sql/delta/VeloxDeltaTransactionObserverSuite.scala
new file mode 120000
index 0000000000..b7da75bc7f
--- /dev/null
+++ 
b/backends-velox/src-delta40/test/scala/org/apache/spark/sql/delta/VeloxDeltaTransactionObserverSuite.scala
@@ -0,0 +1 @@
+../../../../../../../../src-delta33/test/scala/org/apache/spark/sql/delta/VeloxDeltaTransactionObserverSuite.scala
\ No newline at end of file


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

Reply via email to