Skip to content

Commit 1a640d1

Browse files
committed
validation
1 parent 5c0c0f8 commit 1a640d1

3 files changed

Lines changed: 339 additions & 15 deletions

File tree

common/utils/src/main/resources/error/error-conditions.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@
203203
],
204204
"sqlState" : "42703"
205205
},
206+
"AUTOCDC_RESERVED_COLUMN_NAME_CONFLICT" : {
207+
"message" : [
208+
"Using <caseSensitivity> column name comparison, the column `<columnName>` in the <schemaName> schema conflicts with the reserved AutoCDC column name `<reservedColumnName>`. Rename or remove the column."
209+
],
210+
"sqlState" : "42710"
211+
},
206212
"AVRO_CANNOT_WRITE_NULL_FIELD" : {
207213
"message" : [
208214
"Cannot write null value for field <name> defined as non-null Avro data type <dataType>.",

sql/pipelines/src/main/scala/org/apache/spark/sql/pipelines/autocdc/Scd1BatchProcessor.scala

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,26 @@
1717

1818
package org.apache.spark.sql.pipelines.autocdc
1919

20-
import org.apache.spark.sql.{functions => F}
20+
import org.apache.spark.SparkException
21+
import org.apache.spark.sql.{functions => F, AnalysisException}
22+
import org.apache.spark.sql.Column
2123
import org.apache.spark.sql.catalyst.util.QuotingUtils
2224
import org.apache.spark.sql.classic.DataFrame
25+
import org.apache.spark.sql.types.{DataType, StructField, StructType}
2326
import org.apache.spark.util.ArrayImplicits._
2427

2528
/**
2629
* Per-microbatch processor for SCD Type 1 AutoCDC flows, complying to the specified [[changeArgs]]
2730
* configuration.
31+
*
32+
* @param changeArgs The CDC flow configuration.
33+
* @param resolvedSequencingType The post-analysis [[DataType]] of the sequencing column, derived
34+
* from the flow's resolved DataFrame at flow setup time.
2835
*/
29-
case class Scd1BatchProcessor(changeArgs: ChangeArgs) {
36+
case class Scd1BatchProcessor(
37+
changeArgs: ChangeArgs,
38+
resolvedSequencingType: DataType) {
39+
3040
/**
3141
* Deduplicate the incoming CDC microbatch by key, keeping the most recent event per key
3242
* as ordered by [[ChangeArgs.sequencing]].
@@ -59,9 +69,102 @@ case class Scd1BatchProcessor(changeArgs: ChangeArgs) {
5969
)
6070
.select(F.col(s"$winningRowCol.*"))
6171
}
72+
73+
/**
74+
* Project the CDC metadata column onto the microbatch.
75+
*/
76+
def extendMicrobatchRowsWithCdcMetadata(microbatchDf: DataFrame): DataFrame = {
77+
// Proactively validate the reserved CDC metadata column does not exist in the microbatch.
78+
validateCdcMetadataColumnNotPresent(microbatchDf)
79+
80+
val rowDeleteSequence: Column = changeArgs.deleteCondition match {
81+
case Some(deleteCondition) =>
82+
F.when(deleteCondition, changeArgs.sequencing).otherwise(F.lit(null))
83+
case None =>
84+
F.lit(null)
85+
}
86+
87+
val rowUpsertSequence: Column =
88+
// A row that is not a delete must be an upsert, these are mutually exclusive and a complete
89+
// set of CDC event types.
90+
F.when(rowDeleteSequence.isNull, changeArgs.sequencing).otherwise(F.lit(null))
91+
92+
microbatchDf.withColumn(
93+
Scd1BatchProcessor.cdcMetadataColName,
94+
Scd1BatchProcessor.constructCdcMetadataCol(
95+
deleteSequence = rowDeleteSequence,
96+
upsertSequence = rowUpsertSequence,
97+
sequencingType = resolvedSequencingType
98+
)
99+
)
100+
}
101+
102+
private def validateCdcMetadataColumnNotPresent(microbatchDf: DataFrame): Unit = {
103+
val ignoreColumnNameCase =
104+
!microbatchDf.sparkSession.sessionState.conf.caseSensitiveAnalysis
105+
106+
microbatchDf.schema.fieldNames
107+
.find { fieldName =>
108+
if (ignoreColumnNameCase) {
109+
fieldName.equalsIgnoreCase(Scd1BatchProcessor.cdcMetadataColName)
110+
} else {
111+
fieldName.equals(Scd1BatchProcessor.cdcMetadataColName)
112+
}
113+
}
114+
.foreach { conflictingColumnName =>
115+
throw new AnalysisException(
116+
errorClass = "AUTOCDC_RESERVED_COLUMN_NAME_CONFLICT",
117+
messageParameters = Map(
118+
"caseSensitivity" -> CaseSensitivityLabels.of(!ignoreColumnNameCase),
119+
"columnName" -> conflictingColumnName,
120+
"schemaName" -> "microbatch",
121+
"reservedColumnName" -> Scd1BatchProcessor.cdcMetadataColName
122+
)
123+
)
124+
}
125+
}
62126
}
63127

64128
object Scd1BatchProcessor {
65129
// Columns prefixed with `__spark_autocdc_` are reserved for internal SDP AutoCDC processing.
66-
private val winningRowColName = "__spark_autocdc_winning_row"
130+
private[autocdc] val winningRowColName: String = "__spark_autocdc_winning_row"
131+
private[autocdc] val cdcMetadataColName: String = "__spark_autocdc_metadata"
132+
133+
private[autocdc] val cdcDeleteSequenceFieldName: String = "deleteSequence"
134+
private[autocdc] val cdcUpsertSequenceFieldName: String = "upsertSequence"
135+
136+
/**
137+
* Schema of the CDC metadata struct column for SCD1.
138+
*/
139+
private def cdcMetadataColSchema(sequencingType: DataType): StructType =
140+
StructType(
141+
Seq(
142+
// The sequencing of the event if it represents a delete, null otherwise.
143+
StructField(cdcDeleteSequenceFieldName, sequencingType, nullable = true),
144+
// The sequencing of the event if it represents an upsert, null otherwise.
145+
StructField(cdcUpsertSequenceFieldName, sequencingType, nullable = true)
146+
)
147+
)
148+
149+
/**
150+
* Construct the CDC metadata struct column for SCD1, following the exact schema and field
151+
* ordering defined by [[cdcMetadataColSchema]].
152+
*/
153+
private[autocdc] def constructCdcMetadataCol(
154+
deleteSequence: Column,
155+
upsertSequence: Column,
156+
sequencingType: DataType): Column = {
157+
val cdcMetadataFieldsInOrder = cdcMetadataColSchema(sequencingType).fields.map { field =>
158+
val value = field.name match {
159+
case `cdcDeleteSequenceFieldName` => deleteSequence
160+
case `cdcUpsertSequenceFieldName` => upsertSequence
161+
case other =>
162+
throw SparkException.internalError(
163+
s"Unable to construct SCD1 CDC metadata column due to unknown `${other}` field."
164+
)
165+
}
166+
value.cast(field.dataType).as(field.name)
167+
}
168+
F.struct(cdcMetadataFieldsInOrder.toImmutableArraySeq: _*)
169+
}
67170
}

0 commit comments

Comments
 (0)