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

szehon-ho 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 6eb0fb8e7f83 [SPARK-60094][SDP] Gate AutoCDC ignore-null updates
6eb0fb8e7f83 is described below

commit 6eb0fb8e7f83d585ae5720822607ef0582ee60a7
Author: AnishMahto <[email protected]>
AuthorDate: Thu Oct 8 18:02:54 2026 -0700

    [SPARK-60094][SDP] Gate AutoCDC ignore-null updates
    
    ### What changes were proposed in this pull request?
    Until AutoCDC ignore-null is fully built E2E for both SCD1 and SCD2, we 
should explicitly gate its usage.
    
    In ignore-null's current partial implementation state, if it runs then we 
may produce partial results that get persisted against existing AutoCDC 
target/aux tables.
    
    Both the current (partially complete) implementation and API support for 
ignore-null is merged into Spark master, and is planned to go out as of Spark 
4.4. Once the implementation is complete, this gating can be removed.
    
    **Gating is intentionally done as a `GraphValidation` to avoid reverting 
ignore-null Python/SQL API and tests, which are currently unreleased and only 
in Spark master.**
    
    The consequence is if a Spark release was made today, the ignore-null API 
would be released but usage invalidated at execution time.
    
    ### Why are the changes needed?
    Gate incomplete AutoCDC ignore-null implementation.
    
    ### Does this PR introduce _any_ user-facing change?
    No, currently the user facing ignore-null APIs are unreleased.
    
    ### How was this patch tested?
    Unit tests.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Co-authored with Opus 5.5
    
    Closes #59309 from AnishMahto/SPARK-60094-autocdc-gate-ignore-null-updates.
    
    Authored-by: AnishMahto <[email protected]>
    Signed-off-by: Szehon Ho <[email protected]>
    (cherry picked from commit 26445bf61aa36a7001e0be4afebc76d691a24661)
    Signed-off-by: Szehon Ho <[email protected]>
---
 .../connect/pipelines/PythonPipelineSuite.scala    | 12 +++----
 .../spark/sql/pipelines/graph/DataflowGraph.scala  |  1 +
 .../sql/pipelines/graph/GraphValidations.scala     | 13 +++++++
 .../graph/ConnectInvalidPipelineSuite.scala        | 40 +++++++++++++++++++++-
 4 files changed, 58 insertions(+), 8 deletions(-)

diff --git 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
index c85820f0079a..d4edee481f5f 100644
--- 
a/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
+++ 
b/sql/connect/server/src/test/scala/org/apache/spark/sql/connect/pipelines/PythonPipelineSuite.scala
@@ -972,15 +972,13 @@ class PythonPipelineSuite
   }
 
   /**
-   * Builds the graph and runs it through resolution and validation, returning 
the resolved
-   * [[AutoCdcMergeFlow]]. Unlike [[buildAutoCdcFlow]], this exercises the 
graph analysis that
-   * rejects invalid change args (e.g. an ignore-null column that is also a 
key), so a test that
-   * asserts on the resolved flow is guaranteed to describe a usable 
configuration.
+   * Builds and resolves the graph, returning the resolved 
[[AutoCdcMergeFlow]]. Unlike
+   * [[buildAutoCdcFlow]], this exercises the flow analysis that rejects 
invalid change args (e.g.
+   * an ignore-null column that is also a key), so a test that asserts on the 
resolved flow is
+   * guaranteed to describe a configuration the flow accepts.
    */
   private def resolveAutoCdcMergeFlow(pipelineSource: String): 
AutoCdcMergeFlow = {
-    val graph = buildGraph(pipelineSource)
-      .resolve(sessionCaseSensitive)
-      .validate(sessionCaseSensitive)
+    val graph = buildGraph(pipelineSource).resolve(sessionCaseSensitive)
     graph.resolvedFlow(graphIdentifier("target")) match {
       case f: AutoCdcMergeFlow => f
       case other => fail(s"Expected an AutoCdcMergeFlow, got: $other")
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
index cd1dbf63ccdf..4ff45e330125 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/DataflowGraph.scala
@@ -249,6 +249,7 @@ case class DataflowGraph(
     validateEveryDatasetHasFlow()
     validateTablesAreResettable()
     validateFlowStreamingness()
+    validateNoAutoCdcIgnoreNullSelection()
   }.failed
 
   /**
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
index 5b4eaba4e709..d017a81d5b1d 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/GraphValidations.scala
@@ -83,6 +83,19 @@ trait GraphValidations extends Logging {
     case _ => false
   }
 
+  /** Rejects any AutoCDC flow with an ignore-null selection, since neither 
SCD type supports it. */
+  protected[graph] def validateNoAutoCdcIgnoreNullSelection(): Unit = {
+    resolvedFlows
+      .collectFirst {
+        case f: AutoCdcMergeFlow if f.changeArgs.ignoreNullSelection.isDefined 
=> f
+      }
+      .foreach { flow =>
+        throw new UnsupportedOperationException(
+          s"AutoCDC flow ${flow.displayName} specifies ignore-null updates, 
which are not yet " +
+            "supported.")
+      }
+  }
+
   /**
    * Validate that each resolved flow is correctly either a streaming flow or 
non-streaming flow,
    * depending on the flow type (ex. once flow vs non-once flow) and the 
dataset type the flow
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
index 172d8354932b..5fd1190ca6c3 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/ConnectInvalidPipelineSuite.scala
@@ -19,7 +19,7 @@ package org.apache.spark.sql.pipelines.graph
 
 import org.apache.spark.sql.AnalysisException
 import org.apache.spark.sql.execution.streaming.runtime.MemoryStream
-import org.apache.spark.sql.pipelines.autocdc.{ChangeArgs, ScdType, 
UnqualifiedColumnName}
+import org.apache.spark.sql.pipelines.autocdc.{ChangeArgs, ColumnSelection, 
ScdType, UnqualifiedColumnName}
 import org.apache.spark.sql.pipelines.utils.{PipelineTest, 
TestGraphRegistrationContext}
 import org.apache.spark.sql.test.SharedSparkSession
 import org.apache.spark.sql.types.{IntegerType, StructType}
@@ -772,6 +772,44 @@ class ConnectInvalidPipelineSuite extends PipelineTest 
with SharedSparkSession {
     )
   }
 
+  gridTest("AutoCDC flow with ignore-null updates is rejected as unsupported")(
+    Seq(ScdType.Type1, ScdType.Type2)
+  ) { scdType =>
+    val session = spark
+    import session.implicits._
+
+    val graph = new TestGraphRegistrationContext(spark) {
+      val cdcEvents = MemoryStream[Int].toDF().select($"value" as "id", 
$"value" as "seq")
+      registerTable("target")
+      registerFlow(
+        AutoCdcFlow(
+          identifier = fullyQualifiedIdentifier("auto_cdc_flow"),
+          destinationIdentifier = fullyQualifiedIdentifier("target"),
+          func = dfFlowFunc(cdcEvents),
+          queryContext = QueryContext(
+            currentCatalog = 
Some(TestGraphRegistrationContext.DEFAULT_CATALOG),
+            currentDatabase = 
Some(TestGraphRegistrationContext.DEFAULT_DATABASE)
+          ),
+          origin = QueryOrigin.empty,
+          changeArgs = ChangeArgs(
+            keys = Seq(UnqualifiedColumnName("id")),
+            sequencing = $"seq",
+            storedAsScdType = scdType,
+            ignoreNullSelection = 
Some(ColumnSelection.ExcludeColumns(Seq.empty))
+          )
+        )
+      )
+    }.resolveToDataflowGraph()
+
+    val ex = intercept[UnsupportedOperationException] {
+      validateGraph(graph)
+    }
+    val flowName = fullyQualifiedIdentifier("auto_cdc_flow").unquotedString
+    assert(
+      ex.getMessage ==
+        s"AutoCDC flow $flowName specifies ignore-null updates, which are not 
yet supported.")
+  }
+
   test("DUPLICATE_GRAPH_ELEMENT: duplicate graph element identifiers") {
     checkError(
       exception = intercept[AnalysisException] {


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

Reply via email to