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/07/27 20:29:13 UTC
(iceberg-go) branch main updated: fix(table): safely match binary equality-delete partitions (#1528)
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 ef139c7ef fix(table): safely match binary equality-delete partitions (#1528)
ef139c7ef is described below
commit ef139c7ef83866afd100521d71af975f9d3318b6
Author: Minh Vu <vu...@gmail.com>
AuthorDate: Mon Jul 27 22:29:07 2026 +0200
fix(table): safely match binary equality-delete partitions (#1528)
## Problem
Equality-delete planning compares data-file and delete-file partition
maps before attaching a delete to a scan task. The maps store values as
`any`, and the previous direct `!=` comparison panicked when a supported
partition value was non-comparable in Go.
Binary and fixed partition values are represented as `[]byte`, so a
valid table could fail during scan planning with `comparing uncomparable
type []uint8`.
## Fix
Compare the maps with `maps.EqualFunc` and `reflect.DeepEqual`.
This keeps the existing matching contract:
- both maps must contain the same field IDs
- values must have the same concrete type and value
- independently allocated byte slices with the same bytes match
- different binary values or type mismatches do not match
It also remains safe if another non-comparable value reaches a custom
`DataFile` implementation. Comparable primitives retain their previous
equality semantics, including NaN values remaining unequal.
## Tests
The tests cover the comparison helper and the real equality-delete
matching path, including:
- equal and different byte slices
- binary versus string values
- comparable multi-field partitions
- different partition field IDs
- selecting only the matching delete file without panicking
Validation:
- `go test -count=1 ./...`
- `go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
run --timeout=10m`
Fixes #1349
Signed-off-by: Minh Vu <vu...@gmail.com>
---
table/scanner.go | 14 ++------
table/scanner_internal_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 75 insertions(+), 11 deletions(-)
diff --git a/table/scanner.go b/table/scanner.go
index 91c5e7466..28e5f4b4a 100644
--- a/table/scanner.go
+++ b/table/scanner.go
@@ -22,7 +22,9 @@ import (
"context"
"fmt"
"iter"
+ "maps"
"math"
+ "reflect"
"slices"
"strings"
"sync"
@@ -590,17 +592,7 @@ func matchEqualityDeletesToData(dataEntry iceberg.ManifestEntry, eqDeleteEntries
}
func partitionsMatch(a, b map[int]any) bool {
- if len(a) != len(b) {
- return false
- }
-
- for k, v := range a {
- if bv, ok := b[k]; !ok || bv != v {
- return false
- }
- }
-
- return true
+ return maps.EqualFunc(a, b, reflect.DeepEqual)
}
// buildDVIndex indexes deletion vectors by the data file path they reference.
diff --git a/table/scanner_internal_test.go b/table/scanner_internal_test.go
index 62cee6dd5..0ca010b2e 100644
--- a/table/scanner_internal_test.go
+++ b/table/scanner_internal_test.go
@@ -53,6 +53,78 @@ func newDeleteManifest(minSeqNum int64) iceberg.ManifestFile {
Build()
}
+func TestPartitionsMatchHandlesBinaryValues(t *testing.T) {
+ tests := []struct {
+ name string
+ left map[int]any
+ right map[int]any
+ match bool
+ }{
+ {
+ name: "equal binary values",
+ left: map[int]any{1000: []byte{0xde, 0xad}},
+ right: map[int]any{1000: append([]byte(nil), 0xde, 0xad)},
+ match: true,
+ },
+ {
+ name: "different binary values",
+ left: map[int]any{1000: []byte{0xde, 0xad}},
+ right: map[int]any{1000: []byte{0xbe, 0xef}},
+ },
+ {
+ name: "binary and string values differ",
+ left: map[int]any{1000: []byte("value")},
+ right: map[int]any{1000: "value"},
+ },
+ {
+ name: "comparable values still match",
+ left: map[int]any{1000: int32(7), 1001: "region"},
+ right: map[int]any{1000: int32(7), 1001: "region"},
+ match: true,
+ },
+ {
+ name: "different field IDs",
+ left: map[int]any{1000: []byte{1}},
+ right: map[int]any{1001: []byte{1}},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.NotPanics(t, func() {
+ assert.Equal(t, tt.match, partitionsMatch(tt.left, tt.right))
+ })
+ })
+ }
+}
+
+func TestMatchEqualityDeletesToDataHandlesBinaryPartitions(t *testing.T) {
+ dataSeqNum := int64(1)
+ deleteSeqNum := int64(2)
+ dataFile := &mockDataFile{
+ path: "data.parquet",
+ partition: map[int]any{1000: []byte{0xde, 0xad}},
+ }
+ matchingDelete := &mockDataFile{
+ path: "matching-delete.parquet",
+ partition: map[int]any{1000: append([]byte(nil), 0xde, 0xad)},
+ }
+ nonMatchingDelete := &mockDataFile{
+ path: "non-matching-delete.parquet",
+ partition: map[int]any{1000: []byte{0xbe, 0xef}},
+ }
+
+ dataEntry := iceberg.NewManifestEntry(
+ iceberg.EntryStatusADDED, nil, &dataSeqNum, nil, dataFile)
+ deleteEntries := []iceberg.ManifestEntry{
+ iceberg.NewManifestEntry(iceberg.EntryStatusADDED, nil, &deleteSeqNum, nil, nonMatchingDelete),
+ iceberg.NewManifestEntry(iceberg.EntryStatusADDED, nil, &deleteSeqNum, nil, matchingDelete),
+ }
+
+ assert.Equal(t, []iceberg.DataFile{matchingDelete},
+ matchEqualityDeletesToData(dataEntry, deleteEntries))
+}
+
func TestMinSequenceNum(t *testing.T) {
tests := []struct {
name string