Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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<>();

Copy link
Copy Markdown
Contributor

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 content header whose value is data or deletes, but this map omits it; the generated manifest has content = null. A single constructor-level value would also be insufficient because this IcebergManifestFile writes both Content.DATA and Content.DELETES, selected only by rollingWrite. Please build the metadata per writer from its Content (or use separate writer factories), and test both data and delete manifests.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This only adds metadata to manifests created after the upgrade. createMetadataWithBase retains baseDataManifestFileMetas for add-only commits and retains existing DV manifests when there is no new index, so an already affected table remains a mixture of new and legacy headerless manifests and Snowflake still has to traverse the legacy files. Please provide a one-time manifest rewrite/migration path (or an explicit operational migration) and add an upgrade test starting from existing manifests.


this.manifestList = IcebergManifestList.create(table, pathFactory);

this.indexFileHandler = table.store().newIndexFileHandler();
this.needAddDvToIceberg = needAddDvToIceberg();
}
Expand Down Expand Up @@ -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<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.paimon.format.FormatWriterFactory;
import org.apache.paimon.format.SimpleColStats;
import org.apache.paimon.format.SimpleStatsCollector;
import org.apache.paimon.format.avro.AvroFileFormat;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.Path;
import org.apache.paimon.iceberg.IcebergOptions;
Expand All @@ -52,8 +53,10 @@

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static org.apache.paimon.iceberg.manifest.IcebergConversions.toByteBuffer;
Expand All @@ -66,7 +69,24 @@ public class IcebergManifestFile extends ObjectsFile<IcebergManifestEntry> {

private static final long UNASSIGNED_SEQ = -1L;

private static final String ROW_NAME_MAPPING =
"org.apache.paimon.avro.generated.record:manifest_entry,"
+ "iceberg:true,"
+ "manifest_entry_data_file:r2,"
+ "r2_partition:r102,"
+ "kv_name_r2_null_value_counts:k121_v122,"
+ "k_id_k121_v122:121,"
+ "v_id_k121_v122:122,"
+ "kv_name_r2_lower_bounds:k126_v127,"
+ "k_id_k126_v127:126,"
+ "v_id_k126_v127:127,"
+ "kv_name_r2_upper_bounds:k129_v130,"
+ "k_id_k129_v130:129,"
+ "v_id_k129_v130:130";

private final Map<Content, FormatWriterFactory> writerFactories;
private final RowType partitionType;
// Default (DATA) writer factory, kept for the base ObjectsFile.
private final FormatWriterFactory writerFactory;
private final MemorySize targetFileSize;

Expand All @@ -75,7 +95,31 @@ public IcebergManifestFile(
RowType partitionType,
boolean withFirstRowId,
FormatReaderFactory readerFactory,
FormatWriterFactory writerFactory,
Map<Content, FormatWriterFactory> writerFactories,
String compression,
PathFactory pathFactory,
MemorySize targetFileSize) {
// Java forbids `this.` assignments before super(); route the DATA factory through
// the parameter list instead.
this(
fileIO,
partitionType,
withFirstRowId,
readerFactory,
writerFactories,
writerFactories.get(Content.DATA),
compression,
pathFactory,
targetFileSize);
}

private IcebergManifestFile(
FileIO fileIO,
RowType partitionType,
boolean withFirstRowId,
FormatReaderFactory readerFactory,
Map<Content, FormatWriterFactory> writerFactories,
FormatWriterFactory defaultWriterFactory,
String compression,
PathFactory pathFactory,
MemorySize targetFileSize) {
Expand All @@ -84,12 +128,13 @@ public IcebergManifestFile(
new IcebergManifestEntrySerializer(partitionType, withFirstRowId),
IcebergManifestEntry.schema(partitionType, withFirstRowId),
readerFactory,
writerFactory,
defaultWriterFactory,
compression,
pathFactory,
null);
this.partitionType = partitionType;
this.writerFactory = writerFactory;
this.writerFactories = writerFactories;
this.writerFactory = defaultWriterFactory;
this.targetFileSize = targetFileSize;
}

Expand All @@ -99,34 +144,43 @@ public String compression() {
}

public static IcebergManifestFile create(FileStoreTable table, IcebergPathFactory pathFactory) {
return create(table, pathFactory, new HashMap<>());
}

public static IcebergManifestFile create(
FileStoreTable table,
IcebergPathFactory pathFactory,
Map<String, String> avroMetadata) {
RowType partitionType = table.schema().logicalPartitionType();
Options avroOptions = Options.fromMap(table.options());
boolean withFirstRowId =
avroOptions.get(IcebergOptions.FORMAT_VERSION) >= IcebergMetadata.FORMAT_VERSION_V3;
RowType entryType = IcebergManifestEntry.schema(partitionType, withFirstRowId);
// https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/ManifestReader.java
avroOptions.set(
"avro.row-name-mapping",
"org.apache.paimon.avro.generated.record:manifest_entry,"
+ "iceberg:true,"
+ "manifest_entry_data_file:r2,"
+ "r2_partition:r102,"
+ "kv_name_r2_null_value_counts:k121_v122,"
+ "k_id_k121_v122:121,"
+ "v_id_k121_v122:122,"
+ "kv_name_r2_lower_bounds:k126_v127,"
+ "k_id_k126_v127:126,"
+ "v_id_k126_v127:127,"
+ "kv_name_r2_upper_bounds:k129_v130,"
+ "k_id_k129_v130:129,"
+ "v_id_k129_v130:130");
FileFormat manifestFileAvro = FileFormat.fromIdentifier("avro", avroOptions);
avroOptions.set("avro.row-name-mapping", ROW_NAME_MAPPING);
// The "content" Avro header differs per manifest (data vs deletes), so build one
// writer factory per content. See PR #9497 review.
Map<Content, FormatWriterFactory> writerFactories = new HashMap<>();
FormatReaderFactory readerFactory = null;
for (Content content : Content.values()) {
Options contentOptions = Options.fromMap(table.options());
contentOptions.set("avro.row-name-mapping", ROW_NAME_MAPPING);
Map<String, String> contentMetadata = new HashMap<>(avroMetadata);
contentMetadata.put("content", content == Content.DATA ? "data" : "deletes");
AvroFileFormat.setAvroMetadata(contentOptions, contentMetadata);
FileFormat contentAvro = FileFormat.fromIdentifier("avro", contentOptions);
writerFactories.put(content, contentAvro.createWriterFactory(entryType));
if (content == Content.DATA) {
readerFactory =
contentAvro.createReaderFactory(entryType, entryType, new ArrayList<>());
}
}
return new IcebergManifestFile(
table.fileIO(),
partitionType,
withFirstRowId,
manifestFileAvro.createReaderFactory(entryType, entryType, new ArrayList<>()),
manifestFileAvro.createWriterFactory(entryType),
readerFactory,
writerFactories,
avroOptions.get(IcebergOptions.MANIFEST_COMPRESSION),
pathFactory.manifestFileFactory(),
table.coreOptions().manifestTargetSize());
Expand Down Expand Up @@ -195,7 +249,11 @@ public List<IcebergManifestFileMeta> rollingWrite(
public SingleFileWriter<IcebergManifestEntry, IcebergManifestFileMeta> createWriter(
long sequenceNumber, Content content) {
return new IcebergManifestEntryWriter(
writerFactory, pathFactory.newPath(), compression, sequenceNumber, content);
writerFactories.get(content),
pathFactory.newPath(),
compression,
sequenceNumber,
content);
}

private class IcebergManifestEntryWriter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public class AvroFileFormat extends FileFormat {
private static final ConfigOption<Map<String, String>> AVRO_ROW_NAME_MAPPING =
ConfigOptions.key("avro.row-name-mapping").mapType().defaultValue(new HashMap<>());

private static final ConfigOption<Map<String, String>> AVRO_METADATA =
ConfigOptions.key("avro.metadata").mapType().defaultValue(new HashMap<>());

private final Options options;
private final int zstdLevel;

Expand Down Expand Up @@ -92,6 +95,12 @@ public AvroBlockWriter createBlockWriter(
AvroSchemaConverter.convertToSchema(rowType, options.get(AVRO_ROW_NAME_MAPPING));
AvroRowDatumWriter datumWriter = new AvroRowDatumWriter(rowType);
DataFileWriter<InternalRow> writer = new DataFileWriter<>(datumWriter);
Map<String, String> metadata = options.get(AVRO_METADATA);
if (metadata != null) {
for (Map.Entry<String, String> entry : metadata.entrySet()) {
writer.setMeta(entry.getKey(), entry.getValue());
}
}
writer.setCodec(createCodecFactory(compression));
writer.setFlushOnEveryBlock(false);
writer.create(schema, new CloseShieldOutputStream(out));
Expand Down Expand Up @@ -138,4 +147,13 @@ public FormatWriter create(PositionOutputStream out, String compression)
return createBlockWriter(out, rowType, compression);
}
}

/**
* Sets Avro file-level metadata key-value pairs on the given options. These metadata are
* written into the Avro container file header and are visible to Iceberg-compatible readers
* (e.g. Snowflake).
*/
public static void setAvroMetadata(Options options, Map<String, String> metadata) {
options.set(AVRO_METADATA, metadata);
}
}
Loading