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 7dd736493 fix(table): validate deletion vector fields in RowDelta 
(#2004 follow-up) (#2016)
7dd736493 is described below

commit 7dd7364930b6c888aa939a4fe19038ea51cc1086
Author: Badal Prasad Singh <[email protected]>
AuthorDate: Thu Sep 24 02:47:03 2026 +0530

    fix(table): validate deletion vector fields in RowDelta (#2004 follow-up) 
(#2016)
    
    * fix: reject dvs below v3
    
    Signed-off-by: badalprasadsingh <[email protected]>
    
    * minor follow ups
    
    Signed-off-by: badalprasadsingh <[email protected]>
    
    * minor
    
    Signed-off-by: badalprasadsingh <[email protected]>
    
    * chore: minor
    
    Signed-off-by: badalprasadsingh <[email protected]>
    
    * minor
    
    Signed-off-by: badalprasadsingh <[email protected]>
    
    * minor
    
    Signed-off-by: badalprasadsingh <[email protected]>
    
    ---------
    
    Signed-off-by: badalprasadsingh <[email protected]>
---
 table/data_file_meta_test.go           |   2 +-
 table/row_delta.go                     |   9 ++-
 table/row_delta_test.go                |  88 +++++++++++++++++----------
 table/row_lineage_prune_delete_test.go |  49 ++++++++++-----
 table/row_lineage_rewrite_test.go      |   2 +-
 table/row_lineage_scan_test.go         |  16 ++---
 table/transaction.go                   | 107 +++++++++++++++++----------------
 7 files changed, 158 insertions(+), 115 deletions(-)

diff --git a/table/data_file_meta_test.go b/table/data_file_meta_test.go
index b1101ea46..9ca145ed8 100644
--- a/table/data_file_meta_test.go
+++ b/table/data_file_meta_test.go
@@ -1228,7 +1228,7 @@ func 
TestDataFileFromMetadata_V3RewriteAcceptsNilFirstRowID(t *testing.T) {
 }
 
 func TestDataFileFromMetadata_EndToEndRowDelta(t *testing.T) {
-       tbl := newRowDeltaCommitTestTableVersion(t, 3)
+       tbl := newRowDeltaCommitTestTableVersion(t, 2)
        sch := tbl.Schema()
 
        const dataPath = "s3://bucket/test/data/insert.parquet"
diff --git a/table/row_delta.go b/table/row_delta.go
index bc37809f9..0e9508528 100644
--- a/table/row_delta.go
+++ b/table/row_delta.go
@@ -198,8 +198,13 @@ func (rd *RowDelta) Commit(ctx context.Context) error {
                                ct, f.FilePath())
                }
 
-               if err := validateDeletionVectorFormatVersion(f, 
meta.formatVersion, "row delta"); err != nil {
-                       return err
+               if IsDeletionVector(f) {
+                       if err := validateDeletionVectorToAdd(f, 
meta.formatVersion, "row delta"); err != nil {
+                               return err
+                       }
+               } else if ct == iceberg.EntryContentPosDeletes && 
meta.formatVersion >= 3 {
+                       return fmt.Errorf("position delete file %s must be a 
deletion vector for v%d table for row delta",
+                               f.FilePath(), meta.formatVersion)
                }
 
                // Equality delete files must declare which columns form the 
delete key,
diff --git a/table/row_delta_test.go b/table/row_delta_test.go
index 38f791217..237f0457c 100644
--- a/table/row_delta_test.go
+++ b/table/row_delta_test.go
@@ -1108,66 +1108,86 @@ func TestRowDeltaRemoveDeletesFailsInsteadOfReplaying(t 
*testing.T) {
                "the data file must carry exactly one live DV: the peer's")
 }
 
-func buildPuffinPosDeleteWithoutRef(t *testing.T, path string) 
iceberg.DataFile {
-       t.Helper()
-
-       b, err := iceberg.NewDataFileBuilder(*iceberg.UnpartitionedSpec, 
iceberg.EntryContentPosDeletes,
-               path, iceberg.PuffinFile, nil, nil, nil, 2, 128)
-       require.NoError(t, err)
-
-       return b.Build()
-}
-
-func TestRowDeltaRejectsDeletionVectorBelowV3(t *testing.T) {
+func TestRowDeltaRejectsInvalidDeletionVector(t *testing.T) {
        const (
                dataPath = "s3://bucket/data/insert.parquet"
                dvPath   = "s3://bucket/data/dv-001.puffin"
        )
+       offset, length := int64(4), int64(64)
+       negativeOffset, zeroLength := int64(-1), int64(0)
 
        tests := []struct {
                name          string
                formatVersion int
                rows          []iceberg.DataFile
-               deletes       func(*testing.T) []iceberg.DataFile
+               deletes       []iceberg.DataFile
                errContains   string
        }{
                {
                        name:          "deletion vector alone on v2",
                        formatVersion: 2,
-                       deletes: func(t *testing.T) []iceberg.DataFile {
-                               return []iceberg.DataFile{buildDVFile(t, 
dvPath, dataPath)}
-                       },
-                       errContains: "requires table format version >= 3",
+                       deletes:       []iceberg.DataFile{buildDVFile(t, 
dvPath, dataPath)},
+                       errContains:   "requires table format version >= 3",
                },
                {
                        name:          "deletion vector beside valid files on 
v2",
                        formatVersion: 2,
                        rows:          []iceberg.DataFile{buildDataFile(t, 
dataPath)},
-                       deletes: func(t *testing.T) []iceberg.DataFile {
-                               return []iceberg.DataFile{
-                                       buildPosDeleteFile(t, 
"s3://bucket/data/pos-del.parquet"),
-                                       buildEqDeleteFile(t, 
"s3://bucket/data/eq-del.parquet", []int{1}),
-                                       buildDVFile(t, dvPath, dataPath),
-                               }
+                       deletes: []iceberg.DataFile{
+                               buildPosDeleteFile(t, 
"s3://bucket/data/pos-del.parquet"),
+                               buildEqDeleteFile(t, 
"s3://bucket/data/eq-del.parquet", []int{1}),
+                               buildDVFile(t, dvPath, dataPath),
                        },
                        errContains: "requires table format version >= 3",
                },
                {
-                       name:          "deletion vector without referenced data 
file on v2",
+                       name:          "format version check precedes field 
checks on v2",
                        formatVersion: 2,
-                       deletes: func(t *testing.T) []iceberg.DataFile {
-                               return 
[]iceberg.DataFile{buildPuffinPosDeleteWithoutRef(t, dvPath)}
-                       },
-                       errContains: "requires table format version >= 3",
+                       deletes:       
[]iceberg.DataFile{newRewriteDeletionVector(t, dvPath, "", nil, nil)},
+                       errContains:   "requires table format version >= 3",
                },
                {
                        // v1 rejects every delete file before the DV check is 
reached.
                        name:          "deletion vector on v1",
                        formatVersion: 1,
-                       deletes: func(t *testing.T) []iceberg.DataFile {
-                               return []iceberg.DataFile{buildDVFile(t, 
dvPath, dataPath)}
-                       },
-                       errContains: "format version >= 2",
+                       deletes:       []iceberg.DataFile{buildDVFile(t, 
dvPath, dataPath)},
+                       errContains:   "format version >= 2",
+               },
+               {
+                       name:          "missing referenced data file on v3",
+                       formatVersion: 3,
+                       deletes:       
[]iceberg.DataFile{newRewriteDeletionVector(t, dvPath, "", &offset, &length)},
+                       errContains:   "missing referenced_data_file",
+               },
+               {
+                       name:          "missing content offset on v3",
+                       formatVersion: 3,
+                       deletes:       
[]iceberg.DataFile{newRewriteDeletionVector(t, dvPath, dataPath, nil, &length)},
+                       errContains:   "missing content_offset",
+               },
+               {
+                       name:          "negative content offset on v3",
+                       formatVersion: 3,
+                       deletes:       
[]iceberg.DataFile{newRewriteDeletionVector(t, dvPath, dataPath, 
&negativeOffset, &length)},
+                       errContains:   "invalid content_offset -1",
+               },
+               {
+                       name:          "missing content size on v3",
+                       formatVersion: 3,
+                       deletes:       
[]iceberg.DataFile{newRewriteDeletionVector(t, dvPath, dataPath, &offset, nil)},
+                       errContains:   "missing content_size_in_bytes",
+               },
+               {
+                       name:          "nonpositive content size on v3",
+                       formatVersion: 3,
+                       deletes:       
[]iceberg.DataFile{newRewriteDeletionVector(t, dvPath, dataPath, &offset, 
&zeroLength)},
+                       errContains:   "invalid content_size_in_bytes 0",
+               },
+               {
+                       name:          "parquet position delete on v3",
+                       formatVersion: 3,
+                       deletes:       []iceberg.DataFile{buildPosDeleteFile(t, 
"s3://bucket/data/pos-del.parquet")},
+                       errContains:   "must be a deletion vector for v3 table",
                },
        }
 
@@ -1175,7 +1195,7 @@ func TestRowDeltaRejectsDeletionVectorBelowV3(t 
*testing.T) {
                t.Run(tt.name, func(t *testing.T) {
                        tbl := newRowDeltaCommitTestTableVersion(t, 
tt.formatVersion)
 
-                       rd := 
tbl.NewTransaction().NewRowDelta(nil).AddRows(tt.rows...).AddDeletes(tt.deletes(t)...)
+                       rd := 
tbl.NewTransaction().NewRowDelta(nil).AddRows(tt.rows...).AddDeletes(tt.deletes...)
 
                        err := rd.Commit(t.Context())
                        require.Error(t, err)
@@ -1235,13 +1255,15 @@ func TestRowDeltaRemoveDeletesRequiresV3(t *testing.T) {
 // DV) while adding a valid DV.
 // Assertion: Commit fails identifying the live entry as not a DV.
 func TestRowDeltaRemoveDeletesRejectsNonDV(t *testing.T) {
-       tbl := newRowDeltaCommitTestTableVersion(t, 3)
+       tbl := newRowDeltaCommitTestTableVersion(t, 2)
        location := tbl.Location()
 
+       // v3 rejects new Parquet pos-deletes, so the live one predates an 
upgrade.
        posDelPath := location + "/data/pos-del-001.parquet"
        tx := tbl.NewTransaction()
        require.NoError(t, tx.NewRowDelta(nil).
                AddDeletes(buildPosDeleteFile(t, 
posDelPath)).Commit(t.Context()))
+       require.NoError(t, tx.UpgradeFormatVersion(3))
        tbl, err := tx.Commit(t.Context())
        require.NoError(t, err)
 
diff --git a/table/row_lineage_prune_delete_test.go 
b/table/row_lineage_prune_delete_test.go
index 9710a6b9f..1716b2fc1 100644
--- a/table/row_lineage_prune_delete_test.go
+++ b/table/row_lineage_prune_delete_test.go
@@ -43,12 +43,13 @@ import (
 // test prunes a leading row group and deletes a row that lives only in the
 // surviving group.
 
-// TestScanPruningWithPositionalDeletes covers a Parquet positional delete 
whose
-// target (pos 6 = id=7) is in the second row group while the filter prunes the
-// first. The deleted row must vanish and survivors must keep their _row_id.
+// TestScanPruningWithPositionalDeletes covers a legacy Parquet positional 
delete,
+// committed before the v3 upgrade, whose target (pos 6 = id=7) is in the 
second row group
+// while the filter prunes the first.
+// The deleted row must vanish and survivors must keep their _row_id.
 func TestScanPruningWithPositionalDeletes(t *testing.T) {
        ctx := context.Background()
-       tbl := buildTwoRowGroupV3Table(t)
+       tbl := buildTwoRowGroupTable(t, newV2RowLineageTestTable(t))
 
        tasks, err := tbl.Scan().PlanFiles(ctx)
        require.NoError(t, err)
@@ -64,10 +65,7 @@ func TestScanPruningWithPositionalDeletes(t *testing.T) {
                *iceberg.UnpartitionedSpec, iceberg.EntryContentPosDeletes,
                posDelPath, iceberg.ParquetFile, nil, nil, nil, 1, 256)
        require.NoError(t, err)
-       tx := tbl.NewTransaction()
-       require.NoError(t, 
tx.NewRowDelta(nil).AddDeletes(b.Build()).Commit(ctx))
-       tbl, err = tx.Commit(ctx)
-       require.NoError(t, err)
+       tbl = commitLegacyPosDelete(t, tbl, b.Build())
 
        filter := 
table.WithRowFilter(iceberg.GreaterThan(iceberg.Reference("id"), int64(5)))
 
@@ -85,7 +83,7 @@ func TestScanPruningWithPositionalDeletes(t *testing.T) {
 // drops pos 6 (id=7) in the surviving second row group while the first is 
pruned.
 func TestScanPruningWithDeletionVector(t *testing.T) {
        ctx := context.Background()
-       tbl := buildTwoRowGroupV3Table(t)
+       tbl := buildTwoRowGroupTable(t, newV3RowLineageTestTable(t))
 
        tasks, err := tbl.Scan().PlanFiles(ctx)
        require.NoError(t, err)
@@ -119,7 +117,7 @@ func TestScanPruningWithDeletionVector(t *testing.T) {
 // must keep advancing across batch boundaries, not reset per batch.
 func TestScanPruningSurvivingGroupSpansBatches(t *testing.T) {
        ctx := context.Background()
-       tbl := buildTwoRowGroupV3Table(t)
+       tbl := buildTwoRowGroupTable(t, newV3RowLineageTestTable(t))
 
        tx := tbl.NewTransaction()
        require.NoError(t, 
tx.SetProperties(iceberg.Properties{table.ParquetBatchSizeKey: "2"}))
@@ -133,7 +131,7 @@ func TestScanPruningSurvivingGroupSpansBatches(t 
*testing.T) {
 
 func TestScanPruningAllRowGroupsCompletes(t *testing.T) {
        ctx := context.Background()
-       tbl := buildTwoRowGroupV3Table(t)
+       tbl := buildTwoRowGroupTable(t, newV3RowLineageTestTable(t))
 
        _, itr, err := tbl.Scan(
                table.WithRowLineage(),
@@ -222,12 +220,11 @@ func 
TestReadTaskDeletionVectorSupersedesPositionalDeletes(t *testing.T) {
 // scan cannot prove bloom skipped a group, since a correct scan emits the same
 // rows whether or not the empty group was physically read.
 
-// buildTwoRowGroupV3Table writes a v3 row-lineage table with ids 1..10 across 
two
-// 5-row row groups, so an id>5 filter prunes the first group via stats.
-func buildTwoRowGroupV3Table(t *testing.T) *table.Table {
+// buildTwoRowGroupTable appends ids 1..10 to tbl across two 5-row row groups,
+// so an id>5 filter prunes the first group via stats.
+func buildTwoRowGroupTable(t *testing.T, tbl *table.Table) *table.Table {
        t.Helper()
        ctx := context.Background()
-       tbl := newV3RowLineageTestTable(t)
 
        tx := tbl.NewTransaction()
        require.NoError(t, 
tx.SetProperties(iceberg.Properties{table.ParquetRowGroupLimitKey: "5"}))
@@ -255,6 +252,28 @@ func buildTwoRowGroupV3Table(t *testing.T) *table.Table {
        return tbl
 }
 
+// commitLegacyPosDelete adds posDel while tbl is still v2, then upgrades it 
to v3.
+// The no-op Delete must keep committing a v3 snapshot: that snapshot gives 
the existing rows a _row_id.
+func commitLegacyPosDelete(t *testing.T, tbl *table.Table, posDel 
iceberg.DataFile) *table.Table {
+       t.Helper()
+       ctx := context.Background()
+
+       tx := tbl.NewTransaction()
+       require.NoError(t, tx.NewRowDelta(nil).AddDeletes(posDel).Commit(ctx))
+       require.NoError(t, tx.UpgradeFormatVersion(3))
+       require.NoError(t, tx.Delete(ctx, iceberg.AlwaysFalse{}, nil))
+       tbl, err := tx.Commit(ctx)
+       require.NoError(t, err)
+
+       tasks, err := tbl.Scan().PlanFiles(ctx)
+       require.NoError(t, err)
+       for _, task := range tasks {
+               require.NotNil(t, task.FirstRowID, "upgraded data file %s must 
inherit a first_row_id", task.File.FilePath())
+       }
+
+       return tbl
+}
+
 // idsMatchingFilter scans without row lineage and returns the surviving ids, 
sorted.
 func idsMatchingFilter(t *testing.T, tbl *table.Table, opts 
...table.ScanOption) []int64 {
        t.Helper()
diff --git a/table/row_lineage_rewrite_test.go 
b/table/row_lineage_rewrite_test.go
index 2c9b87c7b..d6f7e5ae2 100644
--- a/table/row_lineage_rewrite_test.go
+++ b/table/row_lineage_rewrite_test.go
@@ -302,7 +302,7 @@ func TestCoWRewriteDoesNotPruneMissingInitialDefault(t 
*testing.T) {
 
 func TestCoWRewriteNormalizesTheSurvivorComplement(t *testing.T) {
        ctx := context.Background()
-       tbl := buildTwoRowGroupV3Table(t)
+       tbl := buildTwoRowGroupTable(t, newV3RowLineageTestTable(t))
 
        var err error
        tbl, err = tbl.Delete(ctx,
diff --git a/table/row_lineage_scan_test.go b/table/row_lineage_scan_test.go
index 909dfdd79..619c872e1 100644
--- a/table/row_lineage_scan_test.go
+++ b/table/row_lineage_scan_test.go
@@ -120,11 +120,12 @@ func TestScanRowLineagePreservedThroughEqualityDeletes(t 
*testing.T) {
                "equality-deleted rows must not renumber surviving rows' 
_row_id")
 }
 
-// TestScanRowLineagePreservedThroughPositionalDeletes: a Parquet 
position-delete
-// file drops rows during the scan; survivors must keep their original _row_id.
+// TestScanRowLineagePreservedThroughPositionalDeletes: a legacy Parquet 
position-delete
+// file, committed before the v3 upgrade, drops rows during the scan; 
survivors must
+// keep their original _row_id.
 func TestScanRowLineagePreservedThroughPositionalDeletes(t *testing.T) {
        ctx := context.Background()
-       tbl := newV3RowLineageTestTable(t)
+       tbl := newV2RowLineageTestTable(t)
 
        arrowSchema := arrow.NewSchema([]arrow.Field{
                {Name: "id", Type: arrow.PrimitiveTypes.Int64, Nullable: false},
@@ -138,7 +139,6 @@ func TestScanRowLineagePreservedThroughPositionalDeletes(t 
*testing.T) {
 
        tbl, err = tbl.Append(ctx, array.NewTableReader(data, -1), nil)
        require.NoError(t, err)
-       require.Equal(t, map[int64]int64{1: 0, 2: 1, 3: 2, 4: 3, 5: 4}, 
rowIDByID(t, tbl))
 
        tasks, err := tbl.Scan().PlanFiles(ctx)
        require.NoError(t, err)
@@ -156,13 +156,7 @@ func TestScanRowLineagePreservedThroughPositionalDeletes(t 
*testing.T) {
                *iceberg.UnpartitionedSpec, iceberg.EntryContentPosDeletes,
                posDelPath, iceberg.ParquetFile, nil, nil, nil, 2, 256)
        require.NoError(t, err)
-
-       tx := tbl.NewTransaction()
-       rd := tx.NewRowDelta(nil)
-       rd.AddDeletes(b.Build())
-       require.NoError(t, rd.Commit(ctx))
-       tbl, err = tx.Commit(ctx)
-       require.NoError(t, err)
+       tbl = commitLegacyPosDelete(t, tbl, b.Build())
 
        assert.Equal(t, map[int64]int64{1: 0, 3: 2, 5: 4}, rowIDByID(t, tbl),
                "position-deleted rows must not renumber surviving rows' 
_row_id")
diff --git a/table/transaction.go b/table/transaction.go
index 1b5508f24..6a96b7d5f 100644
--- a/table/transaction.go
+++ b/table/transaction.go
@@ -1189,9 +1189,31 @@ type deleteFilesToAddSet struct {
        dvsByRef map[string]rewriteDeleteFileAddition
 }
 
-func validateDeletionVectorFormatVersion(df iceberg.DataFile, formatVersion 
int, operation string) error {
-       if IsDeletionVector(df) && formatVersion < 3 {
-               return fmt.Errorf("deletion vector %s requires table format 
version >= 3 for %s", df.FilePath(), operation)
+// validateDeletionVectorToAdd is safe on any DataFile and no-ops on non-DVs.
+func validateDeletionVectorToAdd(df iceberg.DataFile, formatVersion int, 
operation string) error {
+       if !IsDeletionVector(df) {
+               return nil
+       }
+       path := df.FilePath()
+       if formatVersion < 3 {
+               return fmt.Errorf("deletion vector %s requires table format 
version >= 3 for %s", path, operation)
+       }
+       if ref := df.ReferencedDataFile(); ref == nil || *ref == "" {
+               return fmt.Errorf("deletion vector %s is missing 
referenced_data_file for %s", path, operation)
+       }
+       offset := df.ContentOffset()
+       if offset == nil {
+               return fmt.Errorf("deletion vector %s is missing content_offset 
for %s", path, operation)
+       }
+       if *offset < 0 {
+               return fmt.Errorf("deletion vector %s has invalid 
content_offset %d for %s", path, *offset, operation)
+       }
+       length := df.ContentSizeInBytes()
+       if length == nil {
+               return fmt.Errorf("deletion vector %s is missing 
content_size_in_bytes for %s", path, operation)
+       }
+       if *length <= 0 {
+               return fmt.Errorf("deletion vector %s has invalid 
content_size_in_bytes %d for %s", path, *length, operation)
        }
 
        return nil
@@ -1265,10 +1287,6 @@ func (t *Transaction) 
validateDeleteFilesToAdd(deleteFiles []rewriteDeleteFileAd
                        }
                }
 
-               if err := validateDeletionVectorFormatVersion(df, 
meta.formatVersion, operation); err != nil {
-                       return nil, err
-               }
-
                if !IsDeletionVector(df) {
                        if meta.formatVersion >= 3 && df.ContentType() == 
iceberg.EntryContentPosDeletes {
                                return nil, fmt.Errorf("position delete file %s 
must be a deletion vector for v%d table for %s",
@@ -1286,54 +1304,39 @@ func (t *Transaction) 
validateDeleteFilesToAdd(deleteFiles []rewriteDeleteFileAd
                        continue
                }
 
-               if IsDeletionVector(df) {
-                       ref := df.ReferencedDataFile()
-                       if ref == nil || *ref == "" {
-                               return nil, fmt.Errorf("deletion vector to add 
is missing referenced_data_file for %s", operation)
-                       }
-                       offset := df.ContentOffset()
-                       if offset == nil {
-                               return nil, fmt.Errorf("deletion vector %s is 
missing content_offset for %s", path, operation)
-                       }
-                       if *offset < 0 {
-                               return nil, fmt.Errorf("deletion vector %s has 
invalid content_offset %d for %s", path, *offset, operation)
-                       }
-                       length := df.ContentSizeInBytes()
-                       if length == nil {
-                               return nil, fmt.Errorf("deletion vector %s is 
missing content_size_in_bytes for %s", path, operation)
-                       }
-                       if *length <= 0 {
-                               return nil, fmt.Errorf("deletion vector %s has 
invalid content_size_in_bytes %d for %s", path, *length, operation)
-                       }
+               if err := validateDeletionVectorToAdd(df, meta.formatVersion, 
operation); err != nil {
+                       return nil, err
+               }
+               // non-nil: validateDeletionVectorToAdd checked all three.
+               ref, offset, length := df.ReferencedDataFile(), 
df.ContentOffset(), df.ContentSizeInBytes()
 
-                       blob := deletionVectorBlobKey{path: path, offset: 
*offset, length: *length}
-                       if setToAdd.dvBlobs == nil {
-                               setToAdd.dvBlobs = 
make(map[deletionVectorBlobKey]struct{}, len(deleteFiles))
-                       }
-                       if _, ok := setToAdd.dvBlobs[blob]; ok {
-                               return nil, fmt.Errorf("deletion vector blob 
identity must be unique for %s: %s at offset %d with length %d",
-                                       operation, path, *offset, *length)
-                       }
-                       if _, ok := setToAdd.dvsByRef[*ref]; ok {
-                               return nil, fmt.Errorf("deletion vectors to add 
must reference distinct data files for %s: %s",
-                                       operation, *ref)
-                       }
-                       if pathAlreadyAdded {
-                               if _, ok := setToAdd.dvPaths[path]; !ok {
-                                       return nil, fmt.Errorf("delete file 
path %s cannot identify both a deletion vector container and a regular delete 
file for %s",
-                                               path, operation)
-                               }
-                       }
-                       if setToAdd.dvPaths == nil {
-                               setToAdd.dvPaths = make(map[string]struct{}, 
len(deleteFiles))
-                       }
-                       if setToAdd.dvsByRef == nil {
-                               setToAdd.dvsByRef = 
make(map[string]rewriteDeleteFileAddition, len(deleteFiles))
+               blob := deletionVectorBlobKey{path: path, offset: *offset, 
length: *length}
+               if setToAdd.dvBlobs == nil {
+                       setToAdd.dvBlobs = 
make(map[deletionVectorBlobKey]struct{}, len(deleteFiles))
+               }
+               if _, ok := setToAdd.dvBlobs[blob]; ok {
+                       return nil, fmt.Errorf("deletion vector blob identity 
must be unique for %s: %s at offset %d with length %d",
+                               operation, path, *offset, *length)
+               }
+               if _, ok := setToAdd.dvsByRef[*ref]; ok {
+                       return nil, fmt.Errorf("deletion vectors to add must 
reference distinct data files for %s: %s",
+                               operation, *ref)
+               }
+               if pathAlreadyAdded {
+                       if _, ok := setToAdd.dvPaths[path]; !ok {
+                               return nil, fmt.Errorf("delete file path %s 
cannot identify both a deletion vector container and a regular delete file for 
%s",
+                                       path, operation)
                        }
-                       setToAdd.dvPaths[path] = struct{}{}
-                       setToAdd.dvBlobs[blob] = struct{}{}
-                       setToAdd.dvsByRef[*ref] = addition
                }
+               if setToAdd.dvPaths == nil {
+                       setToAdd.dvPaths = make(map[string]struct{}, 
len(deleteFiles))
+               }
+               if setToAdd.dvsByRef == nil {
+                       setToAdd.dvsByRef = 
make(map[string]rewriteDeleteFileAddition, len(deleteFiles))
+               }
+               setToAdd.dvPaths[path] = struct{}{}
+               setToAdd.dvBlobs[blob] = struct{}{}
+               setToAdd.dvsByRef[*ref] = addition
        }
 
        return setToAdd, nil

Reply via email to