You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ze...@apache.org on 2026/06/24 20:54:42 UTC
(iceberg-go) branch main updated: fix(manifest): commit writer state after encode (#1306)
This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git
The following commit(s) were added to refs/heads/main by this push:
new 2a831b00 fix(manifest): commit writer state after encode (#1306)
2a831b00 is described below
commit 2a831b00b65d5955243764eaa85aef06f7e3ae05
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Wed Jun 24 22:54:36 2026 +0200
fix(manifest): commit writer state after encode (#1306)
ManifestWriter.addEntry updated file counts, row counts, partition summaries, and the caller's DataFile partition data before prepareEntry and writer.Encode could fail, so a rejected entry left the writer looking as if it had been written. Encode the entry (through an Avro-field clone so partition conversion does not mutate the caller's DataFile) before updating any writer counters or summaries, keeping those mutations on the success side of Encode.
---
manifest.go | 66 +++++++++++++++++++++++++++-----------------------------
manifest_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 86 insertions(+), 34 deletions(-)
diff --git a/manifest.go b/manifest.go
index d887f7bd..91e1a179 100644
--- a/manifest.go
+++ b/manifest.go
@@ -1341,54 +1341,52 @@ func (w *ManifestWriter) addEntry(entry *manifestEntry) error {
return errors.New("cannot add entry to closed manifest writer")
}
- switch entry.Status() {
- case EntryStatusADDED:
- w.addedFiles++
- w.addedRows += entry.DataFile().Count()
- case EntryStatusEXISTING:
- w.existingFiles++
- w.existingRows += entry.DataFile().Count()
- case EntryStatusDELETED:
- w.deletedFiles++
- w.deletedRows += entry.DataFile().Count()
+ status := entry.Status()
+ switch status {
+ case EntryStatusADDED, EntryStatusEXISTING, EntryStatusDELETED:
default:
- return fmt.Errorf("unknown entry status: %v", entry.Status())
+ return fmt.Errorf("unknown entry status: %v", status)
}
+ count := entry.DataFile().Count()
+ partition := entry.Data.Partition()
- if setter, ok := entry.DataFile().(hasFieldToIDMap); ok {
- setter.setFieldNameToIDMap(w.partFieldNameToID)
- setter.setFieldIDToLogicalTypeMap(w.partFieldIDToType)
+ entryToEncode := *entry
+ if dataFile, ok := entry.DataFile().(*dataFile); ok {
+ encodeDataFile := cloneDataFileAvroFields(dataFile)
+ encodeDataFile.PartitionData = avroEncodePartitionData(partition, w.partFieldNameToID, w.partFieldIDToType)
+ entryToEncode.Data = encodeDataFile
}
- w.partitions = append(w.partitions, entry.Data.Partition())
- partitionData := avroPartitionData(entry.Data.Partition(), w.partFieldIDToType)
+ toEncode, err := w.impl.prepareEntry(&entryToEncode, w.snapshotID)
+ if err != nil {
+ return err
+ }
- if dataFile, ok := entry.DataFile().(*dataFile); ok {
- convertedPartitionData := make(map[string]any)
- for fieldID, convertedValue := range partitionData {
- for fieldName, id := range w.partFieldNameToID {
- if id == fieldID {
- convertedPartitionData[fieldName] = convertedValue
+ if err := w.writer.Encode(toEncode); err != nil {
+ return err
+ }
- break
- }
- }
- }
- dataFile.PartitionData = convertedPartitionData
+ switch status {
+ case EntryStatusADDED:
+ w.addedFiles++
+ w.addedRows += count
+ case EntryStatusEXISTING:
+ w.existingFiles++
+ w.existingRows += count
+ case EntryStatusDELETED:
+ w.deletedFiles++
+ w.deletedRows += count
}
- if entry.Status() == EntryStatusADDED || entry.Status() == EntryStatusEXISTING {
+ w.partitions = append(w.partitions, partition)
+
+ if status == EntryStatusADDED || status == EntryStatusEXISTING {
if seq := entry.SequenceNum(); seq >= 0 && (w.minSeqNum < 0 || seq < w.minSeqNum) {
w.minSeqNum = seq
}
}
- toEncode, err := w.impl.prepareEntry(entry, w.snapshotID)
- if err != nil {
- return err
- }
-
- return w.writer.Encode(toEncode)
+ return nil
}
func (w *ManifestWriter) Add(entry ManifestEntry) error {
diff --git a/manifest_test.go b/manifest_test.go
index 1ec5fc25..228d891c 100644
--- a/manifest_test.go
+++ b/manifest_test.go
@@ -2084,6 +2084,60 @@ func (w *limitedWriter) Write(p []byte) (int, error) {
return len(p), nil
}
+func (m *ManifestTestSuite) TestManifestWriterDoesNotCommitStateOnEncodeError() {
+ schema := NewSchema(0,
+ NestedField{ID: 1, Name: "dt", Type: PrimitiveTypes.Date},
+ )
+ partitionSpec := NewPartitionSpecID(1,
+ PartitionField{FieldID: 1000, SourceIDs: []int{1}, Name: "dt", Transform: IdentityTransform{}},
+ )
+
+ var out bytes.Buffer
+ writer, err := NewManifestWriter(2, &out, partitionSpec, schema, snapshotID)
+ m.Require().NoError(err)
+
+ badPartitionValue := struct{ Value string }{Value: "not-a-date"}
+ entry := newDatePartitionManifestEntry(m.T(), partitionSpec, snapshotID, badPartitionValue)
+ dataFile := entry.DataFile().(*dataFile)
+ originalPartitionData := map[string]any{"dt": badPartitionValue}
+ m.Equal(originalPartitionData, dataFile.PartitionData)
+
+ err = writer.Add(entry)
+ m.Require().Error(err)
+
+ m.Zero(writer.addedFiles)
+ m.Zero(writer.addedRows)
+ m.Zero(writer.existingFiles)
+ m.Zero(writer.existingRows)
+ m.Zero(writer.deletedFiles)
+ m.Zero(writer.deletedRows)
+ m.Empty(writer.partitions)
+ m.Equal(int64(-1), writer.minSeqNum)
+ m.Equal(originalPartitionData, dataFile.PartitionData)
+}
+
+func newDatePartitionManifestEntry(t *testing.T, partitionSpec PartitionSpec, snapshotID int64, partitionValue any) ManifestEntry {
+ t.Helper()
+
+ seqNum := int64(1)
+ builder, err := NewDataFileBuilder(
+ partitionSpec,
+ EntryContentData,
+ "s3://bucket/ns/table/data/date-partition.parquet",
+ ParquetFile,
+ map[int]any{1000: partitionValue},
+ nil,
+ nil,
+ 5,
+ 1024,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return NewManifestEntry(EntryStatusADDED, &snapshotID, &seqNum, &seqNum, builder.Build())
+}
+
func (m *ManifestTestSuite) TestWriteManifestListClosesWriterOnError() {
// A v2 manifest list cannot reference v3 manifests because the v2 entry
// schema has no first_row_id column; this gives us a deterministic