-
Notifications
You must be signed in to change notification settings - Fork 29.4k
[SPARK-59014][SQL] Reject a data column that hides a captured metadata column during DSv2 refresh validation #58295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,8 @@ import java.util.Locale | |
|
|
||
| import org.apache.spark.sql.catalyst.SQLConfHelper | ||
| import org.apache.spark.sql.catalyst.analysis.Resolver | ||
| import org.apache.spark.sql.catalyst.util.{quoteIfNeeded, MetadataColumnHelper} | ||
| import org.apache.spark.sql.catalyst.expressions.MetadataAttributeWithLogicalName | ||
| import org.apache.spark.sql.catalyst.util.{quoteIdentifier, quoteIfNeeded} | ||
| import org.apache.spark.sql.connector.catalog.CatalogV2Implicits.IdentifierHelper | ||
| import org.apache.spark.sql.errors.QueryCompilationErrors | ||
| import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation | ||
|
|
@@ -100,7 +101,9 @@ private[sql] object V2TableUtil extends SQLConfHelper { | |
| * @return metadata columns captured by the relation | ||
| */ | ||
| def extractMetadataColumns(relation: DataSourceV2Relation): Seq[MetadataColumn] = { | ||
| val metaAttrNames = relation.output.filter(_.isMetadataCol).map(_.name) | ||
| val metaAttrNames = relation.output.collect { | ||
| case MetadataAttributeWithLogicalName(_, logicalName) => logicalName | ||
| } | ||
| if (metaAttrNames.isEmpty) Nil else filter(metaAttrNames, metadataColumns(relation.table)) | ||
| } | ||
|
|
||
|
|
@@ -111,6 +114,8 @@ private[sql] object V2TableUtil extends SQLConfHelper { | |
| * - Column ID changes (top-level and nested field IDs) | ||
| * - Metadata column type or nullability changes | ||
| * - Removed metadata columns (missing from current table) | ||
| * - Metadata columns hidden by a same-named data column added after capture (when the connector | ||
| * suppresses rather than renames the conflict) | ||
| * | ||
| * @param table the current table metadata | ||
| * @param originMetaCols the originally captured metadata columns | ||
|
|
@@ -130,7 +135,41 @@ private[sql] object V2TableUtil extends SQLConfHelper { | |
| val originMetaSchema = CatalogV2Util.toStructType(originMetaCols) | ||
| val metaCols = filter(originMetaColNames, metadataColumns(table)) | ||
| val metaSchema = CatalogV2Util.toStructType(metaCols) | ||
| SchemaUtils.validateSchemaCompatibility(originMetaSchema, metaSchema, resolver, mode, checkIds) | ||
| val schemaErrors = SchemaUtils.validateSchemaCompatibility( | ||
| originMetaSchema, metaSchema, resolver, mode, checkIds) | ||
| schemaErrors ++ shadowedMetadataColumnErrors(table, metaCols) | ||
| } | ||
|
|
||
| /** | ||
| * Reports captured metadata columns that a data column of the same name now hides. | ||
| * | ||
| * When a data column takes a metadata column's name, a connector that does not rename the | ||
| * conflict (`canRenameConflictingMetadataColumns` is false) suppresses the metadata column via | ||
| * `metadataOutputWithOutConflicts`. A suppressed metadata column can no longer be resolved, so a | ||
| * captured reference to it is broken, and on a partially pruned scan it silently reads the data | ||
| * column's values instead. The `SupportsMetadataColumns` contract advises a non-renaming source | ||
| * to reject such a data-column name but does not enforce it, so this reports the conflict | ||
| * rather than leaving it silent. | ||
| */ | ||
| private def shadowedMetadataColumnErrors( | ||
| table: Table, | ||
| reportedMetaCols: Seq[MetadataColumn]): Seq[String] = { | ||
| if (reportedMetaCols.isEmpty || renamesConflictingMetadataColumns(table)) { | ||
| Nil | ||
| } else { | ||
| val dataColNames = table.columns.iterator.map(c => normalize(c.name)).toSet | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocking (P1): The planner decides metadata/data conflicts with the configured SQL resolver, but this check uses |
||
| reportedMetaCols | ||
| .filter(metaCol => dataColNames.contains(normalize(metaCol.name))) | ||
| .map { metaCol => | ||
| s"${quoteIdentifier(metaCol.name)} metadata column is hidden by a data column " + | ||
| "with the same name" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private def renamesConflictingMetadataColumns(table: Table): Boolean = table match { | ||
| case hasMeta: SupportsMetadataColumns => hasMeta.canRenameConflictingMetadataColumns | ||
| case _ => false | ||
| } | ||
|
|
||
| private def filter(colNames: Seq[String], cols: Seq[MetadataColumn]): Seq[MetadataColumn] = { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit (P3): This explanation says the already-captured metadata attribute is suppressed and unresolvable, but
metadataOutputWithOutConflictsreturns metadata attributes already present inrelation.output, and refresh preserves that output viar.copy(table = currentTable). The wrong-value path occurs later whenPushDownUtils.toOutputAttrsreconciles the refreshed scan schema to retained output attributes by physical name, which can bind the same-named data field to the metadata attribute. Please reword this comment so it describes that mechanism.