This is an automated email from the ASF dual-hosted git repository.

laskoviymishka 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 ad3a17ea fix(manifest): reject row ID allocation overflow (#1627)
ad3a17ea is described below

commit ad3a17eac5fc08f1242156e7c5af238d7fbe9bb8
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 5 19:01:49 2026 +0200

    fix(manifest): reject row ID allocation overflow (#1627)
    
    * fix(manifest): reject row ID allocation overflow
    
    * fix(manifest): update row ID cursor after encoding
    
    * fix(manifest): handle unknown legacy row counts
    
    * fix(manifest): keep row ID cursor transactional
---
 manifest.go      |  56 ++++++++++++++++++++++++++--
 manifest_test.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 164 insertions(+), 3 deletions(-)

diff --git a/manifest.go b/manifest.go
index d41c008b..f2912eb8 100644
--- a/manifest.go
+++ b/manifest.go
@@ -1587,6 +1587,10 @@ func NewManifestListWriterV2(out io.Writer, snapshotID, 
sequenceNumber int64, pa
 }
 
 func NewManifestListWriterV3(out io.Writer, snapshotId, sequenceNumber, 
firstRowID int64, parentSnapshot *int64) (*ManifestListWriter, error) {
+       if firstRowID < 0 {
+               return nil, fmt.Errorf("%w: first row ID must be non-negative: 
%d", ErrInvalidArgument, firstRowID)
+       }
+
        m := &ManifestListWriter{
                version:          3,
                out:              out,
@@ -1608,6 +1612,28 @@ func NewManifestListWriterV3(out io.Writer, snapshotId, 
sequenceNumber, firstRow
        })
 }
 
+func advanceRowID(firstRowID, existingRows, addedRows int64) (int64, error) {
+       if firstRowID < 0 {
+               return 0, fmt.Errorf("%w: first row ID must be non-negative: 
%d", ErrInvalidArgument, firstRowID)
+       }
+       if existingRows == -1 {
+               existingRows = 0
+       }
+       if addedRows == -1 {
+               addedRows = 0
+       }
+       if existingRows < 0 || addedRows < 0 {
+               return 0, fmt.Errorf("%w: row counts must be non-negative: 
existing=%d added=%d",
+                       ErrInvalidArgument, existingRows, addedRows)
+       }
+       if existingRows > math.MaxInt64-firstRowID || addedRows > 
math.MaxInt64-firstRowID-existingRows {
+               return 0, fmt.Errorf("%w: assigning %d existing and %d added 
rows from first row ID %d overflows int64",
+                       ErrInvalidArgument, existingRows, addedRows, firstRowID)
+       }
+
+       return firstRowID + existingRows + addedRows, nil
+}
+
 func (m *ManifestListWriter) init(meta map[string][]byte) error {
        fileSchema, err := internal.NewManifestFileSchema(m.version)
        if err != nil {
@@ -1639,10 +1665,18 @@ func (m *ManifestListWriter) NextRowID() *int64 {
        return m.nextRowID
 }
 
-func (m *ManifestListWriter) AddManifests(files []ManifestFile) error {
+func (m *ManifestListWriter) AddManifests(files []ManifestFile) (err error) {
        if len(files) == 0 {
                return nil
        }
+       if m.version == 3 && m.nextRowID != nil {
+               batchStartRowID := *m.nextRowID
+               defer func() {
+                       if err != nil {
+                               *m.nextRowID = batchStartRowID
+                       }
+               }()
+       }
 
        switch m.version {
        case 1:
@@ -1663,6 +1697,8 @@ func (m *ManifestListWriter) AddManifests(files 
[]ManifestFile) error {
 
        case 2, 3:
                for _, file := range files {
+                       var assignedNextRowID *int64
+
                        // Per the Iceberg spec a v2 manifest list may 
reference v1 manifest
                        // files (and a v3 list may reference v1 or v2 
manifests) so that a
                        // table can be upgraded without rewriting historical 
manifests. The
@@ -1685,8 +1721,12 @@ func (m *ManifestListWriter) AddManifests(files 
[]ManifestFile) error {
                                        if wrapped.FirstRowIDValue == nil {
                                                if m.nextRowID != nil {
                                                        firstRowID := 
*m.nextRowID
+                                                       nextRowID, err := 
advanceRowID(firstRowID, wrapped.ExistingRowsCount, wrapped.AddedRowsCount)
+                                                       if err != nil {
+                                                               return 
fmt.Errorf("manifest %q: %w", wrapped.Path, err)
+                                                       }
                                                        wrapped.FirstRowIDValue 
= &firstRowID
-                                                       *m.nextRowID += 
wrapped.ExistingRowsCount + wrapped.AddedRowsCount
+                                                       assignedNextRowID = 
&nextRowID
                                                }
                                        }
                                }
@@ -1715,6 +1755,9 @@ func (m *ManifestListWriter) AddManifests(files 
[]ManifestFile) error {
                        if err := m.writer.Encode(wrapped); err != nil {
                                return err
                        }
+                       if assignedNextRowID != nil {
+                               *m.nextRowID = *assignedNextRowID
+                       }
                }
        default:
                return fmt.Errorf("unsupported manifest version: %d", m.version)
@@ -1798,6 +1841,10 @@ func WriteManifestV3(
        snapshotID int64,
        entries []ManifestEntry,
 ) (mf ManifestFile, nextFirstRowID int64, err error) {
+       if firstRowID < 0 {
+               return nil, 0, fmt.Errorf("%w: first row ID must be 
non-negative: %d", ErrInvalidArgument, firstRowID)
+       }
+
        cnt := &internal.CountingWriter{W: out}
 
        w, err := NewManifestWriter(3, cnt, spec, schema, snapshotID)
@@ -1824,7 +1871,10 @@ func WriteManifestV3(
        raw := mf.(*manifestFile)
        v := firstRowID
        raw.FirstRowIDValue = &v
-       nextFirstRowID = firstRowID + raw.AddedRowsCount + raw.ExistingRowsCount
+       nextFirstRowID, err = advanceRowID(firstRowID, raw.ExistingRowsCount, 
raw.AddedRowsCount)
+       if err != nil {
+               return nil, 0, fmt.Errorf("manifest %q: %w", filename, err)
+       }
 
        return raw, nextFirstRowID, nil
 }
diff --git a/manifest_test.go b/manifest_test.go
index 002b5fd3..7cc73030 100644
--- a/manifest_test.go
+++ b/manifest_test.go
@@ -1280,6 +1280,22 @@ func (m *ManifestTestSuite) TestWriteManifestV3() {
                m.EqualValues(520, nextID) // 500 + 10 + 10
        })
 
+       m.Run("rejects negative first row ID", func() {
+               var buf bytes.Buffer
+               _, _, err := WriteManifestV3("/manifest.avro", &buf, -1, 
partitionSpec, testSchema, entrySnapshotID, entries)
+               m.Require().ErrorIs(err, ErrInvalidArgument)
+               m.Require().ErrorContains(err, "first row ID must be 
non-negative")
+       })
+
+       m.Run("rejects next row ID overflow", func() {
+               var buf bytes.Buffer
+               _, _, err := WriteManifestV3(
+                       "/manifest.avro", &buf, math.MaxInt64-count, 
partitionSpec, testSchema, entrySnapshotID, entries,
+               )
+               m.Require().ErrorIs(err, ErrInvalidArgument)
+               m.Require().ErrorContains(err, "overflows int64")
+       })
+
        m.Run("read inheritance from manifest", func() {
                var buf bytes.Buffer
                firstRowID := int64(500)
@@ -2475,6 +2491,82 @@ func (m *ManifestTestSuite) 
TestV3ManifestListWriterRowIDTracking() {
        m.Require().NoError(err)
 }
 
+func (m *ManifestTestSuite) 
TestV3ManifestListWriterRejectsInvalidRowIDRanges() {
+       m.Run("negative first row ID", func() {
+               var buf bytes.Buffer
+               writer, err := NewManifestListWriterV3(&buf, snapshotID, 1, -1, 
nil)
+               m.Nil(writer)
+               m.Require().ErrorIs(err, ErrInvalidArgument)
+               m.Require().ErrorContains(err, "first row ID must be 
non-negative")
+       })
+
+       m.Run("negative row count", func() {
+               var buf bytes.Buffer
+               writer, err := NewManifestListWriterV3(&buf, snapshotID, 1, 0, 
nil)
+               m.Require().NoError(err)
+               manifest := NewManifestFile(3, "negative.avro", 100, 1, 
snapshotID).AddedRows(-2).Build()
+               err = writer.AddManifests([]ManifestFile{manifest})
+               m.Require().ErrorIs(err, ErrInvalidArgument)
+               m.Require().ErrorContains(err, "row counts must be 
non-negative")
+       })
+
+       m.Run("negative existing row count", func() {
+               var buf bytes.Buffer
+               writer, err := NewManifestListWriterV3(&buf, snapshotID, 1, 0, 
nil)
+               m.Require().NoError(err)
+               manifest := NewManifestFile(3, "negative-existing.avro", 100, 
1, snapshotID).ExistingRows(-2).Build()
+               err = writer.AddManifests([]ManifestFile{manifest})
+               m.Require().ErrorIs(err, ErrInvalidArgument)
+               m.Require().ErrorContains(err, "row counts must be 
non-negative")
+       })
+
+       m.Run("overflow", func() {
+               var buf bytes.Buffer
+               writer, err := NewManifestListWriterV3(&buf, snapshotID, 1, 
math.MaxInt64, nil)
+               m.Require().NoError(err)
+               manifest := NewManifestFile(3, "overflow.avro", 100, 1, 
snapshotID).AddedRows(1).Build()
+               err = writer.AddManifests([]ManifestFile{manifest})
+               m.Require().ErrorIs(err, ErrInvalidArgument)
+               m.Require().ErrorContains(err, "overflows int64")
+               m.EqualValues(math.MaxInt64, *writer.NextRowID())
+       })
+
+       m.Run("existing row count overflow", func() {
+               var buf bytes.Buffer
+               writer, err := NewManifestListWriterV3(&buf, snapshotID, 1, 
math.MaxInt64, nil)
+               m.Require().NoError(err)
+               manifest := NewManifestFile(3, "overflow-existing.avro", 100, 
1, snapshotID).ExistingRows(1).Build()
+               err = writer.AddManifests([]ManifestFile{manifest})
+               m.Require().ErrorIs(err, ErrInvalidArgument)
+               m.Require().ErrorContains(err, "overflows int64")
+               m.EqualValues(math.MaxInt64, *writer.NextRowID())
+       })
+
+       m.Run("later validation failure leaves cursor unchanged", func() {
+               var buf bytes.Buffer
+               writer, err := NewManifestListWriterV3(&buf, snapshotID, 1, 10, 
nil)
+               m.Require().NoError(err)
+               manifest := NewManifestFile(3, "other-snapshot.avro", 100, 1, 
snapshotID+1).AddedRows(5).Build()
+               err = writer.AddManifests([]ManifestFile{manifest})
+               m.Require().ErrorContains(err, "unassigned sequence number")
+               m.EqualValues(10, *writer.NextRowID())
+       })
+
+       m.Run("batch failure leaves cursor unchanged", func() {
+               var buf bytes.Buffer
+               startRowID := int64(math.MaxInt64 - 5)
+               writer, err := NewManifestListWriterV3(&buf, snapshotID, 1, 
startRowID, nil)
+               m.Require().NoError(err)
+               manifests := []ManifestFile{
+                       NewManifestFile(3, "valid.avro", 100, 1, 
snapshotID).AddedRows(5).Build(),
+                       NewManifestFile(3, "overflow.avro", 100, 1, 
snapshotID).AddedRows(1).Build(),
+               }
+               err = writer.AddManifests(manifests)
+               m.Require().ErrorContains(err, "overflows int64")
+               m.EqualValues(startRowID, *writer.NextRowID())
+       })
+}
+
 func (m *ManifestTestSuite) TestV3ManifestListWriterAssignedRowIDDelta() {
        // Assigned row-id delta = sum of (existing+added) for all data 
manifests in list.
        var buf bytes.Buffer
@@ -3135,6 +3227,25 @@ func (m *ManifestTestSuite) 
TestV3ManifestListAcceptsV1AndV2Manifests() {
        m.Nil(v2Entry.FirstRowID(), "delete manifests must not be assigned 
first_row_id")
 }
 
+func (m *ManifestTestSuite) 
TestV3ManifestListAssignsZeroForV1ManifestWithUnknownRowCounts() {
+       legacy := *(manifestFileRecordsV1[0].(*manifestFile))
+       legacy.AddedRowsCount = -1
+       legacy.ExistingRowsCount = -1
+
+       var v1Buf bytes.Buffer
+       m.Require().NoError(WriteManifestList(1, &v1Buf, snapshotID, nil, nil, 
0, []ManifestFile{&legacy}))
+       manifests, err := ReadManifestList(&v1Buf)
+       m.Require().NoError(err)
+       m.Require().Len(manifests, 1)
+
+       var v3Buf bytes.Buffer
+       writer, err := NewManifestListWriterV3(&v3Buf, snapshotID, 1, 1000, nil)
+       m.Require().NoError(err)
+       m.Require().NoError(writer.AddManifests(manifests))
+       m.EqualValues(1000, *writer.NextRowID())
+       m.Require().NoError(writer.Close())
+}
+
 // TestV2ManifestListRejectsV3Manifests confirms that a v2 manifest list still
 // refuses to reference v3 manifest files, since the v2 entry schema has no
 // place to record first_row_id and accepting them would silently drop data.

Reply via email to