Skip to content

Commit a80dfae

Browse files
committed
Refactor DeltaSourceMetadataEvolutionSupport to be reuseable in v2
1 parent dd2dc63 commit a80dfae

2 files changed

Lines changed: 170 additions & 86 deletions

File tree

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

Lines changed: 65 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,31 @@ 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 raw schema and configuration inputs (no v1 Metadata dependency) so it can be
431+
* reused by both v1 and v2 connectors.
432+
*
433+
* @return (upgradedSchema, maxColumnId) - the schema with IDs/physical names assigned,
434+
* and the final max column ID.
435435
*/
436-
def assignColumnIdAndPhysicalName(
437-
newMetadata: Metadata,
438-
oldMetadata: Metadata,
436+
private[delta] def assignColumnIdAndPhysicalNameToSchema(
437+
newSchema: StructType,
438+
oldSchema: StructType,
439+
newConfiguration: Map[String, String],
440+
oldConfiguration: Map[String, String],
439441
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, _) => {
442+
isOverwritingSchema: Boolean): (StructType, Long) = {
443+
var maxId = DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMap(newConfiguration) max
444+
DeltaConfigs.COLUMN_MAPPING_MAX_ID.fromMap(oldConfiguration) max
445+
findMaxColumnId(newSchema)
446+
val resultSchema =
447+
SchemaMergingUtils.transformColumns(newSchema)((path, field, _) => {
448448
val builder = new MetadataBuilder().withMetadata(field.metadata)
449449

450450
lazy val fullName = path :+ field.name
451451
lazy val existingFieldOpt =
452452
SchemaUtils.findNestedFieldIgnoreCase(
453-
oldMetadata.schema, fullName, includeCollections = true)
453+
oldSchema, fullName, includeCollections = true)
454454
lazy val canReuseColumnMappingMetadataDuringOverwrite = {
455455
val canReuse =
456456
isOverwritingSchema &&
@@ -484,12 +484,12 @@ trait DeltaColumnMappingBase extends DeltaLogging {
484484
if (!hasPhysicalName(field)) {
485485
val physicalName = if (isChangingModeOnExistingTable) {
486486
if (existingFieldOpt.isEmpty) {
487-
if (oldMetadata.schema.isEmpty) {
487+
if (oldSchema.isEmpty) {
488488
// We should relax the check for tables that have both an empty schema
489489
// and no data. Assumption: no schema => no data
490490
generatePhysicalName
491491
} else throw DeltaErrors.schemaChangeDuringMappingModeChangeNotSupported(
492-
oldMetadata.schema, newMetadata.schema)
492+
oldSchema, newSchema)
493493
} else {
494494
// When changing from NoMapping to NameMapping mode, we directly use old display names
495495
// as physical names. This is by design: 1) We don't need to rewrite the
@@ -509,13 +509,32 @@ trait DeltaColumnMappingBase extends DeltaLogging {
509509
}
510510
field.copy(metadata = builder.build())
511511
})
512-
513512
// 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)
513+
if (IcebergCompat.anyEnabled(newConfiguration).exists(_.version >= 2)) {
514+
rewriteFieldIdsForIceberg(resultSchema, maxId)
516515
} else {
517-
(newSchema, maxId)
516+
(resultSchema, maxId)
518517
}
518+
}
519+
520+
/**
521+
* For each column/field in a Metadata's schema, assign id using the current maximum id
522+
* as the basis and increment from there, and assign physical name using UUID
523+
* @param newMetadata The new metadata to assign Ids and physical names
524+
* @param oldMetadata The old metadata
525+
* @param isChangingModeOnExistingTable whether this is part of a commit that changes the
526+
* mapping mode on a existing table
527+
* @return new metadata with Ids and physical names assigned
528+
*/
529+
def assignColumnIdAndPhysicalName(
530+
newMetadata: Metadata,
531+
oldMetadata: Metadata,
532+
isChangingModeOnExistingTable: Boolean,
533+
isOverwritingSchema: Boolean): Metadata = {
534+
val (finalSchema, newMaxId) = assignColumnIdAndPhysicalNameToSchema(
535+
newMetadata.schema, oldMetadata.schema,
536+
newMetadata.configuration, oldMetadata.configuration,
537+
isChangingModeOnExistingTable, isOverwritingSchema)
519538

520539
newMetadata.copy(
521540
schemaString = finalSchema.json,
@@ -775,10 +794,12 @@ trait DeltaColumnMappingBase extends DeltaLogging {
775794
* As of now, `newMetadata` is column mapping read compatible with `oldMetadata` if
776795
* no rename column or drop column has happened in-between.
777796
*/
778-
def hasNoColumnMappingSchemaChanges(newMetadata: Metadata, oldMetadata: Metadata,
797+
def hasNoColumnMappingSchemaChanges(
798+
newMetadata: AbstractMetadata,
799+
oldMetadata: AbstractMetadata,
779800
allowUnsafeReadOnPartitionChanges: Boolean = false): Boolean = {
780-
def hasColMappingOrPartitionSchemaChangeByMetadata(newMetadata: Metadata,
781-
oldMetadata: Metadata): Boolean = {
801+
def hasColMappingOrPartitionSchemaChangeByMetadata(
802+
newMetadata: AbstractMetadata, oldMetadata: AbstractMetadata): Boolean = {
782803
val isBothColumnMappingEnabled =
783804
newMetadata.columnMappingMode != NoMapping && oldMetadata.columnMappingMode != NoMapping
784805
hasColMappingOrPartitionSchemaChange(
@@ -802,15 +823,22 @@ trait DeltaColumnMappingBase extends DeltaLogging {
802823
// the new metadata, as the upgrade would use the logical name as the physical name, we could
803824
// easily capture any difference in the schema using the same is{Drop,Rename}ColumnOperation
804825
// 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
826+
val (upgradedSchema, _) = assignColumnIdAndPhysicalNameToSchema(
827+
oldMetadata.schema, oldMetadata.schema,
828+
oldMetadata.configuration, oldMetadata.configuration,
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+
val columnMappingMode: DeltaColumnMappingMode = newMetadata.columnMappingMode
841+
}
814842
!hasColMappingOrPartitionSchemaChangeByMetadata(newMetadata, upgradedMetadata)
815843
} else {
816844
// Prohibit reading across a downgrade.

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

Lines changed: 105 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,22 @@ 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+
readSchemaAtSourceInit,
128+
readPartitionSchemaAtSourceInit,
129+
readConfigurationsAtSourceInit,
130+
spark)
154131
}
155132

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-
167133
/**
168134
* If the current stream metadata is not equal to the metadata change in [[metadataChangeOpt]],
169135
* return a metadata change barrier [[IndexedFile]].
@@ -685,6 +651,96 @@ object DeltaSourceMetadataEvolutionSupport extends Logging {
685651
spark.sessionState.conf.getConf(
686652
DeltaSQLConf.DELTA_TYPE_WIDENING_BYPASS_STREAMING_TYPE_CHANGE_CHECK)
687653

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

0 commit comments

Comments
 (0)