Skip to content

Commit ed92a0f

Browse files
committed
Refactor DeltaSourceMetadataEvolutionSupport to be reuseable in v2
1 parent 9e95a5e commit ed92a0f

5 files changed

Lines changed: 295 additions & 94 deletions

File tree

spark/src/main/scala/org/apache/spark/sql/delta/DeltaColumnMapping.scala

Lines changed: 66 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.apache.spark.sql.delta.actions.{Metadata, Protocol}
2525
import org.apache.spark.sql.delta.commands.cdc.CDCReader
2626
import org.apache.spark.sql.delta.metering.DeltaLogging
2727
import org.apache.spark.sql.delta.schema.{SchemaMergingUtils, SchemaUtils}
28+
import org.apache.spark.sql.delta.v2.interop.AbstractMetadata
2829
import org.apache.spark.sql.delta.sources.DeltaSQLConf
2930
import org.json4s.DefaultFormats
3031
import org.json4s.jackson.JsonMethods._
@@ -425,32 +426,34 @@ trait DeltaColumnMappingBase extends DeltaLogging {
425426
}
426427

427428
/**
428-
* For each column/field in a Metadata's schema, assign id using the current maximum id
429-
* as the basis and increment from there, and assign physical name using UUID
430-
* @param newMetadata The new metadata to assign Ids and physical names
431-
* @param oldMetadata The old metadata
432-
* @param isChangingModeOnExistingTable whether this is part of a commit that changes the
433-
* mapping mode on a existing table
434-
* @return new metadata with Ids and physical names assigned
429+
* Core logic for assigning column IDs and physical names to a schema.
430+
* Takes [[AbstractMetadata]] (no v1 Metadata dependency) so it can be reused by both v1
431+
* and v2 connectors. Bundling schema + configuration on each side avoids the swap footgun
432+
* of having two `StructType` and two `Map` parameters next to each other.
433+
*
434+
* @return (upgradedSchema, maxColumnId) - the schema with IDs/physical names assigned,
435+
* and the final max column ID.
435436
*/
436-
def assignColumnIdAndPhysicalName(
437-
newMetadata: Metadata,
438-
oldMetadata: Metadata,
437+
private[delta] def assignColumnIdAndPhysicalNameToSchema(
438+
newMetadata: AbstractMetadata,
439+
oldMetadata: AbstractMetadata,
439440
isChangingModeOnExistingTable: Boolean,
440-
isOverwritingSchema: Boolean): Metadata = {
441-
val rawSchema = newMetadata.schema
442-
var maxId = DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMetaData(newMetadata) max
443-
DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMetaData(oldMetadata) max
444-
findMaxColumnId(rawSchema)
445-
val startId = maxId
446-
val newSchema =
447-
SchemaMergingUtils.transformColumns(rawSchema)((path, field, _) => {
441+
isOverwritingSchema: Boolean): (StructType, Long) = {
442+
val newSchema = newMetadata.schema
443+
val oldSchema = oldMetadata.schema
444+
val newConfiguration = newMetadata.configuration
445+
val oldConfiguration = oldMetadata.configuration
446+
var maxId = DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMap(newConfiguration) max
447+
DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMap(oldConfiguration) max
448+
findMaxColumnId(newSchema)
449+
val resultSchema =
450+
SchemaMergingUtils.transformColumns(newSchema)((path, field, _) => {
448451
val builder = new MetadataBuilder().withMetadata(field.metadata)
449452

450453
lazy val fullName = path :+ field.name
451454
lazy val existingFieldOpt =
452455
SchemaUtils.findNestedFieldIgnoreCase(
453-
oldMetadata.schema, fullName, includeCollections = true)
456+
oldSchema, fullName, includeCollections = true)
454457
lazy val canReuseColumnMappingMetadataDuringOverwrite = {
455458
val canReuse =
456459
isOverwritingSchema &&
@@ -484,12 +487,12 @@ trait DeltaColumnMappingBase extends DeltaLogging {
484487
if (!hasPhysicalName(field)) {
485488
val physicalName = if (isChangingModeOnExistingTable) {
486489
if (existingFieldOpt.isEmpty) {
487-
if (oldMetadata.schema.isEmpty) {
490+
if (oldSchema.isEmpty) {
488491
// We should relax the check for tables that have both an empty schema
489492
// and no data. Assumption: no schema => no data
490493
generatePhysicalName
491494
} else throw DeltaErrors.schemaChangeDuringMappingModeChangeNotSupported(
492-
oldMetadata.schema, newMetadata.schema)
495+
oldSchema, newSchema)
493496
} else {
494497
// When changing from NoMapping to NameMapping mode, we directly use old display names
495498
// as physical names. This is by design: 1) We don't need to rewrite the
@@ -509,13 +512,30 @@ trait DeltaColumnMappingBase extends DeltaLogging {
509512
}
510513
field.copy(metadata = builder.build())
511514
})
512-
513515
// Starting from IcebergCompatV2, we require writing field-id for List/Map nested fields
514-
val (finalSchema, newMaxId) = if (IcebergCompat.isGeqEnabled(newMetadata, 2)) {
515-
rewriteFieldIdsForIceberg(newSchema, maxId)
516+
if (IcebergCompat.anyEnabled(newConfiguration).exists(_.version >= 2)) {
517+
rewriteFieldIdsForIceberg(resultSchema, maxId)
516518
} else {
517-
(newSchema, maxId)
519+
(resultSchema, maxId)
518520
}
521+
}
522+
523+
/**
524+
* For each column/field in a Metadata's schema, assign id using the current maximum id
525+
* as the basis and increment from there, and assign physical name using UUID
526+
* @param newMetadata The new metadata to assign Ids and physical names
527+
* @param oldMetadata The old metadata
528+
* @param isChangingModeOnExistingTable whether this is part of a commit that changes the
529+
* mapping mode on a existing table
530+
* @return new metadata with Ids and physical names assigned
531+
*/
532+
def assignColumnIdAndPhysicalName(
533+
newMetadata: Metadata,
534+
oldMetadata: Metadata,
535+
isChangingModeOnExistingTable: Boolean,
536+
isOverwritingSchema: Boolean): Metadata = {
537+
val (finalSchema, newMaxId) = assignColumnIdAndPhysicalNameToSchema(
538+
newMetadata, oldMetadata, isChangingModeOnExistingTable, isOverwritingSchema)
519539

520540
newMetadata.copy(
521541
schemaString = finalSchema.json,
@@ -775,10 +795,12 @@ trait DeltaColumnMappingBase extends DeltaLogging {
775795
* As of now, `newMetadata` is column mapping read compatible with `oldMetadata` if
776796
* no rename column or drop column has happened in-between.
777797
*/
778-
def hasNoColumnMappingSchemaChanges(newMetadata: Metadata, oldMetadata: Metadata,
798+
def hasNoColumnMappingSchemaChanges(
799+
newMetadata: AbstractMetadata,
800+
oldMetadata: AbstractMetadata,
779801
allowUnsafeReadOnPartitionChanges: Boolean = false): Boolean = {
780-
def hasColMappingOrPartitionSchemaChangeByMetadata(newMetadata: Metadata,
781-
oldMetadata: Metadata): Boolean = {
802+
def hasColMappingOrPartitionSchemaChangeByMetadata(
803+
newMetadata: AbstractMetadata, oldMetadata: AbstractMetadata): Boolean = {
782804
val isBothColumnMappingEnabled =
783805
newMetadata.columnMappingMode != NoMapping && oldMetadata.columnMappingMode != NoMapping
784806
hasColMappingOrPartitionSchemaChange(
@@ -802,15 +824,22 @@ trait DeltaColumnMappingBase extends DeltaLogging {
802824
// the new metadata, as the upgrade would use the logical name as the physical name, we could
803825
// easily capture any difference in the schema using the same is{Drop,Rename}ColumnOperation
804826
// utils.
805-
var upgradedMetadata = assignColumnIdAndPhysicalName(
806-
oldMetadata, oldMetadata, isChangingModeOnExistingTable = true, isOverwritingSchema = false
807-
)
808-
// need to change to a column mapping mode too so the utils below can recognize
809-
upgradedMetadata = upgradedMetadata.copy(
810-
configuration = upgradedMetadata.configuration ++
811-
Map(DeltaConfigs.COLUMN_MAPPING_MODE.key -> newMetadata.columnMappingMode.name)
812-
)
813-
// use the same check
827+
val (upgradedSchema, upgradedMaxId) = assignColumnIdAndPhysicalNameToSchema(
828+
newMetadata = oldMetadata, oldMetadata = oldMetadata,
829+
isChangingModeOnExistingTable = true, isOverwritingSchema = false)
830+
// Construct an AbstractMetadata with the upgraded schema and the new column mapping mode
831+
// so the comparison utils below can recognize column mapping metadata.
832+
val upgradedMetadata = new AbstractMetadata {
833+
val id: String = oldMetadata.id
834+
val name: String = oldMetadata.name
835+
val description: String = oldMetadata.description
836+
val schema: StructType = upgradedSchema
837+
val partitionColumns: Seq[String] = oldMetadata.partitionColumns
838+
val configuration: Map[String, String] = oldMetadata.configuration +
839+
(DeltaConfigs.COLUMN_MAPPING_MODE.key -> newMetadata.columnMappingMode.name,
840+
DeltaConfigs.COLUMN_MAPPING_MAX_ID.key -> upgradedMaxId.toString)
841+
val columnMappingMode: DeltaColumnMappingMode = newMetadata.columnMappingMode
842+
}
814843
!hasColMappingOrPartitionSchemaChangeByMetadata(newMetadata, upgradedMetadata)
815844
} else {
816845
// Prohibit reading across a downgrade.

spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSource.scala

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,8 @@ trait DeltaSourceBase extends Source
143143
*/
144144
protected val readSchemaAtSourceInit: StructType = readSnapshotDescriptor.metadata.schema
145145

146-
protected val readPartitionSchemaAtSourceInit: StructType =
147-
readSnapshotDescriptor.metadata.partitionSchema
148-
149146
protected val readProtocolAtSourceInit: Protocol = readSnapshotDescriptor.protocol
150147

151-
protected val readConfigurationsAtSourceInit: Map[String, String] =
152-
readSnapshotDescriptor.metadata.configuration
153-
154148
/**
155149
* Create a snapshot descriptor, customizing its metadata using metadata tracking if necessary
156150
*/

spark/src/main/scala/org/apache/spark/sql/delta/sources/DeltaSourceMetadataEvolutionSupport.scala

Lines changed: 97 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import org.apache.spark.sql.delta.actions.{Action, FileAction, Metadata, Protoco
2525
import org.apache.spark.sql.delta.schema.SchemaUtils
2626
import org.apache.spark.sql.delta.storage.ClosableIterator
2727
import org.apache.spark.sql.delta.storage.ClosableIterator._
28+
import org.apache.spark.sql.delta.v2.interop.{AbstractMetadata, AbstractProtocol}
2829

2930
import org.apache.spark.internal.Logging
3031
import org.apache.spark.sql.SparkSession
@@ -89,26 +90,13 @@ import org.apache.spark.sql.types.StructType
8990
*/
9091
trait DeltaSourceMetadataEvolutionSupport extends DeltaSourceBase { base: DeltaSource =>
9192

92-
/**
93-
* Whether this DeltaSource is utilizing a schema log entry as its read schema.
94-
*
95-
* If user explicitly turn on the flag to fall back to using latest schema to read (i.e. the
96-
* legacy mode), we will ignore the schema log.
97-
*/
9893
protected def trackingMetadataChange: Boolean =
99-
!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges &&
100-
metadataTrackingLog.flatMap(_.getCurrentTrackedMetadata).nonEmpty
94+
DeltaSourceMetadataEvolutionSupport.shouldTrackMetadataChange(
95+
schemaReadOptions, metadataTrackingLog)
10196

102-
/**
103-
* Whether a schema tracking log is provided (and is empty), so we could initialize eagerly.
104-
* This should only be used for the first write to the schema log, after then, schema tracking
105-
* should not rely on this state any more.
106-
*/
10797
protected def readyToInitializeMetadataTrackingEagerly: Boolean =
108-
!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges &&
109-
metadataTrackingLog.exists { log =>
110-
log.getCurrentTrackedMetadata.isEmpty && log.initMetadataLogEagerly
111-
}
98+
DeltaSourceMetadataEvolutionSupport.shouldInitializeMetadataTrackingEagerly(
99+
schemaReadOptions, metadataTrackingLog)
112100

113101

114102
/**
@@ -126,44 +114,20 @@ trait DeltaSourceMetadataEvolutionSupport extends DeltaSourceBase { base: DeltaS
126114
}
127115
}
128116

129-
/**
130-
* Check the table metadata or protocol changed since the initial read snapshot. We make sure:
131-
* 1. The schema is the same, except for internal metadata, AND
132-
* 2. The delta related table configurations are strictly equal, AND
133-
* 3. The incoming metadata change should not be considered a failure-causing change if we have
134-
* marked the persisted schema and the stream progress is behind that schema version.
135-
* This could happen when we've already merged consecutive schema changes during the analysis
136-
* phase and we are using the merged schema as the read schema. All the schema changes in
137-
* between can be safely ignored because they won't contribute any data.
138-
*/
139117
private def hasMetadataOrProtocolChangeComparedToStreamMetadata(
140118
metadataChangeOpt: Option[Metadata],
141119
protocolChangeOpt: Option[Protocol],
142120
newSchemaVersion: Long): Boolean = {
143-
if (persistedMetadataAtSourceInit.exists(_.deltaCommitVersion >= newSchemaVersion)) {
144-
false
145-
} else {
146-
protocolChangeOpt.exists(_ != readProtocolAtSourceInit) ||
147-
metadataChangeOpt.exists { newMetadata =>
148-
hasSchemaChangeComparedToStreamMetadata(newMetadata.schema) ||
149-
newMetadata.partitionSchema != readPartitionSchemaAtSourceInit ||
150-
newMetadata.configuration.filterKeys(_.startsWith("delta.")).toMap !=
151-
readConfigurationsAtSourceInit.filterKeys(_.startsWith("delta.")).toMap
152-
}
153-
}
121+
DeltaSourceMetadataEvolutionSupport.hasMetadataOrProtocolChangeComparedToStreamMetadata(
122+
metadataChangeOpt,
123+
protocolChangeOpt,
124+
newSchemaVersion,
125+
persistedMetadataAtSourceInit,
126+
readProtocolAtSourceInit,
127+
readSnapshotDescriptor.metadata,
128+
spark)
154129
}
155130

156-
/**
157-
* Check that the give schema is the same as the schema from the initial read snapshot.
158-
*/
159-
private def hasSchemaChangeComparedToStreamMetadata(newSchema: StructType): Boolean =
160-
if (spark.conf.get(DeltaSQLConf.DELTA_STREAMING_IGNORE_INTERNAL_METADATA_FOR_SCHEMA_CHANGE)) {
161-
DeltaTableUtils.removeInternalWriterMetadata(spark, newSchema) !=
162-
DeltaTableUtils.removeInternalWriterMetadata(spark, readSchemaAtSourceInit)
163-
} else {
164-
newSchema != readSchemaAtSourceInit
165-
}
166-
167131
/**
168132
* If the current stream metadata is not equal to the metadata change in [[metadataChangeOpt]],
169133
* return a metadata change barrier [[IndexedFile]].
@@ -685,6 +649,90 @@ object DeltaSourceMetadataEvolutionSupport extends Logging {
685649
spark.sessionState.conf.getConf(
686650
DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK)
687651

652+
/**
653+
* Whether this DeltaSource is utilizing a schema log entry as its read schema.
654+
*
655+
* If user explicitly turn on the flag to fall back to using latest schema to read (i.e. the
656+
* legacy mode), we will ignore the schema log.
657+
*/
658+
def shouldTrackMetadataChange(
659+
schemaReadOptions: DeltaStreamUtils.SchemaReadOptions,
660+
metadataTrackingLog: Option[DeltaSourceMetadataTrackingLog]): Boolean = {
661+
!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges &&
662+
metadataTrackingLog.flatMap(_.getCurrentTrackedMetadata).nonEmpty
663+
}
664+
665+
/**
666+
* Whether a schema tracking log is provided (and is empty), so we could initialize eagerly.
667+
* This should only be used for the first write to the schema log, after then, schema tracking
668+
* should not rely on this state any more.
669+
*/
670+
def shouldInitializeMetadataTrackingEagerly(
671+
schemaReadOptions: DeltaStreamUtils.SchemaReadOptions,
672+
metadataTrackingLog: Option[DeltaSourceMetadataTrackingLog]): Boolean = {
673+
!schemaReadOptions.allowUnsafeStreamingReadOnColumnMappingSchemaChanges &&
674+
metadataTrackingLog.exists { log =>
675+
log.getCurrentTrackedMetadata.isEmpty && log.initMetadataLogEagerly
676+
}
677+
}
678+
679+
/**
680+
* Check the table metadata or protocol changed since the initial read snapshot. We make sure:
681+
* 1. The schema is the same, except for internal metadata, AND
682+
* 2. The delta related table configurations are strictly equal, AND
683+
* 3. The incoming metadata change should not be considered a failure-causing change if we have
684+
* marked the persisted schema and the stream progress is behind that schema version.
685+
* This could happen when we've already merged consecutive schema changes during the analysis
686+
* phase and we are using the merged schema as the read schema. All the schema changes in
687+
* between can be safely ignored because they won't contribute any data.
688+
*
689+
* @param metadataChangeOpt New metadata action, if any.
690+
* @param protocolChangeOpt New protocol action, if any.
691+
* @param newSchemaVersion The version of the incoming change.
692+
* @param persistedMetadataAtSourceInit The persisted metadata at source init, if any.
693+
* @param readProtocolAtSourceInit The protocol at source init.
694+
* @param readMetadataAtSourceInit The metadata at source init (schema, partition schema, and
695+
* configuration). Bundled to avoid the swap footgun of three
696+
* adjacent params.
697+
* @param spark The SparkSession (used for SQL conf checks).
698+
*/
699+
def hasMetadataOrProtocolChangeComparedToStreamMetadata(
700+
metadataChangeOpt: Option[AbstractMetadata],
701+
protocolChangeOpt: Option[AbstractProtocol],
702+
newSchemaVersion: Long,
703+
persistedMetadataAtSourceInit: Option[PersistedMetadata],
704+
readProtocolAtSourceInit: AbstractProtocol,
705+
readMetadataAtSourceInit: AbstractMetadata,
706+
spark: SparkSession): Boolean = {
707+
if (persistedMetadataAtSourceInit.exists(_.deltaCommitVersion >= newSchemaVersion)) {
708+
false
709+
} else {
710+
protocolChangeOpt.exists(p => !p.equalsByFields(readProtocolAtSourceInit)) ||
711+
metadataChangeOpt.exists { newMetadata =>
712+
hasSchemaChangeComparedToStreamMetadata(
713+
newMetadata.schema, readMetadataAtSourceInit.schema, spark) ||
714+
newMetadata.partitionSchema != readMetadataAtSourceInit.partitionSchema ||
715+
newMetadata.configuration.filterKeys(_.startsWith("delta.")).toMap !=
716+
readMetadataAtSourceInit.configuration.filterKeys(_.startsWith("delta.")).toMap
717+
}
718+
}
719+
}
720+
721+
/**
722+
* Check that the given schema is the same as the schema from the initial read snapshot.
723+
*/
724+
private def hasSchemaChangeComparedToStreamMetadata(
725+
newSchema: StructType,
726+
readSchemaAtSourceInit: StructType,
727+
spark: SparkSession): Boolean = {
728+
if (spark.conf.get(DeltaSQLConf.DELTA_STREAMING_IGNORE_INTERNAL_METADATA_FOR_SCHEMA_CHANGE)) {
729+
DeltaTableUtils.removeInternalWriterMetadata(spark, newSchema) !=
730+
DeltaTableUtils.removeInternalWriterMetadata(spark, readSchemaAtSourceInit)
731+
} else {
732+
newSchema != readSchemaAtSourceInit
733+
}
734+
}
735+
688736
/**
689737
* Speculate ahead and find the next merged consecutive metadata change if possible.
690738
* A metadata change is either:

0 commit comments

Comments
 (0)