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

szehon-ho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new c052ccaf8ccd [SPARK-58261][SQL][SDP] Support SCD Type 2 syntax in SQL 
AUTO CDC
c052ccaf8ccd is described below

commit c052ccaf8ccd431f02df80c0c48e7018f691010d
Author: Andreas Neumann <[email protected]>
AuthorDate: Thu Jul 23 11:15:29 2026 -0700

    [SPARK-58261][SQL][SDP] Support SCD Type 2 syntax in SQL AUTO CDC
    
    ### What changes were proposed in this pull request?
    
    Add SQL syntax for selecting the SCD type and history-tracking columns of 
an AUTO CDC flow, mirroring the recently added Python/Connect surface. Both SQL 
AUTO CDC forms -- `CREATE STREAMING TABLE ... FLOW AUTO CDC ...` and `CREATE 
FLOW ... AS AUTO CDC INTO ...` -- now accept:
    
    - `STORED AS SCD TYPE 1 | 2` (defaults to SCD Type 1 when omitted), and
    - `TRACK HISTORY ON (<cols>)` / `TRACK HISTORY ON * EXCEPT (<cols>)` under 
SCD Type 2.
    
    Specifically:
    - Grammar: add multi-word lexer tokens `SCD_TYPE_1`/`SCD_TYPE_2` and 
`HISTORY`/`TRACK` keywords; add `autoCdcStoredAsClause` and 
`autoCdcTrackHistoryClause`, appended in fixed order after the existing 
`COLUMNS` clause. `HISTORY`/`TRACK` are registered as non-reserved.
    - AST/plans: thread `storedAsScdType` (Int, default 1) and the 
track-history column lists through `AutoCdcParams`, `AutoCdcInto`, and 
`CreateStreamingTableAutoCdc`.
    - Registration: `SqlGraphRegistrationContext.buildChangeArgs` maps the SCD 
type onto `ScdType`, builds `trackHistorySelection`, and rejects TRACK HISTORY 
under SCD1 as well as specifying both track-history lists.
    
    The SCD type and track-history columns feed the existing `ChangeArgs` model 
(`storedAsScdType` / `trackHistorySelection`), the same contract the engine's 
SCD2 execution work consumes.
    
    ### Why are the changes needed?
    
    SQL AUTO CDC previously only supported SCD Type 1 and rejected `STORED AS 
SCD TYPE 2` / `TRACK HISTORY` at parse time. SCD Type 2 (history tracking) is a 
common CDC requirement and is already modeled in the engine's `ChangeArgs` / 
`ScdType`, so the SQL surface was the missing piece.
    
    ### Does this PR introduce any user-facing change?
    
    Yes. SQL AUTO CDC statements accept `STORED AS SCD TYPE 1|2` and, under SCD 
Type 2, `TRACK HISTORY ON ...`. Previously these clauses failed to parse. This 
is a change within the unreleased master branch only.
    
    ### How was this patch tested?
    
    New parser tests in `AutoCdcParserSuite` (SCD type 1/2, TRACK HISTORY 
include/except, all clauses combined, clause-ordering and invalid-type negative 
cases) and registration tests in `SqlPipelineSuite` (SCD2 and track-history map 
onto `ChangeArgs`; TRACK HISTORY without SCD2 is rejected). Ran 
`AutoCdcParserSuite`, `SQLKeywordSuite`, and the AUTO CDC `SqlPipelineSuite` 
cases.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Co-authored-by: Opus 4.8
    
    Closes #57434 from anew/spark-58261-sql-syntax-scd2.
    
    Authored-by: Andreas Neumann <[email protected]>
    Signed-off-by: Szehon Ho <[email protected]>
---
 docs/sql-ref-ansi-compliance.md                    |   3 +
 .../spark/sql/catalyst/parser/SqlBaseLexer.g4      |   3 +
 .../spark/sql/catalyst/parser/SqlBaseParser.g4     |  18 ++
 .../spark/sql/catalyst/parser/AstBuilder.scala     |  42 +++-
 .../sql/catalyst/plans/logical/AutoCdcInto.scala   |  18 +-
 .../sql/catalyst/plans/logical/v2Commands.scala    |  15 +-
 .../analysis/UnsupportedOperationsSuite.scala      |   5 +-
 .../jdbc/SparkConnectDatabaseMetaDataSuite.scala   |   2 +-
 .../spark/sql/execution/SparkSqlParser.scala       |   5 +-
 .../sql-tests/results/keywords-enforced.sql.out    |   3 +
 .../resources/sql-tests/results/keywords.sql.out   |   3 +
 .../sql-tests/results/nonansi/keywords.sql.out     |   3 +
 .../execution/command/v2/AutoCdcParserSuite.scala  | 229 ++++++++++++++++++---
 .../ThriftServerWithSparkContextSuite.scala        |   2 +-
 .../graph/SqlGraphRegistrationContext.scala        |  58 +++++-
 .../sql/pipelines/graph/SqlPipelineSuite.scala     |  95 ++++++++-
 16 files changed, 457 insertions(+), 47 deletions(-)

diff --git a/docs/sql-ref-ansi-compliance.md b/docs/sql-ref-ansi-compliance.md
index 367abce6cb8d..f4ac58a4e3f0 100644
--- a/docs/sql-ref-ansi-compliance.md
+++ b/docs/sql-ref-ansi-compliance.md
@@ -583,6 +583,7 @@ Below is a list of all the keywords in Spark SQL.
 |GROUPING|non-reserved|non-reserved|reserved|
 |HANDLER|non-reserved|non-reserved|non-reserved|
 |HAVING|reserved|non-reserved|reserved|
+|HISTORY|non-reserved|non-reserved|non-reserved|
 |HOUR|non-reserved|non-reserved|non-reserved|
 |HOURS|non-reserved|non-reserved|non-reserved|
 |IDENTIFIER|non-reserved|non-reserved|non-reserved|
@@ -741,6 +742,7 @@ Below is a list of all the keywords in Spark SQL.
 |ROLLUP|non-reserved|non-reserved|reserved|
 |ROW|non-reserved|non-reserved|reserved|
 |ROWS|non-reserved|non-reserved|reserved|
+|SCD|non-reserved|non-reserved|non-reserved|
 |SCHEMA|non-reserved|non-reserved|non-reserved|
 |SCHEMAS|non-reserved|non-reserved|non-reserved|
 |SECOND|non-reserved|non-reserved|non-reserved|
@@ -803,6 +805,7 @@ Below is a list of all the keywords in Spark SQL.
 |TINYINT|non-reserved|non-reserved|non-reserved|
 |TO|reserved|non-reserved|reserved|
 |TOUCH|non-reserved|non-reserved|non-reserved|
+|TRACK|non-reserved|non-reserved|non-reserved|
 |TRAILING|reserved|non-reserved|reserved|
 |TRANSACTION|non-reserved|non-reserved|non-reserved|
 |TRANSACTIONS|non-reserved|non-reserved|non-reserved|
diff --git 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
index 64a99e82053b..1445e4015beb 100644
--- 
a/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
+++ 
b/sql/api/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBaseLexer.g4
@@ -300,6 +300,7 @@ GROUPING: 'GROUPING';
 HANDLER: 'HANDLER';
 HAVING: 'HAVING';
 BINARY_HEX: 'X';
+HISTORY: 'HISTORY';
 HOUR: 'HOUR';
 HOURS: 'HOURS';
 IDENTIFIER_KW: 'IDENTIFIER';
@@ -458,6 +459,7 @@ ROW: 'ROW';
 ROWS: 'ROWS';
 SECOND: 'SECOND';
 SECONDS: 'SECONDS';
+SCD: 'SCD';
 SCHEMA: 'SCHEMA';
 SCHEMAS: 'SCHEMAS';
 SECURITY: 'SECURITY';
@@ -519,6 +521,7 @@ TINYINT: 'TINYINT';
 TO: 'TO';
 EXECUTE: 'EXECUTE';
 TOUCH: 'TOUCH';
+TRACK: 'TRACK';
 TRAILING: 'TRAILING';
 TRANSACTION: 'TRANSACTION';
 TRANSACTIONS: 'TRANSACTIONS';
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 29094679e0fb..cdeb34c6a0ca 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
@@ -774,6 +774,8 @@ autoCdcParameters
         autoCdcDeleteClause?
         autoCdcSequenceByClause
         autoCdcColumnsClause?
+        autoCdcStoredAsClause?
+        autoCdcTrackHistoryClause?
     ;
 
 autoCdcDeleteClause
@@ -790,6 +792,16 @@ autoCdcColumnsClause
         ASTERISK EXCEPT LEFT_PAREN exceptCols=identifierSeq RIGHT_PAREN)
     ;
 
+autoCdcStoredAsClause
+    : STORED AS SCD TYPE scdType=INTEGER_VALUE
+    ;
+
+autoCdcTrackHistoryClause
+    : TRACK HISTORY ON (
+        LEFT_PAREN trackCols=identifierSeq RIGHT_PAREN |
+        ASTERISK EXCEPT LEFT_PAREN nonTrackCols=identifierSeq RIGHT_PAREN)
+    ;
+
 identifierReference
     : IDENTIFIER_KW LEFT_PAREN expression RIGHT_PAREN
     | multipartIdentifier
@@ -2160,6 +2172,7 @@ ansiNonReserved
     | GLOBAL
     | GROUPING
     | HANDLER
+    | HISTORY
     | HOUR
     | HOURS
     | IDENTIFIER_KW
@@ -2292,6 +2305,7 @@ ansiNonReserved
     | ROLLUP
     | ROW
     | ROWS
+    | SCD
     | SCHEMA
     | SCHEMAS
     | SECOND
@@ -2346,6 +2360,7 @@ ansiNonReserved
     | TIMESTAMPDIFF
     | TINYINT
     | TOUCH
+    | TRACK
     | TRANSACTION
     | TRANSACTIONS
     | TRANSFORM
@@ -2586,6 +2601,7 @@ nonReserved
     | GROUPING
     | HANDLER
     | HAVING
+    | HISTORY
     | HOUR
     | HOURS
     | IDENTIFIER_KW
@@ -2735,6 +2751,7 @@ nonReserved
     | ROLLUP
     | ROW
     | ROWS
+    | SCD
     | SCHEMA
     | SCHEMAS
     | SECOND
@@ -2795,6 +2812,7 @@ nonReserved
     | TINYINT
     | TO
     | TOUCH
+    | TRACK
     | TRAILING
     | TRANSACTION
     | TRANSACTIONS
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 529aad74956a..26d88c07af5c 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
@@ -1400,7 +1400,10 @@ class AstBuilder extends DataTypeAstBuilder
         deleteCondition = params.deleteCondition,
         sequenceByExpr = params.sequencing,
         includeColumns = params.includeColumns,
-        excludeColumns = params.excludeColumns)
+        excludeColumns = params.excludeColumns,
+        storedAsScdType = params.storedAsScdType,
+        trackHistoryColumns = params.trackHistoryColumns,
+        trackHistoryExceptColumns = params.trackHistoryExceptColumns)
     }
 
   protected def parseAutoCdcParams(params: AutoCdcParametersContext): 
AutoCdcParams =
@@ -1430,13 +1433,43 @@ class AstBuilder extends DataTypeAstBuilder
           visitIdentifierSeq(c.exceptCols).map(UnresolvedAttribute.quoted)
       }
 
+      // STORED AS SCD TYPE <n>. Absent clause defaults to SCD Type 1. Only 1 
and 2 are
+      // supported; reject anything else (including oversized numeric 
literals) with a clear
+      // error rather than a generic parse failure or NumberFormatException. 
Match on the token
+      // text rather than parsing to Int so an overflowing literal cannot 
throw.
+      val storedAsScdType = Option(params.autoCdcStoredAsClause()) match {
+        case Some(c) =>
+          c.scdType.getText match {
+            case "1" => 1
+            case "2" => 2
+            case other =>
+              operationNotAllowed(
+                s"Unsupported SCD type: $other. AUTO CDC only supports STORED 
AS SCD TYPE 1 " +
+                  "or SCD TYPE 2.", c)
+          }
+        case None => 1
+      }
+
+      val trackHistoryClause = Option(params.autoCdcTrackHistoryClause())
+      val trackHistoryColumns = trackHistoryClause.collect {
+        case c if c.trackCols != null =>
+          visitIdentifierSeq(c.trackCols).map(UnresolvedAttribute.quoted)
+      }
+      val trackHistoryExceptColumns = trackHistoryClause.collect {
+        case c if c.nonTrackCols != null =>
+          visitIdentifierSeq(c.nonTrackCols).map(UnresolvedAttribute.quoted)
+      }
+
       AutoCdcParams(
         source = source,
         keys = keys,
         deleteCondition = deleteCondition,
         sequencing = sequencing,
         includeColumns = includeColumns,
-        excludeColumns = excludeColumns)
+        excludeColumns = excludeColumns,
+        storedAsScdType = storedAsScdType,
+        trackHistoryColumns = trackHistoryColumns,
+        trackHistoryExceptColumns = trackHistoryExceptColumns)
     }
 
   /**
@@ -8038,4 +8071,7 @@ case class AutoCdcParams(
     deleteCondition: Option[Expression],
     sequencing: Expression,
     includeColumns: Option[Seq[UnresolvedAttribute]],
-    excludeColumns: Option[Seq[UnresolvedAttribute]])
+    excludeColumns: Option[Seq[UnresolvedAttribute]],
+    storedAsScdType: Int,
+    trackHistoryColumns: Option[Seq[UnresolvedAttribute]],
+    trackHistoryExceptColumns: Option[Seq[UnresolvedAttribute]])
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/AutoCdcInto.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/AutoCdcInto.scala
index cfea98579c63..d78291b7dea3 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/AutoCdcInto.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/AutoCdcInto.scala
@@ -25,7 +25,8 @@ import org.apache.spark.sql.catalyst.trees.BinaryLike
  * Logical plan node for an AUTO CDC INTO operation, used by Spark Declarative 
Pipelines.
  *
  * This represents a CDC (Change Data Capture) operation that applies an 
ordered change event
- * stream from [[source]] into [[targetTable]] using SCD Type 1 (upsert) 
semantics.
+ * stream from [[source]] into [[targetTable]] using SCD Type 1 (upsert) or 
SCD Type 2
+ * (history-tracking) semantics, as selected by [[storedAsScdType]].
  *
  * This node only ever appears as the flow operation of a 
[[CreateFlowCommand]] (parsed from
  * `CREATE FLOW ... AS AUTO CDC INTO ...`); there is no standalone `AUTO CDC 
INTO` syntax. It is a
@@ -53,6 +54,16 @@ import org.apache.spark.sql.catalyst.trees.BinaryLike
  * @param excludeColumns Source columns to exclude from the target table 
(i.e., all columns
  *                       except these). [[None]] when no COLUMNS clause was 
specified. Mutually
  *                       exclusive with [[includeColumns]].
+ * @param storedAsScdType The SCD type of the target table, from `STORED AS 
SCD TYPE <n>`. 1 for
+ *                       SCD Type 1 (upsert) and 2 for SCD Type 2 
(history-tracking). Defaults to
+ *                       1 when no `STORED AS` clause is specified.
+ * @param trackHistoryColumns SCD2-only. An explicit list of columns whose 
value change opens a
+ *                       new history record, from `TRACK HISTORY ON (...)`. 
[[None]] when no TRACK
+ *                       HISTORY clause was specified. Mutually exclusive with
+ *                       [[trackHistoryExceptColumns]].
+ * @param trackHistoryExceptColumns SCD2-only. Columns excluded from history 
tracking, from
+ *                       `TRACK HISTORY ON * EXCEPT (...)`. [[None]] when no 
TRACK HISTORY clause
+ *                       was specified. Mutually exclusive with 
[[trackHistoryColumns]].
  */
 case class AutoCdcInto(
     targetTable: LogicalPlan,
@@ -61,7 +72,10 @@ case class AutoCdcInto(
     deleteCondition: Option[Expression],
     sequenceByExpr: Expression,
     includeColumns: Option[Seq[UnresolvedAttribute]],
-    excludeColumns: Option[Seq[UnresolvedAttribute]]
+    excludeColumns: Option[Seq[UnresolvedAttribute]],
+    storedAsScdType: Int,
+    trackHistoryColumns: Option[Seq[UnresolvedAttribute]],
+    trackHistoryExceptColumns: Option[Seq[UnresolvedAttribute]]
 ) extends LogicalPlan with BinaryLike[LogicalPlan] {
   override def left: LogicalPlan = targetTable
   override def right: LogicalPlan = source
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/v2Commands.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/v2Commands.scala
index 468b43d4915f..e82a65c8f99e 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/v2Commands.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/v2Commands.scala
@@ -852,6 +852,16 @@ case class CreateStreamingTable(
  *                       clause was specified. Mutually exclusive with 
[[excludeColumns]].
  * @param excludeColumns Source columns to exclude. [[None]] when no COLUMNS 
clause was specified.
  *                       Mutually exclusive with [[includeColumns]].
+ * @param storedAsScdType The SCD type of the target table, from `STORED AS 
SCD TYPE <n>`. 1 for
+ *                       SCD Type 1 (upsert) and 2 for SCD Type 2 
(history-tracking). Defaults to
+ *                       1 when no `STORED AS` clause is specified.
+ * @param trackHistoryColumns SCD2-only. An explicit list of columns whose 
value change opens a
+ *                       new history record, from `TRACK HISTORY ON (...)`. 
[[None]] when no TRACK
+ *                       HISTORY clause was specified. Mutually exclusive with
+ *                       [[trackHistoryExceptColumns]].
+ * @param trackHistoryExceptColumns SCD2-only. Columns excluded from history 
tracking, from
+ *                       `TRACK HISTORY ON * EXCEPT (...)`. [[None]] when no 
TRACK HISTORY clause
+ *                       was specified. Mutually exclusive with 
[[trackHistoryColumns]].
  */
 case class CreateStreamingTableAutoCdc(
     name: LogicalPlan,
@@ -864,7 +874,10 @@ case class CreateStreamingTableAutoCdc(
     deleteCondition: Option[Expression],
     sequenceByExpr: Expression,
     includeColumns: Option[Seq[UnresolvedAttribute]],
-    excludeColumns: Option[Seq[UnresolvedAttribute]]
+    excludeColumns: Option[Seq[UnresolvedAttribute]],
+    storedAsScdType: Int,
+    trackHistoryColumns: Option[Seq[UnresolvedAttribute]],
+    trackHistoryExceptColumns: Option[Seq[UnresolvedAttribute]]
 ) extends BinaryCommand with CreatePipelineDataset {
   override def left: LogicalPlan = name
   override def right: LogicalPlan = source
diff --git 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/UnsupportedOperationsSuite.scala
 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/UnsupportedOperationsSuite.scala
index 785ca2f6f597..293523b86f99 100644
--- 
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/UnsupportedOperationsSuite.scala
+++ 
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/analysis/UnsupportedOperationsSuite.scala
@@ -103,7 +103,10 @@ class UnsupportedOperationsSuite extends SparkFunSuite 
with SQLHelper {
       deleteCondition = None,
       sequenceByExpr = attribute,
       includeColumns = None,
-      excludeColumns = None))
+      excludeColumns = None,
+      storedAsScdType = 1,
+      trackHistoryColumns = None,
+      trackHistoryExceptColumns = None))
 
   /*
     
=======================================================================================
diff --git 
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
 
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
index babac5dd9486..3d3260c750f6 100644
--- 
a/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
+++ 
b/sql/connect/client/jdbc/src/test/scala/org/apache/spark/sql/connect/client/jdbc/SparkConnectDatabaseMetaDataSuite.scala
@@ -210,7 +210,7 @@ class SparkConnectDatabaseMetaDataSuite extends 
ConnectFunSuite with RemoteSpark
       val metadata = conn.getMetaData
       // scalastyle:off line.size.limit
       // CURRENT_PATH and SYSTEM are excluded: getSQLKeywords drops SQL:2003 
reserved words (see companion).
-      assert(metadata.getSQLKeywords === 
"ADD,AFTER,AGGREGATE,ALIGN,ALWAYS,ANALYZE,ANTI,ANY_VALUE,APPLY,APPROX,ARCHIVE,ASC,ASOF,AUTO,BERNOULLI,BIN,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BUCKET,BUCKETS,BYTE,CACHE,CASCADE,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CLEAR,CLUSTER,CLUSTERED,CODEGEN,COLLATION,COLLATIONS,COLLECTION,COLUMNS,COMMENT,COMPACT,COMPACTIONS,COMPENSATION,COMPUTE,CONCATENATE,CONTAINS,CONTINUE,COST,CURRENT_DATABASE,CURRENT_SCHEMA,DATA,DATABASE,DATABASES,DATEADD,DATEDIFF
 [...]
+      assert(metadata.getSQLKeywords === 
"ADD,AFTER,AGGREGATE,ALIGN,ALWAYS,ANALYZE,ANTI,ANY_VALUE,APPLY,APPROX,ARCHIVE,ASC,ASOF,AUTO,BERNOULLI,BIN,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BUCKET,BUCKETS,BYTE,CACHE,CASCADE,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CLEAR,CLUSTER,CLUSTERED,CODEGEN,COLLATION,COLLATIONS,COLLECTION,COLUMNS,COMMENT,COMPACT,COMPACTIONS,COMPENSATION,COMPUTE,CONCATENATE,CONTAINS,CONTINUE,COST,CURRENT_DATABASE,CURRENT_SCHEMA,DATA,DATABASE,DATABASES,DATEADD,DATEDIFF
 [...]
       // scalastyle:on line.size.limit
     }
   }
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
index 57fadd3ecb29..9acd0ba6def5 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/SparkSqlParser.scala
@@ -1747,7 +1747,10 @@ class SparkSqlAstBuilder extends AstBuilder {
           deleteCondition = params.deleteCondition,
           sequenceByExpr = params.sequencing,
           includeColumns = params.includeColumns,
-          excludeColumns = params.excludeColumns
+          excludeColumns = params.excludeColumns,
+          storedAsScdType = params.storedAsScdType,
+          trackHistoryColumns = params.trackHistoryColumns,
+          trackHistoryExceptColumns = params.trackHistoryExceptColumns
         )
       } else {
         Option(ctx.query) match {
diff --git 
a/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out 
b/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
index 989bfb725111..7a6608885e3c 100644
--- a/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/keywords-enforced.sql.out
@@ -176,6 +176,7 @@ GROUP       true
 GROUPING       false
 HANDLER        false
 HAVING true
+HISTORY        false
 HOUR   false
 HOURS  false
 IDENTIFIED     false
@@ -332,6 +333,7 @@ ROLLBACK    false
 ROLLUP false
 ROW    false
 ROWS   false
+SCD    false
 SCHEMA false
 SCHEMAS        false
 SECOND false
@@ -392,6 +394,7 @@ TIMESTAMP_NTZ       false
 TINYINT        false
 TO     true
 TOUCH  false
+TRACK  false
 TRAILING       true
 TRANSACTION    false
 TRANSACTIONS   false
diff --git a/sql/core/src/test/resources/sql-tests/results/keywords.sql.out 
b/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
index ee127e584f17..db8bfa0e205c 100644
--- a/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/keywords.sql.out
@@ -176,6 +176,7 @@ GROUP       false
 GROUPING       false
 HANDLER        false
 HAVING false
+HISTORY        false
 HOUR   false
 HOURS  false
 IDENTIFIED     false
@@ -332,6 +333,7 @@ ROLLBACK    false
 ROLLUP false
 ROW    false
 ROWS   false
+SCD    false
 SCHEMA false
 SCHEMAS        false
 SECOND false
@@ -392,6 +394,7 @@ TIMESTAMP_NTZ       false
 TINYINT        false
 TO     false
 TOUCH  false
+TRACK  false
 TRAILING       false
 TRANSACTION    false
 TRANSACTIONS   false
diff --git 
a/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out 
b/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
index ee127e584f17..db8bfa0e205c 100644
--- a/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
+++ b/sql/core/src/test/resources/sql-tests/results/nonansi/keywords.sql.out
@@ -176,6 +176,7 @@ GROUP       false
 GROUPING       false
 HANDLER        false
 HAVING false
+HISTORY        false
 HOUR   false
 HOURS  false
 IDENTIFIED     false
@@ -332,6 +333,7 @@ ROLLBACK    false
 ROLLUP false
 ROW    false
 ROWS   false
+SCD    false
 SCHEMA false
 SCHEMAS        false
 SECOND false
@@ -392,6 +394,7 @@ TIMESTAMP_NTZ       false
 TINYINT        false
 TO     false
 TOUCH  false
+TRACK  false
 TRAILING       false
 TRANSACTION    false
 TRANSACTIONS   false
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v2/AutoCdcParserSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v2/AutoCdcParserSuite.scala
index c9d28edaf6a6..34642497eb57 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v2/AutoCdcParserSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/command/v2/AutoCdcParserSuite.scala
@@ -39,9 +39,10 @@ import org.apache.spark.sql.types.IntegerType
  *   1. CREATE FLOW <name> [COMMENT ...] AS AUTO CDC INTO <target> ...
  *   2. CREATE STREAMING TABLE <name> FLOW AUTO CDC ...
  *
- * Snapshot CDC, SCD Type 2, IGNORE NULL UPDATES, and APPLY AS TRUNCATE WHEN 
are not
- * supported and should fail to parse. The standalone AUTO CDC INTO form 
(without CREATE FLOW
- * or CREATE STREAMING TABLE) is also not supported.
+ * Both forms support `STORED AS SCD TYPE 1|2` and, under SCD Type 2, `TRACK 
HISTORY ON ...`.
+ * Snapshot CDC, IGNORE NULL UPDATES, and APPLY AS TRUNCATE WHEN are not 
supported and should
+ * fail to parse. The standalone AUTO CDC INTO form (without CREATE FLOW or 
CREATE STREAMING
+ * TABLE) is also not supported.
  */
 class AutoCdcParserSuite extends CommandSuiteBase with AnalysisTest {
   protected lazy val parser = new SparkSqlParser()
@@ -80,6 +81,10 @@ class AutoCdcParserSuite extends CommandSuiteBase with 
AnalysisTest {
     assert(cdc.sequenceByExpr == UnresolvedAttribute("timestamp"))
     assert(cdc.includeColumns.isEmpty)
     assert(cdc.excludeColumns.isEmpty)
+    // No STORED AS clause defaults to SCD Type 1 with no history tracking.
+    assert(cdc.storedAsScdType == 1)
+    assert(cdc.trackHistoryColumns.isEmpty)
+    assert(cdc.trackHistoryExceptColumns.isEmpty)
   }
 
   test("CREATE FLOW AS AUTO CDC INTO - multipart source name") {
@@ -196,6 +201,164 @@ class AutoCdcParserSuite extends CommandSuiteBase with 
AnalysisTest {
     assert(cdc.includeColumns.get.map(_.name) == Seq("key1", "key2", "key3", 
"timestamp"))
   }
 
+  test("CREATE FLOW AS AUTO CDC INTO - STORED AS SCD TYPE 1") {
+    val plan = parser.parsePlan(
+      """CREATE FLOW f AS AUTO CDC INTO target
+        |FROM STREAM(source)
+        |KEYS (id)
+        |SEQUENCE BY ts
+        |STORED AS SCD TYPE 1""".stripMargin)
+
+    val cdc = 
plan.asInstanceOf[CreateFlowCommand].flowOperation.asInstanceOf[AutoCdcInto]
+    assert(cdc.storedAsScdType == 1)
+    assert(cdc.trackHistoryColumns.isEmpty)
+    assert(cdc.trackHistoryExceptColumns.isEmpty)
+  }
+
+  test("CREATE FLOW AS AUTO CDC INTO - STORED AS SCD TYPE 2") {
+    val plan = parser.parsePlan(
+      """CREATE FLOW f AS AUTO CDC INTO target
+        |FROM STREAM(source)
+        |KEYS (id)
+        |SEQUENCE BY ts
+        |STORED AS SCD TYPE 2""".stripMargin)
+
+    val cdc = 
plan.asInstanceOf[CreateFlowCommand].flowOperation.asInstanceOf[AutoCdcInto]
+    assert(cdc.storedAsScdType == 2)
+    assert(cdc.trackHistoryColumns.isEmpty)
+    assert(cdc.trackHistoryExceptColumns.isEmpty)
+  }
+
+  test("CREATE FLOW AS AUTO CDC INTO - STORED AS SCD TYPE 2 with TRACK HISTORY 
ON") {
+    val plan = parser.parsePlan(
+      """CREATE FLOW f AS AUTO CDC INTO target
+        |FROM STREAM(source)
+        |KEYS (id)
+        |SEQUENCE BY ts
+        |STORED AS SCD TYPE 2
+        |TRACK HISTORY ON (val1, val2)""".stripMargin)
+
+    val cdc = 
plan.asInstanceOf[CreateFlowCommand].flowOperation.asInstanceOf[AutoCdcInto]
+    assert(cdc.storedAsScdType == 2)
+    assert(cdc.trackHistoryColumns.get.map(_.name) == Seq("val1", "val2"))
+    assert(cdc.trackHistoryExceptColumns.isEmpty)
+  }
+
+  test("CREATE FLOW AS AUTO CDC INTO - STORED AS SCD TYPE 2 with TRACK HISTORY 
ON * EXCEPT") {
+    val plan = parser.parsePlan(
+      """CREATE FLOW f AS AUTO CDC INTO target
+        |FROM STREAM(source)
+        |KEYS (id)
+        |SEQUENCE BY ts
+        |STORED AS SCD TYPE 2
+        |TRACK HISTORY ON * EXCEPT (op, ts)""".stripMargin)
+
+    val cdc = 
plan.asInstanceOf[CreateFlowCommand].flowOperation.asInstanceOf[AutoCdcInto]
+    assert(cdc.storedAsScdType == 2)
+    assert(cdc.trackHistoryColumns.isEmpty)
+    assert(cdc.trackHistoryExceptColumns.get.map(_.name) == Seq("op", "ts"))
+  }
+
+  test("CREATE FLOW AS AUTO CDC INTO - all clauses combined including SCD2 and 
TRACK HISTORY") {
+    val plan = parser.parsePlan(
+      """CREATE FLOW f AS AUTO CDC INTO target
+        |FROM STREAM(source)
+        |KEYS (key1, key2)
+        |APPLY AS DELETE WHEN key3 = 3
+        |SEQUENCE BY timestamp
+        |COLUMNS (key1, key2, key3, timestamp)
+        |STORED AS SCD TYPE 2
+        |TRACK HISTORY ON (key3)""".stripMargin)
+
+    val cdc = 
plan.asInstanceOf[CreateFlowCommand].flowOperation.asInstanceOf[AutoCdcInto]
+    assert(cdc.deleteCondition.isDefined)
+    assert(cdc.includeColumns.get.map(_.name) == Seq("key1", "key2", "key3", 
"timestamp"))
+    assert(cdc.storedAsScdType == 2)
+    assert(cdc.trackHistoryColumns.get.map(_.name) == Seq("key3"))
+  }
+
+  test("CREATE STREAMING TABLE FLOW AUTO CDC - STORED AS SCD TYPE 2 with TRACK 
HISTORY ON") {
+    val plan = parser.parsePlan(
+      """CREATE STREAMING TABLE st FLOW AUTO CDC
+        |FROM STREAM(source)
+        |KEYS (id)
+        |SEQUENCE BY ts
+        |STORED AS SCD TYPE 2
+        |TRACK HISTORY ON (val1, val2)""".stripMargin)
+
+    val cst = plan.asInstanceOf[CreateStreamingTableAutoCdc]
+    assert(cst.storedAsScdType == 2)
+    assert(cst.trackHistoryColumns.get.map(_.name) == Seq("val1", "val2"))
+    assert(cst.trackHistoryExceptColumns.isEmpty)
+  }
+
+  test("CREATE STREAMING TABLE FLOW AUTO CDC - STORED AS SCD TYPE 2 with TRACK 
HISTORY EXCEPT") {
+    val plan = parser.parsePlan(
+      """CREATE STREAMING TABLE st FLOW AUTO CDC
+        |FROM STREAM(source)
+        |KEYS (id)
+        |SEQUENCE BY ts
+        |STORED AS SCD TYPE 2
+        |TRACK HISTORY ON * EXCEPT (op)""".stripMargin)
+
+    val cst = plan.asInstanceOf[CreateStreamingTableAutoCdc]
+    assert(cst.storedAsScdType == 2)
+    assert(cst.trackHistoryColumns.isEmpty)
+    assert(cst.trackHistoryExceptColumns.get.map(_.name) == Seq("op"))
+  }
+
+  test("AUTO CDC - TRACK HISTORY before STORED AS is not allowed") {
+    intercept[ParseException] {
+      parser.parsePlan(
+        """CREATE FLOW f AS AUTO CDC INTO target
+          |FROM STREAM(source)
+          |KEYS (id)
+          |SEQUENCE BY ts
+          |TRACK HISTORY ON (val)
+          |STORED AS SCD TYPE 2""".stripMargin)
+    }
+  }
+
+  test("AUTO CDC - STORED AS SCD TYPE 3 is not allowed") {
+    val ex = intercept[ParseException] {
+      parser.parsePlan(
+        """CREATE FLOW f AS AUTO CDC INTO target
+          |FROM STREAM(source)
+          |KEYS (id)
+          |SEQUENCE BY ts
+          |STORED AS SCD TYPE 3""".stripMargin)
+    }
+    assert(ex.getMessage.contains("Unsupported SCD type: 3"))
+  }
+
+  test("AUTO CDC - STORED AS SCD TYPE with an overflowing number is rejected 
cleanly") {
+    // The number is INTEGER_VALUE (DIGIT+), so an oversized literal must 
route through the
+    // clear "Unsupported SCD type" error rather than throwing a 
NumberFormatException.
+    val ex = intercept[ParseException] {
+      parser.parsePlan(
+        """CREATE FLOW f AS AUTO CDC INTO target
+          |FROM STREAM(source)
+          |KEYS (id)
+          |SEQUENCE BY ts
+          |STORED AS SCD TYPE 99999999999999999999""".stripMargin)
+    }
+    assert(ex.getMessage.contains("Unsupported SCD type: 
99999999999999999999"))
+  }
+
+  test("AUTO CDC - STORED AS SCD TYPE tolerates extra whitespace between 
words") {
+    // SCD, TYPE and the number are separate tokens, so any whitespace between 
them is accepted.
+    val plan = parser.parsePlan(
+      """CREATE FLOW f AS AUTO CDC INTO target
+        |FROM STREAM(source)
+        |KEYS (id)
+        |SEQUENCE BY ts
+        |STORED AS SCD   TYPE
+        |    2""".stripMargin)
+
+    val cdc = 
plan.asInstanceOf[CreateFlowCommand].flowOperation.asInstanceOf[AutoCdcInto]
+    assert(cdc.storedAsScdType == 2)
+  }
+
   // 
---------------------------------------------------------------------------
   // CREATE STREAMING TABLE ... FLOW AUTO CDC
   // 
---------------------------------------------------------------------------
@@ -810,35 +973,39 @@ class AutoCdcParserSuite extends CommandSuiteBase with 
AnalysisTest {
     )
   }
 
-  test("STORED AS SCD TYPE 2 is not supported") {
-    checkError(
-      intercept[ParseException] {
-        parser.parsePlan(
-          """CREATE FLOW f AS AUTO CDC INTO target
-            |FROM STREAM(source)
-            |KEYS (id)
-            |SEQUENCE BY ts
-            |STORED AS SCD TYPE 2""".stripMargin)
-      },
-      condition = "PARSE_SYNTAX_ERROR",
-      sqlState = "42601",
-      parameters = Map("error" -> "'STORED'", "hint" -> "")
-    )
+  test("TRACK HISTORY ON without parentheses is not allowed") {
+    // The grammar requires a parenthesized column list or `* EXCEPT (...)`, 
so a bare column
+    // list after TRACK HISTORY ON fails to parse.
+    intercept[ParseException] {
+      parser.parsePlan(
+        """CREATE FLOW f AS AUTO CDC INTO target
+          |FROM STREAM(source)
+          |KEYS (id)
+          |SEQUENCE BY ts
+          |STORED AS SCD TYPE 2
+          |TRACK HISTORY ON value1, value2""".stripMargin)
+    }
   }
 
-  test("TRACK HISTORY ON is not supported") {
-    checkError(
-      intercept[ParseException] {
-        parser.parsePlan(
-          """CREATE FLOW f AS AUTO CDC INTO target
-            |FROM STREAM(source)
-            |KEYS (id)
-            |SEQUENCE BY ts
-            |TRACK HISTORY ON value1, value2""".stripMargin)
-      },
-      condition = "PARSE_SYNTAX_ERROR",
-      sqlState = "42601",
-      parameters = Map("error" -> "'TRACK'", "hint" -> "")
-    )
+  test("new keywords history, track, scd remain usable as identifiers") {
+    // history / track / scd are non-reserved, so they must still parse as 
ordinary table and
+    // column identifiers. This guards their non-reserved classification 
against regressions.
+    val plan = parser.parsePlan(
+      """CREATE FLOW f AS AUTO CDC INTO track
+        |FROM STREAM(scd)
+        |KEYS (history)
+        |SEQUENCE BY track
+        |COLUMNS (history, track, scd)
+        |STORED AS SCD TYPE 2
+        |TRACK HISTORY ON (history, scd)""".stripMargin)
+
+    val cdc = 
plan.asInstanceOf[CreateFlowCommand].flowOperation.asInstanceOf[AutoCdcInto]
+    assert(cdc.targetTable.asInstanceOf[UnresolvedIdentifier].nameParts == 
Seq("track"))
+    assert(streamSource(cdc.source).multipartIdentifier == Seq("scd"))
+    assert(cdc.keys.map(_.name) == Seq("history"))
+    assert(cdc.sequenceByExpr == UnresolvedAttribute("track"))
+    assert(cdc.includeColumns.get.map(_.name) == Seq("history", "track", 
"scd"))
+    assert(cdc.storedAsScdType == 2)
+    assert(cdc.trackHistoryColumns.get.map(_.name) == Seq("history", "scd"))
   }
 }
diff --git 
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
index ff9e203f5915..fbc150f79ab4 100644
--- 
a/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
+++ 
b/sql/hive-thriftserver/src/test/scala/org/apache/spark/sql/hive/thriftserver/ThriftServerWithSparkContextSuite.scala
@@ -214,7 +214,7 @@ trait ThriftServerWithSparkContextSuite extends 
SharedThriftServer {
       val sessionHandle = client.openSession(user, "")
       val infoValue = client.getInfo(sessionHandle, 
GetInfoType.CLI_ODBC_KEYWORDS)
       // scalastyle:off line.size.limit
-      assert(infoValue.getStringValue == 
"ADD,AFTER,AGGREGATE,ALIGN,ALL,ALTER,ALWAYS,ANALYZE,AND,ANTI,ANY,ANY_VALUE,APPLY,APPROX,ARCHIVE,ARRAY,AS,ASC,ASENSITIVE,ASOF,AT,ATOMIC,AUTHORIZATION,AUTO,BEGIN,BERNOULLI,BETWEEN,BIGINT,BIN,BINARY,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BOOLEAN,BOTH,BUCKET,BUCKETS,BY,BYTE,CACHE,CALL,CALLED,CASCADE,CASE,CAST,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CHAR,CHARACTER,CHECK,CLEAR,CLOSE,CLUSTER,CLUSTERED,CODEGEN,COLLATE,COLLATION,COLLATIONS,COLLECTION,C
 [...]
+      assert(infoValue.getStringValue == 
"ADD,AFTER,AGGREGATE,ALIGN,ALL,ALTER,ALWAYS,ANALYZE,AND,ANTI,ANY,ANY_VALUE,APPLY,APPROX,ARCHIVE,ARRAY,AS,ASC,ASENSITIVE,ASOF,AT,ATOMIC,AUTHORIZATION,AUTO,BEGIN,BERNOULLI,BETWEEN,BIGINT,BIN,BINARY,BINDING,BIN_DISTRIBUTE_RATIO,BIN_END,BIN_START,BOOLEAN,BOTH,BUCKET,BUCKETS,BY,BYTE,CACHE,CALL,CALLED,CASCADE,CASE,CAST,CATALOG,CATALOGS,CDC,CHANGE,CHANGES,CHAR,CHARACTER,CHECK,CLEAR,CLOSE,CLUSTER,CLUSTERED,CODEGEN,COLLATE,COLLATION,COLLATIONS,COLLECTION,C
 [...]
       // scalastyle:on line.size.limit
     }
   }
diff --git 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/SqlGraphRegistrationContext.scala
 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/SqlGraphRegistrationContext.scala
index 36a1f17209a2..a8dd94040b91 100644
--- 
a/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/SqlGraphRegistrationContext.scala
+++ 
b/sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/SqlGraphRegistrationContext.scala
@@ -275,9 +275,10 @@ class SqlGraphRegistrationContext(
    * into the [[ChangeArgs]] consumed by an [[AutoCdcFlow]]. Shared by the two 
SQL AUTO CDC entry
    * points: `CREATE STREAMING TABLE ... FLOW AUTO CDC ...` and `CREATE FLOW 
... AS AUTO CDC INTO`.
    *
-   * SQL AUTO CDC syntax only supports SCD Type 1, so 
[[ChangeArgs.storedAsScdType]] is always
-   * [[ScdType.Type1]]. [[includeColumns]] and [[excludeColumns]] are mutually 
exclusive at the
-   * grammar level; the guard here is defensive.
+   * `STORED AS SCD TYPE <n>` selects [[ScdType]] (defaulting to 
[[ScdType.Type1]] when absent).
+   * `TRACK HISTORY ON ...` populates the SCD2-only 
[[ChangeArgs.trackHistorySelection]] and is
+   * rejected under SCD1. [[includeColumns]]/[[excludeColumns]] and the two 
track-history lists are
+   * each mutually exclusive at the grammar level; the guards here are 
defensive.
    */
   private def buildChangeArgs(
       keys: Seq[UnresolvedAttribute],
@@ -285,6 +286,9 @@ class SqlGraphRegistrationContext(
       deleteCondition: Option[Expression],
       includeColumns: Option[Seq[UnresolvedAttribute]],
       excludeColumns: Option[Seq[UnresolvedAttribute]],
+      storedAsScdType: Int,
+      trackHistoryColumns: Option[Seq[UnresolvedAttribute]],
+      trackHistoryExceptColumns: Option[Seq[UnresolvedAttribute]],
       queryOrigin: QueryOrigin): ChangeArgs = {
     val columnSelection: Option[ColumnSelection] = (includeColumns, 
excludeColumns) match {
       case (Some(_), Some(_)) =>
@@ -300,12 +304,50 @@ class SqlGraphRegistrationContext(
         None
     }
 
+    val scdType: ScdType = storedAsScdType match {
+      case 1 => ScdType.Type1
+      case 2 => ScdType.Type2
+      case other =>
+        throw SqlGraphElementRegistrationException(
+          msg = s"Unsupported AUTO CDC SCD type: $other. Only SCD TYPE 1 and 
SCD TYPE 2 are " +
+            "supported.",
+          queryOrigin = queryOrigin
+        )
+    }
+
+    // TRACK HISTORY is only meaningful under SCD2; reject it for SCD1 before 
interpreting the
+    // clause. The grammar allows the clause regardless of the STORED AS type, 
so this is where
+    // the SCD2 requirement is enforced.
+    if (scdType != ScdType.Type2 &&
+      (trackHistoryColumns.isDefined || trackHistoryExceptColumns.isDefined)) {
+      throw SqlGraphElementRegistrationException(
+        msg = "AUTO CDC TRACK HISTORY requires STORED AS SCD TYPE 2.",
+        queryOrigin = queryOrigin
+      )
+    }
+
+    val trackHistorySelection: Option[ColumnSelection] =
+      (trackHistoryColumns, trackHistoryExceptColumns) match {
+        case (Some(_), Some(_)) =>
+          throw SqlGraphElementRegistrationException(
+            msg = "AUTO CDC cannot specify both TRACK HISTORY ON and TRACK 
HISTORY ON * EXCEPT.",
+            queryOrigin = queryOrigin
+          )
+        case (Some(tracked), None) =>
+          
Option(ColumnSelection.IncludeColumns(tracked.map(toUnqualifiedColumnName)))
+        case (None, Some(nonTracked)) =>
+          
Option(ColumnSelection.ExcludeColumns(nonTracked.map(toUnqualifiedColumnName)))
+        case (None, None) =>
+          None
+      }
+
     ChangeArgs(
       keys = keys.map(toUnqualifiedColumnName),
       sequencing = Column(sequenceByExpr),
-      storedAsScdType = ScdType.Type1,
+      storedAsScdType = scdType,
       deleteCondition = deleteCondition.map(Column(_)),
-      columnSelection = columnSelection
+      columnSelection = columnSelection,
+      trackHistorySelection = trackHistorySelection
     )
   }
 
@@ -374,6 +416,9 @@ class SqlGraphRegistrationContext(
             deleteCondition = cst.deleteCondition,
             includeColumns = cst.includeColumns,
             excludeColumns = cst.excludeColumns,
+            storedAsScdType = cst.storedAsScdType,
+            trackHistoryColumns = cst.trackHistoryColumns,
+            trackHistoryExceptColumns = cst.trackHistoryExceptColumns,
             queryOrigin = queryOrigin
           )
         )
@@ -588,6 +633,9 @@ class SqlGraphRegistrationContext(
                 deleteCondition = a.deleteCondition,
                 includeColumns = a.includeColumns,
                 excludeColumns = a.excludeColumns,
+                storedAsScdType = a.storedAsScdType,
+                trackHistoryColumns = a.trackHistoryColumns,
+                trackHistoryExceptColumns = a.trackHistoryExceptColumns,
                 queryOrigin = queryOrigin
               )
             )
diff --git 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
index 3297709174c5..cec8db6ec528 100644
--- 
a/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
+++ 
b/sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/SqlPipelineSuite.scala
@@ -1120,7 +1120,7 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
   // Two SQL forms register an [[AutoCdcFlow]] into the dataflow graph:
   //   1. CREATE STREAMING TABLE <name> FLOW AUTO CDC FROM <source> KEYS (...) 
SEQUENCE BY <expr>
   //   2. CREATE FLOW <name> AS AUTO CDC INTO <target> FROM <source> KEYS 
(...) SEQUENCE BY <expr>
-  // SQL AUTO CDC only supports SCD Type 1.
+  // Both forms support STORED AS SCD TYPE 1|2 and, under SCD2, TRACK HISTORY 
ON ....
   // 
===========================================================================
 
   /** Returns the single unresolved [[AutoCdcFlow]] registered for the given 
flow identifier. */
@@ -1215,6 +1215,99 @@ class SqlPipelineSuite extends PipelineTest with 
SharedSparkSession {
     }
   }
 
+  test("AUTO CDC STORED AS SCD TYPE 2 maps onto ChangeArgs") {
+    val graph = unresolvedDataflowGraphFromSql(
+      sqlText = s"""
+                   |CREATE STREAMING TABLE st
+                   |FLOW AUTO CDC
+                   |FROM STREAM $externalTable1Ident
+                   |KEYS (id)
+                   |SEQUENCE BY id
+                   |STORED AS SCD TYPE 2
+                   |""".stripMargin
+    )
+
+    val flow = autoCdcFlowFor(graph, "st")
+    assert(flow.changeArgs.storedAsScdType == ScdType.Type2)
+    assert(flow.changeArgs.trackHistorySelection.isEmpty)
+  }
+
+  test("AUTO CDC STORED AS SCD TYPE 2 with TRACK HISTORY ON maps onto 
ChangeArgs") {
+    val graph = unresolvedDataflowGraphFromSql(
+      sqlText = s"""
+                   |CREATE STREAMING TABLE target;
+                   |CREATE FLOW f AS AUTO CDC INTO target
+                   |FROM STREAM $externalTable1Ident
+                   |KEYS (id)
+                   |SEQUENCE BY id
+                   |STORED AS SCD TYPE 2
+                   |TRACK HISTORY ON (id)
+                   |""".stripMargin
+    )
+
+    val flow = autoCdcFlowFor(graph, "f")
+    assert(flow.changeArgs.storedAsScdType == ScdType.Type2)
+    flow.changeArgs.trackHistorySelection match {
+      case Some(ColumnSelection.IncludeColumns(cols)) => 
assert(cols.map(_.name) == Seq("id"))
+      case other => fail(s"Expected IncludeColumns(id), got $other")
+    }
+  }
+
+  test("AUTO CDC STORED AS SCD TYPE 2 with TRACK HISTORY ON * EXCEPT maps onto 
ChangeArgs") {
+    val graph = unresolvedDataflowGraphFromSql(
+      sqlText = s"""
+                   |CREATE STREAMING TABLE st
+                   |FLOW AUTO CDC
+                   |FROM STREAM $externalTable1Ident
+                   |KEYS (id)
+                   |SEQUENCE BY id
+                   |STORED AS SCD TYPE 2
+                   |TRACK HISTORY ON * EXCEPT (id)
+                   |""".stripMargin
+    )
+
+    val flow = autoCdcFlowFor(graph, "st")
+    assert(flow.changeArgs.storedAsScdType == ScdType.Type2)
+    flow.changeArgs.trackHistorySelection match {
+      case Some(ColumnSelection.ExcludeColumns(cols)) => 
assert(cols.map(_.name) == Seq("id"))
+      case other => fail(s"Expected ExcludeColumns(id), got $other")
+    }
+  }
+
+  test("AUTO CDC TRACK HISTORY without SCD TYPE 2 is rejected") {
+    val ex = intercept[SqlGraphElementRegistrationException] {
+      unresolvedDataflowGraphFromSql(
+        sqlText = s"""
+                     |CREATE STREAMING TABLE st
+                     |FLOW AUTO CDC
+                     |FROM STREAM $externalTable1Ident
+                     |KEYS (id)
+                     |SEQUENCE BY id
+                     |TRACK HISTORY ON (id)
+                     |""".stripMargin
+      )
+    }
+    assert(ex.getMessage.contains("TRACK HISTORY requires STORED AS SCD TYPE 
2"))
+  }
+
+  test("AUTO CDC TRACK HISTORY with explicit STORED AS SCD TYPE 1 is 
rejected") {
+    // The implicit-default-SCD1 case is covered above; this pins the explicit 
SCD TYPE 1 path.
+    val ex = intercept[SqlGraphElementRegistrationException] {
+      unresolvedDataflowGraphFromSql(
+        sqlText = s"""
+                     |CREATE STREAMING TABLE st
+                     |FLOW AUTO CDC
+                     |FROM STREAM $externalTable1Ident
+                     |KEYS (id)
+                     |SEQUENCE BY id
+                     |STORED AS SCD TYPE 1
+                     |TRACK HISTORY ON (id)
+                     |""".stripMargin
+      )
+    }
+    assert(ex.getMessage.contains("TRACK HISTORY requires STORED AS SCD TYPE 
2"))
+  }
+
   test("Explicit column list is rejected for CREATE STREAMING TABLE FLOW AUTO 
CDC") {
     val ex = intercept[SqlGraphElementRegistrationException] {
       unresolvedDataflowGraphFromSql(


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


Reply via email to