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

dongjoon-hyun pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 58e2554111f1 [SPARK-58007][SQL] Support dynamic table options for MERGE
58e2554111f1 is described below

commit 58e2554111f1311556dde32c7c26ec7d4fe96471
Author: Anurag Mantripragada <[email protected]>
AuthorDate: Tue Jul 21 08:50:27 2026 -0700

    [SPARK-58007][SQL] Support dynamic table options for MERGE
    
    ## What changes were proposed in this pull request?
    
    This PR adds the dynamic table options syntax to MERGE statements. MERGE 
requires options on both the target table and the source table.
    
    ## Why are the changes needed?
    
    This is a continuation to adding options syntax in DMLs SPARK-49098
    
    ## Does this PR introduce any user-facing change?
    
    Yes, it adds new SQL syntax. For example:
    
    ```sql
       MERGE INTO catalog.db.target t WITH (`write.split-size` = 10)
       USING catalog.db.source WITH (`split-size` = 5) s
       ON t.id = s.id
       WHEN MATCHED THEN UPDATE SET t.status = 'archived'
       WHEN NOT MATCHED THEN INSERT *
    ```
    
    ## How was this patch tested?
    
    Added tests to the following suites.
    
      - Parser (DDLParserSuite)
      - End-to-end (MergeIntoTableSuiteBase)
    
    ## Was this patch authored or co-authored using generative AI tooling?
    
    I used Claude Code (Claude Opus 4.8) to generate the code and tests and 
verified manually.
    
    Closes #57227 from anuragmantri/merge-sql-options.
    
    Lead-authored-by: Anurag Mantripragada <[email protected]>
    Co-authored-by: x <[email protected]>
    Signed-off-by: Dongjoon Hyun <[email protected]>
    (cherry picked from commit 8a33fb73fa520ba04be46a1c350e46d79234b53c)
    Signed-off-by: Dongjoon Hyun <[email protected]>
---
 .../spark/sql/catalyst/parser/SqlBaseParser.g4     |   3 +-
 .../catalyst/analysis/RewriteMergeIntoTable.scala  |   3 +-
 .../spark/sql/catalyst/parser/AstBuilder.scala     |   8 +-
 .../spark/sql/catalyst/parser/DDLParserSuite.scala |  49 +++++++
 .../sql/connector/MergeIntoTableSuiteBase.scala    | 144 ++++++++++++++++++++-
 .../sql/connector/RowLevelOperationSuiteBase.scala |  23 +++-
 .../spark/sql/connector/UpdateTableSuiteBase.scala |  28 +---
 7 files changed, 225 insertions(+), 33 deletions(-)

diff --git 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
index e64d88cce1ec..ca5de63ecb0b 100644
--- 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
+++ 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseParser.g4
@@ -750,7 +750,8 @@ dmlStatementNoWith
     | DELETE FROM identifierReference tableAlias whereClause?                  
    #deleteFromTable
     | UPDATE identifierReference tableAlias optionsClause? setClause 
whereClause?  #updateTable
     | MERGE (WITH SCHEMA EVOLUTION)? INTO target=identifierReference 
targetAlias=tableAlias
-        USING (source=identifierReference |
+        targetOptions=optionsClause?
+        USING (source=identifierReference sourceOptions=optionsClause? |
           LEFT_PAREN sourceQuery=query RIGHT_PAREN) sourceAlias=tableAlias
         ON mergeCondition=booleanExpression
         matchedClause*
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RewriteMergeIntoTable.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RewriteMergeIntoTable.scala
index e11a985d85dd..714f815161ae 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RewriteMergeIntoTable.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/RewriteMergeIntoTable.scala
@@ -31,7 +31,6 @@ import 
org.apache.spark.sql.connector.write.RowLevelOperation.Command.MERGE
 import org.apache.spark.sql.errors.QueryCompilationErrors
 import org.apache.spark.sql.execution.datasources.v2.{DataSourceV2Relation, 
ExtractV2Table}
 import org.apache.spark.sql.types.IntegerType
-import org.apache.spark.sql.util.CaseInsensitiveStringMap
 
 /**
  * A rule that rewrites MERGE operations using plans that operate on 
individual or groups of rows.
@@ -128,7 +127,7 @@ object RewriteMergeIntoTable extends RewriteRowLevelCommand 
with PredicateHelper
         case r @ ExtractV2Table(tbl: SupportsRowLevelOperations) =>
           checkNoGeneratedColumns(r, MERGE)
           validateMergeIntoConditions(m)
-          val table = buildOperationTable(tbl, MERGE, 
CaseInsensitiveStringMap.empty())
+          val table = buildOperationTable(tbl, MERGE, r.options)
           table.operation match {
             case _: SupportsDelta =>
               buildWriteDeltaPlan(
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
index dcb8d42153ff..2100241ce9db 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/parser/AstBuilder.scala
@@ -1268,8 +1268,13 @@ class AstBuilder extends DataTypeAstBuilder
   override def visitMergeIntoTable(ctx: MergeIntoTableContext): LogicalPlan = 
withOrigin(ctx) {
     val withSchemaEvolution = ctx.EVOLUTION() != null
 
+    // The target and source may each carry their own dynamic table options 
via `WITH (...)`.
+    // Known limitation: if the same table is used as both the target and the 
source
+    // (e.g. `MERGE INTO t WITH (a) USING t WITH (b) s`), the analyzer's 
relation cache is keyed
+    // without options and reuses the first resolved relation, so the target's 
options win and the
+    // source's are silently dropped.
     val sourceTableOrQuery = if (ctx.source != null) {
-      createUnresolvedRelation(ctx.source)
+      createUnresolvedRelation(ctx.source, Option(ctx.sourceOptions))
     } else if (ctx.sourceQuery != null) {
       visitQuery(ctx.sourceQuery)
     } else {
@@ -1355,6 +1360,7 @@ class AstBuilder extends DataTypeAstBuilder
 
     val targetTable = createUnresolvedRelation(
       ctx.target,
+      Option(ctx.targetOptions),
       writePrivileges = MergeIntoTable.getWritePrivileges(
         matchedActions, notMatchedActions, notMatchedBySourceActions))
     val targetTableAlias = getTableAliasWithoutColumnAlias(ctx.targetAlias, 
"MERGE")
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/DDLParserSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/DDLParserSuite.scala
index 4870c118dbc7..f33c11c8a2d4 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/DDLParserSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/parser/DDLParserSuite.scala
@@ -2317,6 +2317,55 @@ class DDLParserSuite extends AnalysisTest {
         withSchemaEvolution = false))
   }
 
+  test("SPARK-58007: merge into table: with target and source options") {
+    parseCompare(
+      """
+        |MERGE INTO testcat1.ns1.ns2.tbl AS target WITH (`write.split-size` = 
10)
+        |USING testcat2.ns1.ns2.tbl WITH (`split-size` = 5) AS source
+        |ON target.col1 = source.col1
+        |WHEN MATCHED THEN UPDATE SET target.col2 = source.col2
+        |WHEN NOT MATCHED THEN INSERT (target.col1, target.col2) values 
(source.col1, source.col2)
+      """.stripMargin,
+      MergeIntoTable(
+        SubqueryAlias("target",
+          UnresolvedRelation(Seq("testcat1", "ns1", "ns2", "tbl"),
+            new CaseInsensitiveStringMap(java.util.Map.of("write.split-size", 
"10")))),
+        SubqueryAlias("source",
+          UnresolvedRelation(Seq("testcat2", "ns1", "ns2", "tbl"),
+            new CaseInsensitiveStringMap(java.util.Map.of("split-size", 
"5")))),
+        EqualTo(UnresolvedAttribute("target.col1"), 
UnresolvedAttribute("source.col1")),
+        Seq(UpdateAction(None,
+          Seq(Assignment(UnresolvedAttribute("target.col2"),
+            UnresolvedAttribute("source.col2"))))),
+        Seq(InsertAction(None,
+          Seq(Assignment(UnresolvedAttribute("target.col1"), 
UnresolvedAttribute("source.col1")),
+            Assignment(UnresolvedAttribute("target.col2"), 
UnresolvedAttribute("source.col2"))))),
+        Seq.empty,
+        withSchemaEvolution = false))
+  }
+
+  test("SPARK-58007: merge into table: with target options only") {
+    parseCompare(
+      """
+        |MERGE INTO testcat1.ns1.ns2.tbl AS target WITH (`k` = 'v')
+        |USING testcat2.ns1.ns2.tbl AS source
+        |ON target.col1 = source.col1
+        |WHEN MATCHED THEN UPDATE SET target.col2 = source.col2
+      """.stripMargin,
+      MergeIntoTable(
+        SubqueryAlias("target",
+          UnresolvedRelation(Seq("testcat1", "ns1", "ns2", "tbl"),
+            new CaseInsensitiveStringMap(java.util.Map.of("k", "v")))),
+        SubqueryAlias("source", UnresolvedRelation(Seq("testcat2", "ns1", 
"ns2", "tbl"))),
+        EqualTo(UnresolvedAttribute("target.col1"), 
UnresolvedAttribute("source.col1")),
+        Seq(UpdateAction(None,
+          Seq(Assignment(UnresolvedAttribute("target.col2"),
+            UnresolvedAttribute("source.col2"))))),
+        Seq.empty,
+        Seq.empty,
+        withSchemaEvolution = false))
+  }
+
   test("merge into table: using subquery") {
     parseCompare(
       """
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/MergeIntoTableSuiteBase.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/MergeIntoTableSuiteBase.scala
index 1dbc1001b5c0..9b625c1b81d2 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/MergeIntoTableSuiteBase.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/MergeIntoTableSuiteBase.scala
@@ -20,14 +20,16 @@ package org.apache.spark.sql.connector
 import org.apache.spark.SparkRuntimeException
 import org.apache.spark.internal.config
 import org.apache.spark.sql.{AnalysisException, Row}
+import org.apache.spark.sql.QueryTest.withQueryExecutionsCaptured
 import org.apache.spark.sql.catalyst.expressions.{AttributeReference, EqualTo, 
In, Not}
 import org.apache.spark.sql.catalyst.optimizer.BuildLeft
-import org.apache.spark.sql.connector.catalog.{Aborted, Column, 
ColumnDefaultValue, Committed, InMemoryTable, TableInfo}
+import org.apache.spark.sql.catalyst.plans.logical.{ReplaceData, WriteDelta}
+import org.apache.spark.sql.connector.catalog.{Aborted, Column, 
ColumnDefaultValue, Committed, InMemoryBaseTable, InMemoryTable, TableInfo}
 import org.apache.spark.sql.connector.expressions.{GeneralScalarExpression, 
LiteralValue}
 import org.apache.spark.sql.connector.write.MergeSummary
 import org.apache.spark.sql.execution.SparkPlan
 import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
-import org.apache.spark.sql.execution.datasources.v2.MergeRowsExec
+import org.apache.spark.sql.execution.datasources.v2.{DataSourceV2Relation, 
DataSourceV2ScanRelation, InsertOnlyMergeExec, MergeRowsExec}
 import org.apache.spark.sql.execution.joins.{BroadcastHashJoinExec, 
BroadcastNestedLoopJoinExec, CartesianProductExec}
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.sources
@@ -2851,6 +2853,144 @@ abstract class MergeIntoTableSuiteBase extends 
RowLevelOperationSuiteBase
     assert(catalog.lastTransaction.isClosed)
   }
 
+  test("SPARK-58007: merge with dynamic options on the target") {
+    withTable(sourceNameAsString) {
+      createAndInitTable("pk INT NOT NULL, salary INT, dep STRING",
+        """{ "pk": 1, "salary": 100, "dep": "hr" }
+          |{ "pk": 2, "salary": 200, "dep": "software" }
+          |""".stripMargin)
+      sql(s"CREATE TABLE $sourceNameAsString (pk INT NOT NULL, salary INT, dep 
STRING)")
+      sql(s"INSERT INTO $sourceNameAsString VALUES (1, 150, 'hr'), (3, 300, 
'hr')")
+
+      // the target options become the row-level operation options: they reach 
the rewritten
+      // DataSourceV2Relation, the RowLevelOperationInfo, and the write 
builder's LogicalWriteInfo
+      checkRowLevelOperationOptions(
+        sql(
+          s"""MERGE INTO $tableNameAsString t WITH (`write.split-size` = 10)
+             |USING $sourceNameAsString s
+             |ON t.pk = s.pk
+             |WHEN MATCHED THEN UPDATE SET t.salary = s.salary
+             |WHEN NOT MATCHED THEN INSERT *
+             |""".stripMargin),
+        "write.split-size" -> "10")
+
+      checkAnswer(
+        sql(s"SELECT * FROM $tableNameAsString"),
+        Row(1, 150, "hr") :: Row(2, 200, "software") :: Row(3, 300, "hr") :: 
Nil)
+    }
+  }
+
+  test("SPARK-58007: merge with dynamic options on the source") {
+    withTable(sourceNameAsString) {
+      createAndInitTable("pk INT NOT NULL, salary INT, dep STRING",
+        """{ "pk": 1, "salary": 100, "dep": "hr" }
+          |{ "pk": 2, "salary": 200, "dep": "software" }
+          |""".stripMargin)
+      sql(s"CREATE TABLE $sourceNameAsString (pk INT NOT NULL, salary INT, dep 
STRING)")
+      sql(s"INSERT INTO $sourceNameAsString VALUES (1, 150, 'hr'), (3, 300, 
'hr')")
+
+      // the source is read-only; its options must reach the source scan the 
same way a
+      // SELECT ... WITH (...) does
+      val Seq(qe) = withQueryExecutionsCaptured(spark) {
+        sql(
+          s"""MERGE INTO $tableNameAsString t
+             |USING $sourceNameAsString WITH (`split-size` = 5) s
+             |ON t.pk = s.pk
+             |WHEN MATCHED THEN UPDATE SET t.salary = s.salary
+             |WHEN NOT MATCHED THEN INSERT *
+             |""".stripMargin)
+      }
+      val sourceOptions = qe.optimizedPlan.collect {
+        case r: DataSourceV2Relation => r.options
+        case s: DataSourceV2ScanRelation => s.relation.options
+      }.filter(_.containsKey("split-size"))
+      assert(sourceOptions.nonEmpty, "source relation carrying the option was 
not found")
+      sourceOptions.foreach(opts => assert(opts.get("split-size") === "5"))
+
+      checkAnswer(
+        sql(s"SELECT * FROM $tableNameAsString"),
+        Row(1, 150, "hr") :: Row(2, 200, "software") :: Row(3, 300, "hr") :: 
Nil)
+    }
+  }
+
+  test("SPARK-58007: merge with dynamic options on both the target and the 
source") {
+    withTable(sourceNameAsString) {
+      createAndInitTable("pk INT NOT NULL, salary INT, dep STRING",
+        """{ "pk": 1, "salary": 100, "dep": "hr" }
+          |{ "pk": 2, "salary": 200, "dep": "software" }
+          |""".stripMargin)
+      sql(s"CREATE TABLE $sourceNameAsString (pk INT NOT NULL, salary INT, dep 
STRING)")
+      sql(s"INSERT INTO $sourceNameAsString VALUES (1, 150, 'hr'), (3, 300, 
'hr')")
+
+      // the target and source each carry their own options bag; the two must 
not cross-contaminate
+      // (the single-option cases above already verify each bag reaches its 
destination)
+      val Seq(qe) = withQueryExecutionsCaptured(spark) {
+        sql(
+          s"""MERGE INTO $tableNameAsString t WITH (`write.split-size` = 10)
+             |USING $sourceNameAsString WITH (`split-size` = 5) s
+             |ON t.pk = s.pk
+             |WHEN MATCHED THEN UPDATE SET t.salary = s.salary
+             |WHEN NOT MATCHED THEN INSERT *
+             |""".stripMargin)
+      }
+
+      // target write relation carries only its own option, not the source's
+      val writeRelation = qe.optimizedPlan.collectFirst {
+        case rd: ReplaceData => rd.table
+        case wd: WriteDelta => wd.table
+      }.getOrElse(fail("couldn't find row-level operation in optimized plan"))
+        .asInstanceOf[DataSourceV2Relation]
+      assert(writeRelation.options.get("write.split-size") === "10", "target 
relation option")
+      assert(!writeRelation.options.containsKey("split-size"), "target must 
not see source option")
+
+      // source scan carries only its own option, not the target's
+      val sourceOptions = qe.optimizedPlan.collect {
+        case r: DataSourceV2Relation => r.options
+        case s: DataSourceV2ScanRelation => s.relation.options
+      }.filter(_.containsKey("split-size"))
+      assert(sourceOptions.nonEmpty, "source relation carrying the option was 
not found")
+      sourceOptions.foreach { opts =>
+        assert(opts.get("split-size") === "5", "source relation option")
+        assert(!opts.containsKey("write.split-size"), "source must not see 
target option")
+      }
+
+      checkAnswer(
+        sql(s"SELECT * FROM $tableNameAsString"),
+        Row(1, 150, "hr") :: Row(2, 200, "software") :: Row(3, 300, "hr") :: 
Nil)
+    }
+  }
+
+  test("SPARK-58007: merge with dynamic options on the target, insert-only 
fast path") {
+    withTable(sourceNameAsString) {
+      createAndInitTable("pk INT NOT NULL, salary INT, dep STRING",
+        """{ "pk": 1, "salary": 100, "dep": "hr" }
+          |{ "pk": 2, "salary": 200, "dep": "software" }
+          |""".stripMargin)
+      sql(s"CREATE TABLE $sourceNameAsString (pk INT NOT NULL, salary INT, dep 
STRING)")
+      sql(s"INSERT INTO $sourceNameAsString VALUES (1, 150, 'hr'), (3, 300, 
'hr')")
+
+      // a MATCHED-free merge is rewritten to InsertOnlyMerge (a plain 
append), which builds
+      // write from the target's options at a different site than the 
row-level rewrite path.
+      val executedPlan = executeAndKeepPlan {
+        sql(
+          s"""MERGE INTO $tableNameAsString t WITH (`write.split-size` = 10)
+             |USING $sourceNameAsString s
+             |ON t.pk = s.pk
+             |WHEN NOT MATCHED THEN INSERT *
+             |""".stripMargin)
+      }
+      val write = executedPlan.collectFirst {
+        case e: InsertOnlyMergeExec => e.write
+      }.getOrElse(fail("expected an InsertOnlyMergeExec in the executed plan"))
+      val append = write.toBatch.asInstanceOf[InMemoryBaseTable#Append]
+      assert(append.info.options.get("write.split-size") === "10")
+
+      checkAnswer(
+        sql(s"SELECT * FROM $tableNameAsString"),
+        Row(1, 100, "hr") :: Row(2, 200, "software") :: Row(3, 300, "hr") :: 
Nil)
+    }
+  }
+
   private def assertMetric(
       mergeExec: MergeRowsExec,
       metricName: String,
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/RowLevelOperationSuiteBase.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/RowLevelOperationSuiteBase.scala
index 199b9ecbe0a0..5d52325da818 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/RowLevelOperationSuiteBase.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/RowLevelOperationSuiteBase.scala
@@ -28,7 +28,7 @@ import 
org.apache.spark.sql.catalyst.expressions.{DynamicPruningExpression, Expr
 import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, ReplaceData, 
WriteDelta}
 import org.apache.spark.sql.catalyst.types.DataTypeUtils
 import org.apache.spark.sql.catalyst.util.METADATA_COL_ATTR_KEY
-import org.apache.spark.sql.connector.catalog.{CatalogV2Util, Column, Delete, 
Identifier, InMemoryRowLevelOperationTable, 
InMemoryRowLevelOperationTableCatalog, Insert, MetadataColumn, Operation, 
Reinsert, Table, TableInfo, Txn, TxnTable, Update, Write}
+import org.apache.spark.sql.connector.catalog.{CatalogV2Util, Column, Delete, 
Identifier, InMemoryRowLevelOperationTable, 
InMemoryRowLevelOperationTableCatalog, Insert, MetadataColumn, Operation, 
Reinsert, RowLevelOperationWithOptions, Table, TableInfo, Txn, TxnTable, 
Update, Write}
 import 
org.apache.spark.sql.connector.expressions.LogicalExpressions.{identity, 
reference}
 import org.apache.spark.sql.connector.expressions.Transform
 import org.apache.spark.sql.connector.write.RowLevelOperationTable
@@ -179,6 +179,27 @@ abstract class RowLevelOperationSuiteBase
     }.getOrElse(fail("couldn't find row-level operation in optimized plan"))
   }
 
+  // asserts the given SQL options reached every layer that should carry them: 
the rewritten
+  // DataSourceV2Relation, the RowLevelOperationInfo passed to the operation 
builder, and the
+  // write builder's LogicalWriteInfo
+  protected def checkRowLevelOperationOptions(
+      func: => Unit,
+      expectedOptions: (String, String)*): Unit = {
+    val Seq(qe) = withQueryExecutionsCaptured(spark)(func)
+    val writeRelation = qe.optimizedPlan.collectFirst {
+      case rd: ReplaceData => rd.table
+      case wd: WriteDelta => wd.table
+    }.getOrElse(fail("couldn't find row-level operation in optimized plan"))
+      .asInstanceOf[DataSourceV2Relation]
+    val operation = 
writeRelation.table.asInstanceOf[RowLevelOperationTable].operation
+      .asInstanceOf[RowLevelOperationWithOptions]
+    expectedOptions.foreach { case (key, value) =>
+      assert(writeRelation.options.get(key) === value, s"relation option 
'$key'")
+      assert(operation.options.get(key) === value, s"row-level operation 
option '$key'")
+      assert(table.lastWriteInfo.options().get(key) === value, s"write option 
'$key'")
+    }
+  }
+
   protected def assertNoScanPlanning(plan: LogicalPlan): Unit = {
     val relations = plan.collect { case r: DataSourceV2Relation => r }
     assert(relations.nonEmpty, "plan must contain relations")
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/UpdateTableSuiteBase.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/UpdateTableSuiteBase.scala
index c4c5627a12ea..adaa3be4510f 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/UpdateTableSuiteBase.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/UpdateTableSuiteBase.scala
@@ -20,12 +20,9 @@ package org.apache.spark.sql.connector
 import org.apache.spark.SparkRuntimeException
 import org.apache.spark.internal.config
 import org.apache.spark.sql.{sources, AnalysisException, Row}
-import org.apache.spark.sql.QueryTest.withQueryExecutionsCaptured
-import org.apache.spark.sql.catalyst.plans.logical.{ReplaceData, WriteDelta}
-import org.apache.spark.sql.connector.catalog.{Aborted, Column, 
ColumnDefaultValue, Committed, InMemoryTable, RowLevelOperationWithOptions, 
TableChange, TableInfo}
+import org.apache.spark.sql.connector.catalog.{Aborted, Column, 
ColumnDefaultValue, Committed, InMemoryTable, TableChange, TableInfo}
 import org.apache.spark.sql.connector.expressions.{GeneralScalarExpression, 
LiteralValue}
-import org.apache.spark.sql.connector.write.{RowLevelOperationTable, 
UpdateSummary}
-import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
+import org.apache.spark.sql.connector.write.UpdateSummary
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types.{IntegerType, StringType}
 
@@ -1236,27 +1233,6 @@ abstract class UpdateTableSuiteBase extends 
RowLevelOperationSuiteBase {
         Row(2, 200, "software")))
   }
 
-  // asserts the given SQL options reached every layer that should carry them: 
the rewritten
-  // DataSourceV2Relation, the RowLevelOperationInfo passed to the operation 
builder, and the
-  // write builder's LogicalWriteInfo
-  protected def checkRowLevelOperationOptions(
-      func: => Unit,
-      expectedOptions: (String, String)*): Unit = {
-    val Seq(qe) = withQueryExecutionsCaptured(spark)(func)
-    val writeRelation = qe.optimizedPlan.collectFirst {
-      case rd: ReplaceData => rd.table
-      case wd: WriteDelta => wd.table
-    }.getOrElse(fail("couldn't find row-level operation in optimized plan"))
-      .asInstanceOf[DataSourceV2Relation]
-    val operation = 
writeRelation.table.asInstanceOf[RowLevelOperationTable].operation
-      .asInstanceOf[RowLevelOperationWithOptions]
-    expectedOptions.foreach { case (key, value) =>
-      assert(writeRelation.options.get(key) === value, s"relation option 
'$key'")
-      assert(operation.options.get(key) === value, s"row-level operation 
option '$key'")
-      assert(table.lastWriteInfo.options().get(key) === value, s"write option 
'$key'")
-    }
-  }
-
   test("SPARK-57681: update with dynamic options") {
     createAndInitTable("pk INT NOT NULL, salary INT, dep STRING",
       """{ "pk": 1, "salary": 100, "dep": "hr" }


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

Reply via email to