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 6e83162d8 test(table): pin copy-on-write abort when a concurrent
commit removed the same file (#2140)
6e83162d8 is described below
commit 6e83162d8bc4d405a322fc465d78348a27a4c1bf
Author: Revanth <[email protected]>
AuthorDate: Sun Oct 11 08:52:26 2026 -0500
test(table): pin copy-on-write abort when a concurrent commit removed the
same file (#2140)
* test(table): pin copy-on-write abort when a concurrent commit removed the
same file
Signed-off-by: Revanth Ch <[email protected]>
* test(table): pin copy-on-write divergence to checkRemovedFiles and verify
surviving rows
* docs(table): note copy-on-write shares rewriteValidator with
RewriteDataFiles
---------
Signed-off-by: Revanth Ch <[email protected]>
---
table/cow_delete_conflict_test.go | 60 ++++++++++++++++++++++++++++++++-------
table/transaction.go | 11 +++----
2 files changed, 56 insertions(+), 15 deletions(-)
diff --git a/table/cow_delete_conflict_test.go
b/table/cow_delete_conflict_test.go
index ec4c227cd..693681be3 100644
--- a/table/cow_delete_conflict_test.go
+++ b/table/cow_delete_conflict_test.go
@@ -75,7 +75,10 @@ func cowTestRecords(t *testing.T, rowsJSON string)
array.RecordReader {
require.NoError(t, err)
t.Cleanup(data.Release)
- return array.NewTableReader(data, -1)
+ rdr := array.NewTableReader(data, -1)
+ t.Cleanup(rdr.Release)
+
+ return rdr
}
// stageCopyOnWrite stages, without committing, a copy-on-write removal of the
@@ -104,15 +107,8 @@ var (
cowFormatVersions = []string{"2", "3"}
cowIsolations = []table.IsolationLevel{table.IsolationSerializable,
table.IsolationSnapshot}
cowOps = []string{"delete", "overwrite"}
-)
-// TestCopyOnWriteConflict_ConcurrentDeleteOnRemovedFile proves a copy-on-write
-// Delete or Overwrite is rejected, at every isolation level, when a concurrent
-// commit added a delete against a data file it removes. Without the validator,
-// refresh-and-replay swaps the original file for a rewrite built from the
stale
-// snapshot, dropping the concurrent delete and resurrecting its row (#2090).
-func TestCopyOnWriteConflict_ConcurrentDeleteOnRemovedFile(t *testing.T) {
- removals := []struct {
+ cowRemovals = []struct {
name string
filter iceberg.BooleanExpression
}{
@@ -121,11 +117,18 @@ func
TestCopyOnWriteConflict_ConcurrentDeleteOnRemovedFile(t *testing.T) {
// id>=1 matches every row, so the file is dropped outright.
{"fully deleted",
iceberg.GreaterThanEqual(iceberg.Reference("id"), int64(1))},
}
+)
+// TestCopyOnWriteConflict_ConcurrentDeleteOnRemovedFile proves a copy-on-write
+// Delete or Overwrite is rejected, at every isolation level, when a concurrent
+// commit added a delete against a data file it removes. Without the validator,
+// refresh-and-replay swaps the original file for a rewrite built from the
stale
+// snapshot, dropping the concurrent delete and resurrecting its row (#2090).
+func TestCopyOnWriteConflict_ConcurrentDeleteOnRemovedFile(t *testing.T) {
for _, version := range cowFormatVersions {
for _, isolation := range cowIsolations {
for _, op := range cowOps {
- for _, removal := range removals {
+ for _, removal := range cowRemovals {
name := fmt.Sprintf("v%s/%s/%s/%s",
version, isolation, op, removal.name)
t.Run(name, func(t *testing.T) {
ctx := context.Background()
@@ -198,3 +201,40 @@ func
TestCopyOnWriteConflict_ConcurrentDeleteOnUntouchedFileCommits(t *testing.T
}
}
}
+
+// TestCopyOnWriteConflict_ConcurrentRemovalOfSameFileDiverges pins the other
+// half of copy-on-write conflict detection: when a concurrent commit already
+// removed a data file this commit also removes, the stale commit aborts
+// terminally with ErrCommitDiverged (the retry rebuild's checkRemovedFiles)
+// rather than rebuilding the file from the stale snapshot, and the table keeps
+// the concurrent result.
+func TestCopyOnWriteConflict_ConcurrentRemovalOfSameFileDiverges(t *testing.T)
{
+ for _, version := range cowFormatVersions {
+ for _, isolation := range cowIsolations {
+ for _, op := range cowOps {
+ for _, removal := range cowRemovals {
+ name := fmt.Sprintf("v%s/%s/%s/%s",
version, isolation, op, removal.name)
+ t.Run(name, func(t *testing.T) {
+ ctx := context.Background()
+ tbl := appendTenRows(t,
newCoWConflictTestTable(t, version, isolation))
+
+ txn := stageCopyOnWrite(t, tbl,
op, removal.filter)
+
+ // A concurrent copy-on-write
delete of id==4 rewrites the
+ // same data file first.
+ _, err := stageCopyOnWrite(t,
tbl, "delete", iceberg.EqualTo(iceberg.Reference("id"), int64(4))).Commit(ctx)
+ require.NoError(t, err)
+
+ _, err = txn.Commit(ctx)
+ require.ErrorIs(t, err,
table.ErrCommitDiverged)
+ require.ErrorContains(t, err,
"no longer on the branch head")
+ require.NotErrorIs(t, err,
table.ErrCommitFailed)
+
+ require.NoError(t,
tbl.Refresh(ctx))
+ require.Equal(t, []int64{1, 2,
3, 5, 6, 7, 8, 9, 10}, idsInTable(t, tbl))
+ })
+ }
+ }
+ }
+ }
+}
diff --git a/table/transaction.go b/table/transaction.go
index b6a1bb706..9516a3b92 100644
--- a/table/transaction.go
+++ b/table/transaction.go
@@ -2496,12 +2496,13 @@ func (t *Transaction) performCopyOnWriteDeletion(ctx
context.Context, operation
// without this check a refresh-and-replay would swap the original file
// for a rewrite built from the stale snapshot, dropping the concurrent
// deletes and resurrecting their rows. Mirrors Java's copy-on-write
- // validateNoConflictingDeletes: no isolation gating.
- removed := append(slices.Clip(filesToDelete), filesToRewrite...)
+ // validateNoConflictingDeletes: no isolation gating. The check is
+ // shared with RewriteDataFiles via rewriteValidator, so changes there
+ // apply here too. A concurrent removal of one of these files is caught
+ // earlier, by the retry rebuild's checkRemovedFiles
(ErrCommitDiverged).
+ removed := slices.Concat(filesToDelete, filesToRewrite)
if len(removed) > 0 {
- t.addValidator(func(cc *conflictContext) error {
- return validateNoNewDeletesForRewrittenFiles(cc,
removed)
- })
+ t.addValidator(rewriteValidator(removed))
}
return updater, wfs, nil