Repository navigation
feat: bump iceberg-rust past apache/iceberg-rust#3255 and narrow the nested schema evolution fallback #7028
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,8 +85,8 @@ aws-sdk-sts = { version = "1.114.0", default-features = false } | |
| # Pinned to a revision rather than a release because no published iceberg-rust | ||
| # version is on our arrow: 0.10.1 requires arrow/parquet ^58. Move to a plain | ||
| # version requirement once a release ships on arrow 59. Bump policy: #5645. | ||
| iceberg = { git = "https://github.com/apache/iceberg-rust", rev = "1f3bc34058e3f3e45336bc5428f37b92649a608b" } | ||
| iceberg-storage-opendal = { git = "https://github.com/apache/iceberg-rust", rev = "1f3bc34058e3f3e45336bc5428f37b92649a608b", features = ["opendal-memory", "opendal-fs", "opendal-s3", "opendal-gcs", "opendal-oss", "opendal-azdls"] } | ||
| iceberg = { git = "https://github.com/apache/iceberg-rust", rev = "b154603a710d730d2f43f8af4db257cc137237ef" } | ||
| iceberg-storage-opendal = { git = "https://github.com/apache/iceberg-rust", rev = "b154603a710d730d2f43f8af4db257cc137237ef", features = ["opendal-memory", "opendal-fs", "opendal-s3", "opendal-gcs", "opendal-oss", "opendal-azdls"] } | ||
|
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. Please preserve
Member
Author
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. This one is real. In the new pin, |
||
| reqsign-core = "3" | ||
|
|
||
| [profile.release] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -743,17 +743,41 @@ case class CometScanRule(session: SparkSession) | |
| false | ||
| } | ||
|
|
||
| // Projected roots are matched by field ID so historical snapshots still identify renamed | ||
| // columns. Both schema checks below use them. | ||
| val projectedDataColumns = scanExec.output.filterNot(_.isMetadataCol) | ||
| val resolver = session.sessionState.conf.resolver | ||
| val projectedFieldIds = projectedDataColumns.map { attr => | ||
| metadata.globalFieldIdMapping.collectFirst { | ||
| case (fieldName, fieldId) if resolver(fieldName, attr.name) => fieldId | ||
| } | ||
| } | ||
| val resolvedProjectedFieldIds = projectedFieldIds.flatten.toSet | ||
| val hasUnresolvedProjectedFieldIds = projectedFieldIds.exists(_.isEmpty) | ||
|
|
||
| // Column default values are a V3 feature, so V1/V2 tables cannot have them and need no | ||
| // check. Only inspect V3 tables. iceberg-rust does not synthesize a V3 initial-default for | ||
| // a column absent from a data file, so a scan projecting such a column must fall back. A V3 | ||
| // table implies an Iceberg version new enough to expose initialDefault(), so a reflection | ||
| // failure here is unexpected and also falls back rather than risk a crash. | ||
| // a column absent from a data file (it fails a nested one), so a scan reading such a | ||
| // column must fall back. That is the query's schema and, while nested schema pruning is | ||
| // off, the whole of each projected column in the task schema, which holds nested fields | ||
| // the query's does not. A V3 table implies an Iceberg version new enough to expose | ||
| // initialDefault(), so a reflection failure here is unexpected and also falls back rather | ||
| // than risk a crash. | ||
| val defaultValuesSupported = | ||
| if (!formatVersion.exists(_ >= 3)) { | ||
| true | ||
| } else { | ||
| try { | ||
| val defaulted = IcebergReflection.columnsWithInitialDefault(metadata.scanSchema) | ||
| val readDefaults = metadata.taskSchema.toSeq.flatMap { schema => | ||
| if (hasUnresolvedProjectedFieldIds) { | ||
| IcebergReflection.columnsWithInitialDefault(schema) | ||
| } else { | ||
| IcebergReflection.columnsWithInitialDefault(schema, resolvedProjectedFieldIds) | ||
| } | ||
| } | ||
| val defaulted = | ||
| (IcebergReflection.columnsWithInitialDefault( | ||
| metadata.scanSchema) ++ readDefaults).distinct | ||
| if (defaulted.nonEmpty) { | ||
| fallbackReasons += "Iceberg column(s) with V3 default values are not yet " + | ||
| s"supported by Comet's native reader: ${defaulted.mkString(", ")}" | ||
|
|
@@ -769,18 +793,6 @@ case class CometScanRule(session: SparkSession) | |
| } | ||
| } | ||
|
|
||
| // Projected roots are matched by field ID so historical snapshots still identify renamed | ||
| // columns. Both schema checks below use them. | ||
| val projectedDataColumns = scanExec.output.filterNot(_.isMetadataCol) | ||
| val resolver = session.sessionState.conf.resolver | ||
| val projectedFieldIds = projectedDataColumns.map { attr => | ||
| metadata.globalFieldIdMapping.collectFirst { | ||
| case (fieldName, fieldId) if resolver(fieldName, attr.name) => fieldId | ||
| } | ||
| } | ||
| val resolvedProjectedFieldIds = projectedFieldIds.flatten.toSet | ||
| val hasUnresolvedProjectedFieldIds = projectedFieldIds.exists(_.isEmpty) | ||
|
|
||
| // iceberg-rust applies name mappings only at the root (#6790). Nested columns can | ||
| // disappear during projection, or reach the adapter with physical field names that | ||
| // cannot distinguish a mapped alias from a genuinely missing field. Schema history | ||
|
|
@@ -831,16 +843,17 @@ case class CometScanRule(session: SparkSession) | |
| false | ||
| } | ||
|
|
||
| // Neither iceberg-rust nor the batch adaptation after it matches a data file's nested | ||
| // fields to the table's by field id (apache/iceberg-rust#2617). A file written before a | ||
| // nested field was added (to a struct, or to a struct inside a list or map) fails the | ||
| // native scan with "Incorrect number of arrays for StructArray fields", and a nested field | ||
| // renamed since the file was written can read back as NULL. This check walks the schema | ||
| // the scan reads (see CometIcebergNativeScanMetadata.taskSchema), which for VERSION AS OF | ||
| // can be older than the table's, and also the table and scan schemas, so it can fall back | ||
| // when the changed field is pruned away. A FileScanTask does not record which schema wrote | ||
| // its file, so fall back when any schema in the table's history lacks a nested field of a | ||
| // projected column or names it differently. | ||
| // iceberg-rust matches a data file's nested fields to the table's by field id, filling in | ||
| // a field the file lacks with NULLs and applying renames, so schema evolution that adds, | ||
| // renames, drops or reorders nested fields reads correctly. One shape still differs from | ||
| // Spark: a struct (at any depth) whose fields in the schema the scan reads were all added | ||
| // after a file was written. The native read returns a struct of NULLs or NULL there, | ||
| // depending on what else the scan reads, where Spark returns NULL for a top-level struct | ||
| // and a struct of NULLs for a nested one. The schema the scan reads is the task schema (see | ||
| // CometIcebergNativeScanMetadata.taskSchema), which holds only the nested fields Spark | ||
| // keeps while nested schema pruning is on, and which for VERSION AS OF can be older than | ||
| // the table's. A FileScanTask does not record which schema wrote its file, so fall back | ||
| // when any schema in the table's history has such a struct in a projected column. | ||
| val nestedFieldsSupported = | ||
| try { | ||
| val schemas = Seq(metadata.tableSchema, metadata.scanSchema) ++ metadata.taskSchema | ||
|
|
@@ -850,18 +863,17 @@ case class CometScanRule(session: SparkSession) | |
| } else { | ||
| resolvedProjectedFieldIds | ||
| } | ||
| val changed = schemas | ||
| .flatMap { schema => | ||
| IcebergReflection.nestedFieldsAddedOrRenamed(metadata.table, schema, fieldIds) | ||
| } | ||
| val disjoint = metadata.taskSchema.toSeq | ||
|
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. Please preserve fallback for evolved ID-less structs: adding
Member
Author
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. You're right that this changes behavior. For files with neither field ids nor a name mapping, iceberg-rust now matches a struct's children by position only when the counts are equal, so a pruned read of such a struct fails with "cannot reconcile struct fields by id". Before the bump it returned the file's values. I don't think we can keep the old fallback for this case. Nothing at planning time tells us whether a file has ids, and the old guard only fired when the schema history showed a nested add or rename, so keeping it would put back most of what this PR removes. Spark reads every nested field of these files as NULL, so the native scan already differs there. That's tracked in #6794 and listed under "Reads that differ from Spark" in iceberg.md, and the upstream side is apache/iceberg-rust#3436. Does it work for you to leave this as a documented divergence? |
||
| .flatMap(IcebergReflection.structsWithNoFieldsInCommon(metadata.table, _, fieldIds)) | ||
|
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. Could default validation use the task schema? With pruning disabled, unprojected nested defaults now reach unsupported native reads.
Member
Author
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. Good catch. With nested pruning off the task schema holds nested fields the query's schema doesn't, and iceberg-rust errors on a nested default the file lacks. The default check now also covers the projected columns of the task schema, with a test, in bc89e2c. |
||
| .distinct | ||
| .sorted | ||
| if (changed.nonEmpty) { | ||
| fallbackReasons += "Nested fields added or renamed by Iceberg schema evolution are " + | ||
| "not yet supported by Comet's native reader, which cannot match them to data " + | ||
| s"files written before the change: ${changed.mkString(", ")}" | ||
| if (disjoint.nonEmpty) { | ||
| fallbackReasons += "Structs whose fields were all added by Iceberg schema " + | ||
| "evolution are not yet supported by Comet's native reader, which reads data " + | ||
| "files written before the change differently from Spark: " + | ||
| disjoint.mkString(", ") | ||
| } | ||
| changed.isEmpty | ||
| disjoint.isEmpty | ||
| } catch { | ||
| case e: Exception => | ||
| fallbackReasons += "Iceberg reflection failure: could not compare nested fields " + | ||
|
|
||
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.
Could equality deletes bind case-sensitively? This pin rejects
id/IDbecause Comet still setscase_sensitive=false.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.
Agreed. Comet's native tasks were built case-insensitive, and the equality-delete predicate is the only thing that reads that flag. Its names come from the task schema, so they match exactly. Tasks are now case-sensitive in bc89e2c, with a test that fails without it.