spark-test commented on code in PR #57644:
URL: https://github.com/apache/spark/pull/57644#discussion_r4365140833


##########
sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/graph/elements.scala:
##########
@@ -183,10 +184,23 @@ case class VirtualTableInput(
    */
   def load(asStreaming: Boolean): DataFrame = {
     val deducedSchema = specifiedSchema match {
-      // If the user specified a schema, use it directly.
+      // For a declared AUTO CDC target, materialization reconciles the user's 
reserved column(s)
+      // in place and appends any the declaration omitted (see 
DatasetManager.materializeTable).
+      // Produce that same schema here so a same-graph downstream consumer 
plans against the columns
+      // the target is actually created with, rather than the declaration 
verbatim.
+      case Some(ss) if 
availableFlows.exists(GraphElementTypeUtils.isAutoCdcFlow) =>
+        val flowInferredSchema = SchemaInferenceUtils.inferSchemaFromFlows(
+          tableIdentifier = identifier,
+          flows = availableFlows,
+          userSpecifiedSchema = None,
+          sessionCaseSensitive = sessionCaseSensitive)
+        val resolver = SchemaInferenceUtils.resolverFor(
+          SchemaInferenceUtils.effectiveCaseSensitivity(
+            identifier, availableFlows, sessionCaseSensitive))
+        AutoCdcReservedNames.appendEngineOwnedReservedFields(ss, 
flowInferredSchema, resolver)

Review Comment:
   **Nit (P3):** `VirtualTableInput.load` passes the raw declared schema to 
`appendEngineOwnedReservedFields`, and `load` runs while downstream flows are 
analyzed: `FlowAnalysis.readGraphInput` calls it inside the reading flow's 
`Try(FlowAnalysis.analyze(...))`. `DataflowGraph.validationFailure` then 
reports flow-analysis failures (`validateSuccessfulFlowAnalysis`) before 
`validateUserSpecifiedSchemas`, which is where 742a80b added 
`validateDeclaredReservedFields`. Nothing at registration rejects 
case-duplicate columns either: `SqlGraphRegistrationContext` builds the schema 
with `StructType(cst.columns.map(_.toV1Column))`.
   
   As a result, under case-insensitive analysis, a target declared with both 
`__spark_autocdc_metadata` and `__SPARK_AUTOCDC_METADATA`, plus `CREATE 
MATERIALIZED VIEW copy AS SELECT * FROM target` in the same graph, still fails 
with the helper's `SparkException.internalError` ("Ambiguous reserved AUTO CDC 
field ...") attributed to `copy`, instead of 
`AUTOCDC_INVALID_RESERVED_SCHEMA.DUPLICATE_COLUMN` on `target`. The new 
DUPLICATE_COLUMN test passes only because its graph has no reader of the 
target. Under a case-sensitive flow the two names are distinct columns and 
nothing fails. The comments saying the read path only sees validated 
declarations (`AutoCdcReservedNames.scala:88-90` and `124-126`, 
`GraphValidations.scala:294-296`) do not hold for this route.
   
   Suggested fix: have `appendEngineOwnedReservedFields` take the table name 
and run `validateDeclaredReservedFields` before reconciling (or call the 
validator from `load`), so every route raises the user-facing error, then 
update those comments. The validation-time half of the earlier comment on 
`GraphValidations.scala` is addressed by `validateDeclaredReservedFields`.
   
   This extends existing discussions 
[1](https://github.com/apache/spark/pull/57644#issuecomment-6369698193), 
[2](https://github.com/apache/spark/pull/57644#issuecomment-6373631540).
   
   **Verification:**
   - **Regression:** A pipeline whose AUTO CDC target declares both case 
variants of the reserved metadata column under case-insensitive analysis, and 
whose graph also contains a dataset that reads that target, fails with 
AUTOCDC_INVALID_RESERVED_SCHEMA.DUPLICATE_COLUMN naming the target rather than 
with INTERNAL_ERROR.
   - **Compatibility:** These all keep their current validation outcome and 
their current materialized and analysis-time schemas: data-only declarations, 
declarations that list the reserved column once, case-sensitive flows with a 
distinct differently cased user column, and tables not written by AUTO CDC.



##########
sql/pipelines/src/test/scala/org/apache/spark/sql/pipelines/graph/UserSpecifiedSchemaValidationSuite.scala:
##########
@@ -206,4 +247,79 @@ class UserSpecifiedSchemaValidationSuite extends 
PipelineTest with SharedSparkSe
       declaredSchema = Some(autoCdcInferredSchema("auto_cdc_flow"))).validate(
         spark.sessionState.conf.caseSensitiveAnalysis)
   }
+
+  test("a user-specified schema that declares the reserved metadata column 
with the wrong " +
+    "type is rejected for an AUTO CDC flow") {
+    // Declaring the engine-owned metadata column is allowed, but only with 
the shape the engine
+    // produces. A conflicting type is caught when the declared schema is 
merged with the inferred
+    // one, before the AUTO CDC MERGE ever runs, so the target can never be 
created with a
+    // malformed metadata column.
+    val wrongTypeMetadata = StructType(
+      dataSchema.fields :+ 
StructField(AutoCdcReservedNames.cdcMetadataColName, StringType))
+    assertSchemaIncompatible(
+      autoCdcGraph(flowName = "auto_cdc_flow", declaredSchema = 
Some(wrongTypeMetadata)))
+  }
+
+  test("data-only user-specified schema is accepted for an SCD2 AUTO CDC 
flow") {
+    // An SCD2 flow's inferred schema is the data columns plus the SCD2 
interval bounds
+    // __START_AT / __END_AT plus the reserved metadata column. The interval 
bounds are part of the
+    // SCD2 contract and stay user-visible; only the prefixed metadata column 
is engine-reserved,
+    // so a declared schema that omits just that column is accepted.
+    val declaredWithoutReserved = StructType(
+      autoCdcInferredSchema("target", ScdType.Type2)
+        .filterNot(_.name.startsWith(AutoCdcReservedNames.prefix)))
+    autoCdcGraph("target", Some(declaredWithoutReserved), ScdType.Type2)
+      .validate(spark.sessionState.conf.caseSensitiveAnalysis)
+  }
+
+  /** The engine-owned reserved metadata field from the inferred AUTO CDC 
schema. */
+  private def metadataField: StructField =
+    autoCdcInferredSchema("target").fields
+      .find(_.name == AutoCdcReservedNames.cdcMetadataColName).get
+
+  test("a declared schema with an unknown reserved-prefixed column is rejected 
for an AUTO CDC " +
+    "flow") {
+    // A reserved-prefixed column the engine never produces (a typo, or an 
extra reserved-namespace
+    // column) is rejected up front rather than surviving reconciliation into 
the target.
+    val schema = StructType(
+      dataSchema.fields :+ StructField(s"${AutoCdcReservedNames.prefix}bogus", 
IntegerType))
+    val ex = intercept[AnalysisException](
+      validateGraph(autoCdcGraph("auto_cdc_flow", Some(schema))))
+    assert(ex.getCondition == "AUTOCDC_INVALID_RESERVED_SCHEMA.UNKNOWN_COLUMN")

Review Comment:
   **Nit (P3):** 742a80b adds four behaviors that no test would catch 
regressing:
   
   - `validateReservedLevel`'s `ArrayType` arm 
(`AutoCdcReservedNames.scala:192-197`): the nested rejection tests here 
sequence by `col("version")` (LONG), so the metadata struct never contains an 
array. Removing the arm would let an unknown nested field inside an 
`ARRAY<STRUCT<...>>` sequencing field reach the target.
   - `reconcileReservedField` building from the declared field so a `COMMENT` 
survives (`AutoCdcReservedNames.scala:212-218`): no test declares metadata on a 
reserved column, so reverting to `engine.copy(name = declared.name)` stays 
green.
   - `reconcileReservedStruct` appending the engine nested fields a declaration 
omits (`AutoCdcReservedNames.scala:258`): every test declares the full metadata 
struct or adds a field to it. A declaration listing only `deleteSequence` 
passes validation, so dropping the append would create a target without 
`upsertSequence` and fail only at MERGE time.
   - These four `AUTOCDC_INVALID_RESERVED_SCHEMA` tests check only 
`ex.getCondition`, not the parameters `validateReservedLevel` builds 
(`tableName`, the dot-qualified `reservedColumnName`, the backtick-quoted 
`columnNames`, and the engine field names). `checkError`, or the 
`checkErrorInPipelineFailure` pattern the drift tests use, would pin them.
   
   **Verification:**
   - **Regression:** A declared metadata column whose array-of-struct 
sequencing field contains an unknown nested field, or two nested fields 
differing only in case, is rejected with the matching nested-field error, and 
the reported reserved column path names the sequencing field.
   - **Regression:** A declared reserved metadata column carrying a COMMENT 
keeps that comment in the created target and in the schema a same-graph 
consumer is planned against, while still taking the engine's type and 
nullability.
   - **Regression:** A declared metadata column that lists only some engine 
nested fields materializes with all engine nested fields, and the AUTO CDC 
MERGE writes rows successfully.
   - **Behavior:** Each reserved-schema rejection reports the expected message 
parameters, including the table name, the offending column or nested field 
names, and the dot-qualified reserved column path.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to