-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[Bug] Fix Iceberg metadata unreadable by Snowflake - add Avro schema/partition-spec metadata to manifest files #9497
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 |
|---|---|---|
|
|
@@ -72,6 +72,7 @@ | |
| import org.apache.paimon.types.RowType; | ||
| import org.apache.paimon.utils.DataFilePathFactories; | ||
| import org.apache.paimon.utils.FileStorePathFactory; | ||
| import org.apache.paimon.utils.JsonSerdeUtil; | ||
| import org.apache.paimon.utils.ManifestReadThreadPool; | ||
| import org.apache.paimon.utils.Pair; | ||
| import org.apache.paimon.utils.Preconditions; | ||
|
|
@@ -189,8 +190,6 @@ public IcebergCommitCallback(FileStoreTable table, String commitUser) { | |
| metadataCommitterFactory == null ? null : metadataCommitterFactory.create(table); | ||
|
|
||
| this.fileStorePathFactory = table.store().pathFactory(); | ||
| this.manifestFile = IcebergManifestFile.create(table, pathFactory); | ||
| this.manifestList = IcebergManifestList.create(table, pathFactory); | ||
|
|
||
| this.formatVersion = | ||
| table.coreOptions().toConfiguration().get(IcebergOptions.FORMAT_VERSION); | ||
|
|
@@ -200,6 +199,25 @@ public IcebergCommitCallback(FileStoreTable table, String commitUser) { | |
| "Unsupported iceberg format version! Only version 2 or version 3 is valid, but current version is ", | ||
| formatVersion); | ||
|
|
||
| // Compute Iceberg schema and partition spec for Avro manifest metadata. | ||
| // Snowflake and other Iceberg readers require these in the manifest file header. | ||
| // Iceberg field IDs must be positive. Paimon column IDs start at 0, which Iceberg | ||
| // readers reject (see #9012), so remap top-level fields to start from 1. Nested type | ||
| // IDs are left as-is for a follow-up. | ||
| IcebergSchema icebergSchema = withPositiveFieldIds(IcebergSchema.create(table.schema())); | ||
| List<IcebergPartitionField> partitionFields = | ||
| getPartitionFields(table.schema().partitionKeys(), icebergSchema); | ||
| Map<String, String> avroMetadata = new HashMap<>(); | ||
| avroMetadata.put("schema", icebergSchema.toJson()); | ||
| // Iceberg manifest "partition-spec" metadata is the JSON array of partition fields, | ||
| // not the whole spec object (PartitionSpecParser.toJsonFields semantics). | ||
| avroMetadata.put("partition-spec", JsonSerdeUtil.toJson(partitionFields)); | ||
| avroMetadata.put("partition-spec-id", String.valueOf(IcebergPartitionSpec.SPEC_ID)); | ||
| avroMetadata.put("format-version", String.valueOf(formatVersion)); | ||
| this.manifestFile = IcebergManifestFile.create(table, pathFactory, avroMetadata); | ||
|
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. [P2] This only adds metadata to manifests created after the upgrade. |
||
|
|
||
| this.manifestList = IcebergManifestList.create(table, pathFactory); | ||
|
|
||
| this.indexFileHandler = table.store().newIndexFileHandler(); | ||
| this.needAddDvToIceberg = needAddDvToIceberg(); | ||
| } | ||
|
|
@@ -735,6 +753,28 @@ private List<IcebergPartitionField> getPartitionFields( | |
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Rebuilds the schema with positive field IDs starting from 1 for the Avro manifest header. See | ||
| * PR #9497 review: `Schema.Builder` starts Paimon column IDs at 0 and Iceberg readers like | ||
| * Snowflake reject them. | ||
| */ | ||
| private static IcebergSchema withPositiveFieldIds(IcebergSchema schema) { | ||
| int[] nextId = {1}; | ||
| List<IcebergDataField> fields = | ||
| schema.fields().stream() | ||
| .map( | ||
| field -> | ||
| new IcebergDataField( | ||
| nextId[0]++, | ||
| field.name(), | ||
| field.required(), | ||
| field.type(), | ||
| field.dataType(), | ||
| field.doc())) | ||
| .collect(Collectors.toList()); | ||
| return new IcebergSchema(schema.schemaId(), fields); | ||
| } | ||
|
|
||
| /** VARIANT is an Iceberg format-version-3 type; reject publishing it into v2 metadata. */ | ||
| static void checkVariantNotPublishable(RowType rowType) { | ||
| Collection<String> variantFields = new LinkedHashSet<>(); | ||
|
|
||
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.
[P1] Iceberg v2/v3 manifests require a
contentheader whose value isdataordeletes, but this map omits it; the generated manifest hascontent = null. A single constructor-level value would also be insufficient because thisIcebergManifestFilewrites bothContent.DATAandContent.DELETES, selected only byrollingWrite. Please build the metadata per writer from itsContent(or use separate writer factories), and test both data and delete manifests.