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 9fb8cb632 perf(table): fast-path IN metrics pruning with extrema 
(#2117)
9fb8cb632 is described below

commit 9fb8cb632964921df5d6ed1c1bd66d4087aca3d8
Author: Minh Vu <[email protected]>
AuthorDate: Tue Oct 13 13:30:13 2026 +0200

    perf(table): fast-path IN metrics pruning with extrema (#2117)
    
    * perf(table): fast-path IN metrics pruning with extrema
    
    * test(table): verify IN benchmark pruning results
    
    * perf(table): reuse literal comparator for IN extrema
    
    * perf(table): resolve IN extrema comparator once
    
    * perf(table): straighten IN extrema pruning
    
    * docs(expr): update extrema visitor contract
    
    * test(table): cover IN extrema dispatch and types
    
    * bench(table): label IN extrema baselines
    
    * test(expr): keep geo IN off ordering extrema path
    
    * test(table): gofmt IN extrema coverage
    
    * perf(table): reuse IN extrema comparator
    
    * perf(table): reuse manifest IN extrema comparator
    
    * test(expr): prove IN extrema borrowed dispatch
    
    * perf(table): avoid comparator closure allocation for IN extrema
    
    * test(table): cover IN extrema dispatch and overlap
    
    * test: drop redundant bound extrema dispatch coverage
    
    * refactor(table): straighten IN bound evaluation
    
    * style: gofmt bound extrema test
    
    * style: gofmt IN extrema tests
    
    * perf(table): prune oversized IN sets by extrema
    
    * test(table): pin IN extrema evaluator dispatch
    
    * test: drop duplicate geo extrema coverage
    
    * fix(table): preserve IN slow-path pruning and cover extrema dispatch
    
    * style(table): gofmt IN extrema regression tests
    
    * test(table): verify IN extrema fast paths with valid bounds
    
    Check member access with real bound extrema across five literal types and 
remove a condition dominated by the oversized-set return.
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(table): pin IN extrema dispatch and comparator coverage
    
    Preserve member-scan behavior, cover supplied-extrema sentinels,
    verify comparator types, and document oversized and geo fallbacks.
    
    Signed-off-by: Minh Vu <[email protected]>
    
    * style(table): gofmt IN extrema comparator coverage
    
    * test(table): benchmark IN pruning across literal types
    
    Signed-off-by: Codex <[email protected]>
    Signed-off-by: Minh Vu <[email protected]>
    
    * test(table): guard oversized IN overlap from member scans
    
    ---------
    
    Signed-off-by: Minh Vu <[email protected]>
    Signed-off-by: Codex <[email protected]>
---
 bound_set_extrema_internal_test.go       |  19 +
 table/evaluators.go                      | 125 ++++++-
 table/in_predicate_extrema_bench_test.go | 116 ++++++
 table/in_predicate_extrema_test.go       | 586 +++++++++++++++++++++++++++++++
 visitors.go                              |   4 +-
 5 files changed, 836 insertions(+), 14 deletions(-)

diff --git a/bound_set_extrema_internal_test.go 
b/bound_set_extrema_internal_test.go
index 12908dfc6..6bdfb3355 100644
--- a/bound_set_extrema_internal_test.go
+++ b/bound_set_extrema_internal_test.go
@@ -88,3 +88,22 @@ func TestVisitBoundPredicateRefPassesInExtrema(t *testing.T) 
{
        assert.Nil(t, visitor.minLit)
        assert.Nil(t, visitor.maxLit)
 }
+
+func TestVisitBoundPredicateRefGeoSkipsExtrema(t *testing.T) {
+       geometry := GeometryType{}
+       first, err := LiteralFromBytes(geometry, []byte("1234567890abcdef"))
+       require.NoError(t, err)
+       second, err := LiteralFromBytes(geometry, []byte("fedcba9876543210"))
+       require.NoError(t, err)
+
+       schema := NewSchema(1, NestedField{ID: 1, Name: "value", Type: 
geometry})
+       bound, err := BindExpr(schema, SetPredicate(OpIn, Reference("value"), 
[]Literal{first, second}), true)
+       require.NoError(t, err)
+       pred, ok := bound.(BoundPredicate)
+       require.True(t, ok)
+
+       visitor := &boundSetExtremaVisitVisitor{boundSetVisitVisitor: 
boundSetVisitVisitor{needle: first}}
+       assert.True(t, VisitBoundPredicateRef(pred, visitor, 
internal.BoundPredicateRef{}))
+       assert.Nil(t, visitor.minLit)
+       assert.Nil(t, visitor.maxLit)
+}
diff --git a/table/evaluators.go b/table/evaluators.go
index 0e3e61967..1cf077d38 100644
--- a/table/evaluators.go
+++ b/table/evaluators.go
@@ -178,6 +178,9 @@ func (m *manifestEvalVisitor) VisitIn(term 
iceberg.BoundTerm, literals iceberg.S
        return m.visitIn(term, literals, nil, nil)
 }
 
+// VisitInWithExtrema expects minLit and maxLit to either both be the actual
+// extrema of literals or both be nil. A partial pair is treated as unavailable
+// and follows the same path as VisitIn.
 func (m *manifestEvalVisitor) VisitInWithExtrema(term iceberg.BoundTerm, 
literals iceberg.Set[iceberg.Literal], minLit, maxLit iceberg.Literal) bool {
        return m.visitIn(term, literals, minLit, maxLit)
 }
@@ -185,6 +188,7 @@ func (m *manifestEvalVisitor) VisitInWithExtrema(term 
iceberg.BoundTerm, literal
 func (m *manifestEvalVisitor) visitIn(term iceberg.BoundTerm, literals 
iceberg.Set[iceberg.Literal], minLit, maxLit iceberg.Literal) bool {
        pos := term.Ref().Pos()
        field := m.partitionFields[pos]
+       hasExtrema := minLit != nil && maxLit != nil
 
        if field.LowerBound == nil {
                return rowsCannotMatch
@@ -195,8 +199,10 @@ func (m *manifestEvalVisitor) visitIn(term 
iceberg.BoundTerm, literals iceberg.S
                panic(err)
        }
 
-       if maxLit != nil {
-               if getCmpLiteral(lower)(lower, maxLit) > 0 {
+       if hasExtrema {
+               // Like data-file metrics pruning, extrema can reject oversized 
IN
+               // sets before we apply the per-member scan limit.
+               if compareBoundLiterals(lower, maxLit) > 0 {
                        return rowsCannotMatch
                }
        } else {
@@ -214,8 +220,8 @@ func (m *manifestEvalVisitor) visitIn(term 
iceberg.BoundTerm, literals iceberg.S
                        panic(err)
                }
 
-               if minLit != nil {
-                       if getCmpLiteral(upper)(upper, minLit) < 0 {
+               if hasExtrema {
+                       if compareBoundLiterals(upper, minLit) < 0 {
                                return rowsCannotMatch
                        }
                } else if allBoundCheck(upper, literals, -1) {
@@ -332,6 +338,54 @@ func getCmpLiteral(boundary iceberg.Literal) 
func(iceberg.Literal, iceberg.Liter
        panic(iceberg.ErrType)
 }
 
+// compareLiteralValues requires the same concrete literal type on both sides,
+// as guaranteed for bound predicates. A mismatched type assertion will panic.
+func compareLiteralValues[T iceberg.LiteralType](left, right iceberg.Literal) 
int {
+       leftValue := left.(iceberg.TypedLiteral[T])
+       rightValue := right.(iceberg.TypedLiteral[T])
+
+       return leftValue.Comparator()(leftValue.Value(), rightValue.Value())
+}
+
+// Keep this dispatch aligned with getCmpLiteral, removeBoundCheck, and
+// allBoundCheck when adding a new literal type.
+func compareBoundLiterals(left, right iceberg.Literal) int {
+       switch left.(type) {
+       case iceberg.TypedLiteral[bool]:
+               return compareLiteralValues[bool](left, right)
+       case iceberg.TypedLiteral[int32]:
+               return compareLiteralValues[int32](left, right)
+       case iceberg.TypedLiteral[int64]:
+               return compareLiteralValues[int64](left, right)
+       case iceberg.TypedLiteral[float32]:
+               return compareLiteralValues[float32](left, right)
+       case iceberg.TypedLiteral[float64]:
+               return compareLiteralValues[float64](left, right)
+       case iceberg.TypedLiteral[iceberg.Date]:
+               return compareLiteralValues[iceberg.Date](left, right)
+       case iceberg.TypedLiteral[iceberg.Time]:
+               return compareLiteralValues[iceberg.Time](left, right)
+       case iceberg.TypedLiteral[iceberg.Timestamp]:
+               return compareLiteralValues[iceberg.Timestamp](left, right)
+       case iceberg.TypedLiteral[iceberg.TimestampNano]:
+               return compareLiteralValues[iceberg.TimestampNano](left, right)
+       case iceberg.TypedLiteral[[]byte]:
+               return compareLiteralValues[[]byte](left, right)
+       case iceberg.TypedLiteral[string]:
+               return compareLiteralValues[string](left, right)
+       case iceberg.TypedLiteral[uuid.UUID]:
+               return compareLiteralValues[uuid.UUID](left, right)
+       case iceberg.TypedLiteral[iceberg.Decimal]:
+               return compareLiteralValues[iceberg.Decimal](left, right)
+       case iceberg.GeoLiteral:
+               // Bound IN dispatch never supplies geo extrema because geo has 
no
+               // ordering. Keep this guard for direct or incorrectly routed 
calls.
+               panic(fmt.Errorf("%w: geometry/geography has no ordering, 
cannot compare %s bounds",
+                       iceberg.ErrType, left.Type()))
+       }
+       panic(iceberg.ErrType)
+}
+
 func (m *manifestEvalVisitor) VisitEqual(term iceberg.BoundTerm, lit 
iceberg.Literal) bool {
        pos := term.Ref().Pos()
        field := m.partitionFields[pos]
@@ -1160,34 +1214,81 @@ func (m *inclusiveMetricsEval) 
VisitNotEqual(iceberg.BoundTerm, iceberg.Literal)
 }
 
 func (m *inclusiveMetricsEval) VisitIn(t iceberg.BoundTerm, s 
iceberg.Set[iceberg.Literal]) bool {
+       return m.visitIn(t, s, nil, nil)
+}
+
+// VisitInWithExtrema expects minLit and maxLit to either both be the actual
+// extrema of s or both be nil. A partial pair is treated as unavailable and
+// follows the same path as VisitIn.
+func (m *inclusiveMetricsEval) VisitInWithExtrema(
+       t iceberg.BoundTerm, s iceberg.Set[iceberg.Literal], minLit, maxLit 
iceberg.Literal,
+) bool {
+       return m.visitIn(t, s, minLit, maxLit)
+}
+
+func (m *inclusiveMetricsEval) visitIn(
+       t iceberg.BoundTerm, s iceberg.Set[iceberg.Literal], minLit, maxLit 
iceberg.Literal,
+) bool {
        fieldID := t.Ref().Field().ID
 
        if m.containsNullsOnly(fieldID) || m.containsNansOnly(fieldID) {
                return rowsCannotMatch
        }
 
-       if s.Len() > inPredicateLimit {
-               // skip evaluating the predicate if the number of values is too 
big
+       hasExtrema := minLit != nil && maxLit != nil
+       oversized := s.Len() > inPredicateLimit
+       if oversized && !hasExtrema {
+               // Skip evaluating the predicate if the number of values is too 
big.
                return rowsMightMatch
        }
 
-       values := s.Members()
-       if lowerBound, ok := m.boundFor(t, m.lowerBounds[fieldID]); ok {
+       var values []iceberg.Literal
+       lowerBound, hasLowerBound := m.boundFor(t, m.lowerBounds[fieldID])
+       if hasLowerBound {
                if m.isNan(lowerBound) {
                        return rowsMightMatch
                }
-
-               values = removeBoundCheck(lowerBound, values, 1)
-               if len(values) == 0 {
+               if hasExtrema && compareBoundLiterals(lowerBound, maxLit) > 0 {
+                       // Short-circuit before decoding an unnecessary 
(possibly invalid) upper bound.
                        return rowsCannotMatch
                }
+               if !hasExtrema {
+                       // The slow path must finish the lower scan before 
reading the upper bound.
+                       values = removeBoundCheck(lowerBound, s.Members(), 1)
+                       if len(values) == 0 {
+                               return rowsCannotMatch
+                       }
+               }
        }
 
-       if upperBound, ok := m.boundFor(t, m.upperBounds[fieldID]); ok {
+       upperBound, hasUpperBound := m.boundFor(t, m.upperBounds[fieldID])
+       if hasUpperBound {
                if m.isNan(upperBound) {
                        return rowsMightMatch
                }
+               if hasExtrema && compareBoundLiterals(upperBound, minLit) < 0 {
+                       return rowsCannotMatch
+               }
+       }
+
+       if oversized {
+               // Both metrics evaluators prune disjoint oversized IN sets 
with O(1)
+               // extrema comparisons before limiting the member scan. Unlike 
Java's
+               // inclusive evaluator, this can reject files past the IN size 
limit.
+               return rowsMightMatch
+       }
+
+       if hasExtrema || !hasLowerBound {
+               values = s.Members()
+       }
+       if hasExtrema && hasLowerBound {
+               values = removeBoundCheck(lowerBound, values, 1)
+               if len(values) == 0 {
+                       return rowsCannotMatch
+               }
+       }
 
+       if hasUpperBound {
                values = removeBoundCheck(upperBound, values, -1)
                if len(values) == 0 {
                        return rowsCannotMatch
diff --git a/table/in_predicate_extrema_bench_test.go 
b/table/in_predicate_extrema_bench_test.go
index 03ce9ab9b..81a3c6a4f 100644
--- a/table/in_predicate_extrema_bench_test.go
+++ b/table/in_predicate_extrema_bench_test.go
@@ -18,6 +18,7 @@
 package table
 
 import (
+       "fmt"
        "strconv"
        "testing"
 
@@ -116,3 +117,118 @@ func benchmarkManifestIn[T iceberg.LiteralType](b 
*testing.B, values []T, lower,
                manifestInPredicateBenchmarkSink = matched
        }
 }
+
+func BenchmarkInclusiveMetricsEvalInPredicate(b *testing.B) {
+       for _, literalCount := range []int{10, 50, 200} {
+               for _, typ := range []struct {
+                       name    string
+                       typ     iceberg.Type
+                       literal func(int) iceberg.Literal
+               }{
+                       {"int32", iceberg.PrimitiveTypes.Int32, func(value int) 
iceberg.Literal {
+                               return iceberg.NewLiteral(int32(value))
+                       }},
+                       {"string", iceberg.PrimitiveTypes.String, func(value 
int) iceberg.Literal {
+                               // Keep lexical and numeric order identical, 
including the below-set bound.
+                               return 
iceberg.NewLiteral(fmt.Sprintf("value-%06d", value+1000))
+                       }},
+                       {"decimal", iceberg.DecimalTypeOf(12, 2), func(value 
int) iceberg.Literal {
+                               return iceberg.NewLiteral(iceberg.Decimal{Val: 
decimal128.FromI64(int64(value)), Scale: 2})
+                       }},
+               } {
+                       b.Run(typ.name+"/in="+strconv.Itoa(literalCount), 
func(b *testing.B) {
+                               values := make([]iceberg.Literal, literalCount)
+                               for i := range values {
+                                       values[i] = typ.literal(i * 2)
+                               }
+
+                               below := typ.literal(-2)
+                               above := typ.literal(literalCount * 2)
+                               gap := typ.literal((literalCount/2)*2 + 1)
+                               // Disjoint bounds exercise the extrema 
short-circuit. Overlap,
+                               // sparse gaps and missing bounds catch 
regressions on the fallback path.
+                               for _, bounds := range []struct {
+                                       name         string
+                                       lower, upper iceberg.Literal
+                                       wantMatch    bool
+                               }{
+                                       {name: "below-set", lower: below, 
upper: below},
+                                       {name: "above-set", lower: above, 
upper: above},
+                                       {name: "overlap", lower: values[0], 
upper: values[len(values)-1], wantMatch: true},
+                                       {name: "sparse-gap", lower: gap, upper: 
gap},
+                                       {name: "missing-bounds", wantMatch: 
true},
+                               } {
+                                       b.Run(bounds.name, func(b *testing.B) {
+                                               benchmarkInclusiveMetricsIn(b, 
typ.typ, values, bounds.lower, bounds.upper, bounds.wantMatch)
+                                       })
+                               }
+                       })
+               }
+       }
+}
+
+func benchmarkInclusiveMetricsIn(b *testing.B, typ iceberg.Type, values 
[]iceberg.Literal, lower, upper iceberg.Literal, wantMatch bool) {
+       b.Helper()
+
+       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: 
"value", Type: typ})
+       eval, err := newInclusiveMetricsEvaluator(
+               schema, iceberg.SetPredicate(iceberg.OpIn, 
iceberg.Reference("value"), values), true, true,
+       )
+       if err != nil {
+               b.Fatal(err)
+       }
+
+       builder, err := iceberg.NewDataFileBuilder(
+               *iceberg.UnpartitionedSpec, iceberg.EntryContentData, 
"file.parquet", iceberg.ParquetFile,
+               nil, nil, nil, 10, 100,
+       )
+       if err != nil {
+               b.Fatal(err)
+       }
+       builder.
+               ValueCounts(map[int]int64{1: 10}).
+               NullValueCounts(map[int]int64{1: 0}).
+               NaNValueCounts(map[int]int64{1: 0})
+       if lower != nil {
+               encoded, err := lower.MarshalBinary()
+               if err != nil {
+                       b.Fatal(err)
+               }
+               builder.LowerBoundValues(map[int][]byte{1: encoded})
+       }
+       if upper != nil {
+               encoded, err := upper.MarshalBinary()
+               if err != nil {
+                       b.Fatal(err)
+               }
+               builder.UpperBoundValues(map[int][]byte{1: encoded})
+       }
+       file := builder.Build()
+       files := make([]iceberg.DataFile, 10_000)
+       for i := range files {
+               files[i] = file
+       }
+
+       b.ReportAllocs()
+       for b.Loop() {
+               matchedFiles := 0
+               for _, f := range files {
+                       matched, err := eval(f)
+                       if err != nil {
+                               b.Fatal(err)
+                       }
+                       if matched {
+                               matchedFiles++
+                       }
+               }
+               wantFiles := 0
+               if wantMatch {
+                       wantFiles = len(files)
+               }
+               if matchedFiles != wantFiles {
+                       b.Fatalf("matched %d files, want %d", matchedFiles, 
wantFiles)
+               }
+       }
+       b.ReportMetric(float64(len(files)), "files/op")
+       b.ReportMetric(float64(len(values)), "literals/file")
+}
diff --git a/table/in_predicate_extrema_test.go 
b/table/in_predicate_extrema_test.go
index 81f6cb821..663293eda 100644
--- a/table/in_predicate_extrema_test.go
+++ b/table/in_predicate_extrema_test.go
@@ -18,14 +18,56 @@
 package table
 
 import (
+       "math"
        "testing"
 
        "github.com/apache/arrow-go/v18/arrow/decimal128"
+       "github.com/apache/arrow-go/v18/parquet/variant"
        "github.com/apache/iceberg-go"
+       "github.com/google/uuid"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
 )
 
+func TestCompareBoundLiteralsMatchesGetCmpLiteral(t *testing.T) {
+       decimal := func(value int64) iceberg.Decimal {
+               return iceberg.Decimal{Val: decimal128.FromI64(value), Scale: 2}
+       }
+       // Both dispatchers must recognize every orderable concrete literal 
type.
+       tests := []struct {
+               name         string
+               lower, upper iceberg.Literal
+       }{
+               {"bool", iceberg.NewLiteral(false), iceberg.NewLiteral(true)},
+               {"int32", iceberg.NewLiteral(int32(1)), 
iceberg.NewLiteral(int32(2))},
+               {"int64", iceberg.NewLiteral(int64(1)), 
iceberg.NewLiteral(int64(2))},
+               {"float32", iceberg.NewLiteral(float32(1)), 
iceberg.NewLiteral(float32(2))},
+               {"float64", iceberg.NewLiteral(float64(1)), 
iceberg.NewLiteral(float64(2))},
+               {"date", iceberg.NewLiteral(iceberg.Date(1)), 
iceberg.NewLiteral(iceberg.Date(2))},
+               {"time", iceberg.NewLiteral(iceberg.Time(1)), 
iceberg.NewLiteral(iceberg.Time(2))},
+               {"timestamp", iceberg.NewLiteral(iceberg.Timestamp(1)), 
iceberg.NewLiteral(iceberg.Timestamp(2))},
+               {"timestamp nanos", 
iceberg.NewLiteral(iceberg.TimestampNano(1)), 
iceberg.NewLiteral(iceberg.TimestampNano(2))},
+               {"binary", iceberg.NewLiteral([]byte("a")), 
iceberg.NewLiteral([]byte("b"))},
+               {"fixed", iceberg.FixedLiteral("a"), iceberg.FixedLiteral("b")},
+               {"string", iceberg.NewLiteral("a"), iceberg.NewLiteral("b")},
+               {"uuid", iceberg.NewLiteral(uuid.Nil), 
iceberg.NewLiteral(uuid.MustParse("00000000-0000-0000-0000-000000000001"))},
+               {"decimal", iceberg.NewLiteral(decimal(100)), 
iceberg.NewLiteral(decimal(200))},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       compare := getCmpLiteral(tt.lower)
+                       for _, pair := range [][2]iceberg.Literal{
+                               {tt.lower, tt.upper},
+                               {tt.upper, tt.lower},
+                               {tt.lower, tt.lower},
+                       } {
+                               assert.Equal(t, compare(pair[0], pair[1]), 
compareBoundLiterals(pair[0], pair[1]))
+                       }
+               })
+       }
+}
+
 func TestManifestEvaluatorInPredicateExtrema(t *testing.T) {
        decimal := func(value int64) iceberg.Decimal {
                return iceberg.Decimal{Val: decimal128.FromI64(value), Scale: 2}
@@ -139,6 +181,550 @@ func TestManifestEvaluatorInPredicateExtrema(t 
*testing.T) {
        }
 }
 
+func TestInclusiveMetricsEvaluatorInPredicateExtrema(t *testing.T) {
+       encode := func(value int32) []byte {
+               encoded, err := iceberg.NewLiteral(value).MarshalBinary()
+               require.NoError(t, err)
+
+               return encoded
+       }
+
+       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: 
"value", Type: iceberg.PrimitiveTypes.Int32})
+       expr := iceberg.IsIn(iceberg.Reference("value"), int32(1), int32(100))
+       eval, err := newInclusiveMetricsEvaluator(schema, expr, true, true)
+       require.NoError(t, err)
+
+       tests := []struct {
+               name       string
+               lowerBound []byte
+               upperBound []byte
+               want       bool
+       }{
+               {name: "lower bound above set", lowerBound: encode(101), 
upperBound: encode(200), want: false},
+               {name: "upper bound below set", lowerBound: encode(-10), 
upperBound: encode(0), want: false},
+               {name: "sparse set has no member in range", lowerBound: 
encode(40), upperBound: encode(60), want: false},
+               {name: "overlapping range contains member", lowerBound: 
encode(50), upperBound: encode(100), want: true},
+               {name: "upper bound equals minimum literal", upperBound: 
encode(1), want: true},
+               {name: "lower bound equals maximum literal", lowerBound: 
encode(100), want: true},
+               {name: "missing bounds", want: true},
+               {name: "upper bound below set without lower bound", upperBound: 
encode(0), want: false},
+               {name: "lower bound above set without upper bound", lowerBound: 
encode(101), want: false},
+               {name: "disjoint lower bound avoids malformed upper bound", 
lowerBound: encode(101), upperBound: []byte{1}, want: false},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       file := inclusiveMetricsInTestFile(t, tt.lowerBound, 
tt.upperBound)
+                       got, err := eval(file)
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.want, got)
+               })
+       }
+
+       t.Run("NaN bounds fail open", func(t *testing.T) {
+               encodeFloat := func(value float64) []byte {
+                       encoded, err := 
iceberg.NewLiteral(value).MarshalBinary()
+                       require.NoError(t, err)
+
+                       return encoded
+               }
+               nan := encodeFloat(math.NaN())
+               finiteLower := encodeFloat(0)
+               finiteUpper := encodeFloat(3)
+               floatSchema := iceberg.NewSchema(1, iceberg.NestedField{
+                       ID: 1, Name: "value", Type: 
iceberg.PrimitiveTypes.Float64,
+               })
+               floatEval, err := newInclusiveMetricsEvaluator(
+                       floatSchema, iceberg.IsIn(iceberg.Reference("value"), 
float64(1), float64(2)), true, true,
+               )
+               require.NoError(t, err)
+
+               for _, bounds := range []struct {
+                       name         string
+                       lower, upper []byte
+               }{
+                       {name: "both NaN", lower: nan, upper: nan},
+                       {name: "NaN lower finite upper", lower: nan, upper: 
finiteUpper},
+                       {name: "finite lower NaN upper", lower: finiteLower, 
upper: nan},
+               } {
+                       t.Run(bounds.name, func(t *testing.T) {
+                               got, err := 
floatEval(inclusiveMetricsInTestFile(t, bounds.lower, bounds.upper))
+                               require.NoError(t, err)
+                               assert.True(t, got)
+                       })
+               }
+       })
+
+       t.Run("oversized set still uses extrema before member scan limit", 
func(t *testing.T) {
+               values := make([]int32, inPredicateLimit+1)
+               for i := range values {
+                       values[i] = int32(i)
+               }
+               largeEval, err := newInclusiveMetricsEvaluator(
+                       schema, iceberg.IsIn(iceberg.Reference("value"), 
values...), true, true,
+               )
+               require.NoError(t, err)
+
+               disjointLower := int32(inPredicateLimit + 1000)
+               got, err := largeEval(inclusiveMetricsInTestFile(
+                       t, encode(disjointLower), encode(disjointLower+1),
+               ))
+               require.NoError(t, err)
+               assert.False(t, got)
+
+               got, err = largeEval(inclusiveMetricsInTestFile(t, encode(50), 
encode(60)))
+               require.NoError(t, err)
+               assert.True(t, got)
+
+               // An overlapping oversized set must return before scanning its 
members.
+               // This guards the size limit even when extrema cannot prune 
the file.
+               bound, err := iceberg.BindExpr(schema, 
iceberg.IsIn(iceberg.Reference("value"), values...), true)
+               require.NoError(t, err)
+               pred, ok := bound.(iceberg.BoundSetPredicate)
+               require.True(t, ok)
+
+               tracked := &inExtremaTrackingSet{Set: pred.Literals()}
+               visitor := &inclusiveMetricsEval{metricsEvaluator: 
metricsEvaluator{
+                       valueCounts: map[int]int64{1: 10},
+                       nullCounts:  map[int]int64{1: 0},
+                       nanCounts:   map[int]int64{1: 0},
+                       lowerBounds: map[int][]byte{1: encode(50)},
+                       upperBounds: map[int][]byte{1: encode(60)},
+               }}
+               require.True(t, visitor.VisitInWithExtrema(
+                       pred.Term(), tracked, iceberg.NewLiteral(int32(0)), 
iceberg.NewLiteral(int32(inPredicateLimit)),
+               ))
+               require.Zero(t, tracked.memberCalls)
+       })
+}
+
+func TestInclusiveMetricsEvaluatorInPredicateExtremaFastPathTypes(t 
*testing.T) {
+       decimal := func(value int64) iceberg.Decimal {
+               return iceberg.Decimal{Val: decimal128.FromI64(value), Scale: 2}
+       }
+
+       tests := []struct {
+               name           string
+               typ            iceberg.Type
+               expr           iceberg.BooleanExpression
+               minLit, maxLit iceberg.Literal
+               lower, upper   iceberg.Literal
+               want           bool
+       }{
+               {
+                       name:   "string lower disjoint",
+                       typ:    iceberg.PrimitiveTypes.String,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), "a", 
"b"),
+                       minLit: iceberg.NewLiteral("a"), maxLit: 
iceberg.NewLiteral("b"),
+                       lower: iceberg.NewLiteral("m"), upper: 
iceberg.NewLiteral("z"),
+               },
+               {
+                       name:   "string overlaps",
+                       typ:    iceberg.PrimitiveTypes.String,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), "a", 
"b"),
+                       minLit: iceberg.NewLiteral("a"), maxLit: 
iceberg.NewLiteral("b"),
+                       lower: iceberg.NewLiteral("a"), upper: 
iceberg.NewLiteral("a"),
+                       want: true,
+               },
+               {
+                       name:   "decimal upper disjoint",
+                       typ:    iceberg.DecimalTypeOf(12, 2),
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
decimal(100), decimal(200)),
+                       minLit: iceberg.NewLiteral(decimal(100)), maxLit: 
iceberg.NewLiteral(decimal(200)),
+                       lower: iceberg.NewLiteral(decimal(-200)), upper: 
iceberg.NewLiteral(decimal(0)),
+               },
+               {
+                       name:   "decimal overlaps",
+                       typ:    iceberg.DecimalTypeOf(12, 2),
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
decimal(100), decimal(200)),
+                       minLit: iceberg.NewLiteral(decimal(100)), maxLit: 
iceberg.NewLiteral(decimal(200)),
+                       lower: iceberg.NewLiteral(decimal(100)), upper: 
iceberg.NewLiteral(decimal(150)),
+                       want: true,
+               },
+               {
+                       name:   "binary lower disjoint",
+                       typ:    iceberg.PrimitiveTypes.Binary,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
[]byte{1}, []byte{2}),
+                       minLit: iceberg.NewLiteral([]byte{1}), maxLit: 
iceberg.NewLiteral([]byte{2}),
+                       lower: iceberg.NewLiteral([]byte{10}), upper: 
iceberg.NewLiteral([]byte{20}),
+               },
+               {
+                       name:   "binary overlaps",
+                       typ:    iceberg.PrimitiveTypes.Binary,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
[]byte{1}, []byte{2}),
+                       minLit: iceberg.NewLiteral([]byte{1}), maxLit: 
iceberg.NewLiteral([]byte{2}),
+                       lower: iceberg.NewLiteral([]byte{1}), upper: 
iceberg.NewLiteral([]byte{1}),
+                       want: true,
+               },
+               {
+                       name:   "date upper disjoint",
+                       typ:    iceberg.PrimitiveTypes.Date,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
iceberg.Date(10), iceberg.Date(20)),
+                       minLit: iceberg.NewLiteral(iceberg.Date(10)), maxLit: 
iceberg.NewLiteral(iceberg.Date(20)),
+                       lower: iceberg.NewLiteral(iceberg.Date(-10)), upper: 
iceberg.NewLiteral(iceberg.Date(0)),
+               },
+               {
+                       name:   "date overlaps",
+                       typ:    iceberg.PrimitiveTypes.Date,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
iceberg.Date(10), iceberg.Date(20)),
+                       minLit: iceberg.NewLiteral(iceberg.Date(10)), maxLit: 
iceberg.NewLiteral(iceberg.Date(20)),
+                       lower: iceberg.NewLiteral(iceberg.Date(10)), upper: 
iceberg.NewLiteral(iceberg.Date(15)),
+                       want: true,
+               },
+               {
+                       name:   "timestamp lower disjoint",
+                       typ:    iceberg.PrimitiveTypes.Timestamp,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
iceberg.Timestamp(10), iceberg.Timestamp(20)),
+                       minLit: iceberg.NewLiteral(iceberg.Timestamp(10)), 
maxLit: iceberg.NewLiteral(iceberg.Timestamp(20)),
+                       lower: iceberg.NewLiteral(iceberg.Timestamp(30)), 
upper: iceberg.NewLiteral(iceberg.Timestamp(40)),
+               },
+               {
+                       name:   "timestamp overlaps",
+                       typ:    iceberg.PrimitiveTypes.Timestamp,
+                       expr:   iceberg.IsIn(iceberg.Reference("value"), 
iceberg.Timestamp(10), iceberg.Timestamp(20)),
+                       minLit: iceberg.NewLiteral(iceberg.Timestamp(10)), 
maxLit: iceberg.NewLiteral(iceberg.Timestamp(20)),
+                       lower: iceberg.NewLiteral(iceberg.Timestamp(10)), 
upper: iceberg.NewLiteral(iceberg.Timestamp(15)),
+                       want: true,
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 
1, Name: "value", Type: tt.typ})
+                       bound, err := iceberg.BindExpr(schema, tt.expr, true)
+                       require.NoError(t, err)
+                       pred, ok := bound.(iceberg.BoundSetPredicate)
+                       require.True(t, ok)
+
+                       lower, err := tt.lower.MarshalBinary()
+                       require.NoError(t, err)
+                       upper, err := tt.upper.MarshalBinary()
+                       require.NoError(t, err)
+                       newVisitor := func() *inclusiveMetricsEval {
+                               return &inclusiveMetricsEval{metricsEvaluator: 
metricsEvaluator{
+                                       valueCounts: map[int]int64{1: 10},
+                                       nullCounts:  map[int]int64{1: 0},
+                                       nanCounts:   map[int]int64{1: 0},
+                                       lowerBounds: map[int][]byte{1: lower},
+                                       upperBounds: map[int][]byte{1: upper},
+                               }}
+                       }
+
+                       slowSet := &inExtremaTrackingSet{Set: pred.Literals()}
+                       fastSet := &inExtremaTrackingSet{Set: pred.Literals()}
+                       slow := newVisitor().VisitIn(pred.Term(), slowSet)
+                       fast := newVisitor().VisitInWithExtrema(pred.Term(), 
fastSet, tt.minLit, tt.maxLit)
+                       require.Equal(t, slow, fast)
+                       require.Equal(t, tt.want, fast)
+                       require.Equal(t, 1, slowSet.memberCalls)
+                       if tt.want {
+                               // Overlapping ranges must still visit the 
exact set members.
+                               require.Equal(t, 1, fastSet.memberCalls)
+                       } else {
+                               require.Zero(t, fastSet.memberCalls)
+                       }
+               })
+       }
+}
+
+type inExtremaTrackingSet struct {
+       iceberg.Set[iceberg.Literal]
+       memberCalls int
+}
+
+func (s *inExtremaTrackingSet) Members() []iceberg.Literal {
+       s.memberCalls++
+
+       return s.Set.Members()
+}
+
+func TestInclusiveMetricsEvaluatorInPredicateExtremaUsesFastPath(t *testing.T) 
{
+       decimal := func(value int64) iceberg.Decimal {
+               return iceberg.Decimal{Val: decimal128.FromI64(value), Scale: 2}
+       }
+       // Both paths must prune these disjoint files. The extrema path must do 
so
+       // without materializing the set members, using the actual bound 
extrema.
+       tests := []struct {
+               name           string
+               typ            iceberg.Type
+               bounds         []iceberg.Literal
+               minLit, maxLit iceberg.Literal
+       }{
+               {
+                       "string", iceberg.PrimitiveTypes.String,
+                       []iceberg.Literal{iceberg.NewLiteral("m"), 
iceberg.NewLiteral("n")},
+                       iceberg.NewLiteral("a"), iceberg.NewLiteral("b"),
+               },
+               {
+                       "decimal", iceberg.DecimalTypeOf(12, 2),
+                       []iceberg.Literal{iceberg.NewLiteral(decimal(5000)), 
iceberg.NewLiteral(decimal(6000))},
+                       iceberg.NewLiteral(decimal(100)), 
iceberg.NewLiteral(decimal(200)),
+               },
+               {
+                       "binary", iceberg.PrimitiveTypes.Binary,
+                       []iceberg.Literal{iceberg.NewLiteral([]byte("m")), 
iceberg.NewLiteral([]byte("n"))},
+                       iceberg.NewLiteral([]byte("a")), 
iceberg.NewLiteral([]byte("b")),
+               },
+               {
+                       "date", iceberg.PrimitiveTypes.Date,
+                       []iceberg.Literal{iceberg.NewLiteral(iceberg.Date(50)), 
iceberg.NewLiteral(iceberg.Date(60))},
+                       iceberg.NewLiteral(iceberg.Date(1)), 
iceberg.NewLiteral(iceberg.Date(2)),
+               },
+               {
+                       "timestamp", iceberg.PrimitiveTypes.Timestamp,
+                       
[]iceberg.Literal{iceberg.NewLiteral(iceberg.Timestamp(50)), 
iceberg.NewLiteral(iceberg.Timestamp(60))},
+                       iceberg.NewLiteral(iceberg.Timestamp(1)), 
iceberg.NewLiteral(iceberg.Timestamp(2)),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 
1, Name: "value", Type: tt.typ})
+                       bound, err := iceberg.BindExpr(schema, 
iceberg.SetPredicate(
+                               iceberg.OpIn, iceberg.Reference("value"), 
[]iceberg.Literal{tt.minLit, tt.maxLit},
+                       ), true)
+                       require.NoError(t, err)
+                       pred, ok := bound.(iceberg.BoundSetPredicate)
+                       require.True(t, ok)
+
+                       lower, err := tt.bounds[0].MarshalBinary()
+                       require.NoError(t, err)
+                       upper, err := tt.bounds[1].MarshalBinary()
+                       require.NoError(t, err)
+                       newVisitor := func() *inclusiveMetricsEval {
+                               return &inclusiveMetricsEval{metricsEvaluator: 
metricsEvaluator{
+                                       valueCounts: map[int]int64{1: 10},
+                                       nullCounts:  map[int]int64{1: 0},
+                                       nanCounts:   map[int]int64{1: 0},
+                                       lowerBounds: map[int][]byte{1: lower},
+                                       upperBounds: map[int][]byte{1: upper},
+                               }}
+                       }
+
+                       slowSet := &inExtremaTrackingSet{Set: pred.Literals()}
+                       require.False(t, newVisitor().VisitIn(pred.Term(), 
slowSet))
+                       require.Equal(t, 1, slowSet.memberCalls)
+
+                       fastSet := &inExtremaTrackingSet{Set: pred.Literals()}
+                       require.False(t, 
newVisitor().VisitInWithExtrema(pred.Term(), fastSet, tt.minLit, tt.maxLit))
+                       require.Zero(t, fastSet.memberCalls)
+               })
+       }
+
+       t.Run("supplied extrema are used, not recomputed", func(t *testing.T) {
+               schema := iceberg.NewSchema(1, iceberg.NestedField{
+                       ID: 1, Name: "value", Type: 
iceberg.PrimitiveTypes.Int32,
+               })
+               bound, err := iceberg.BindExpr(
+                       schema, iceberg.IsIn(iceberg.Reference("value"), 
int32(50), int32(100)), true,
+               )
+               require.NoError(t, err)
+               pred, ok := bound.(iceberg.BoundSetPredicate)
+               require.True(t, ok)
+
+               lower, err := iceberg.NewLiteral(int32(40)).MarshalBinary()
+               require.NoError(t, err)
+               upper, err := iceberg.NewLiteral(int32(60)).MarshalBinary()
+               require.NoError(t, err)
+               visitor := &inclusiveMetricsEval{metricsEvaluator: 
metricsEvaluator{
+                       valueCounts: map[int]int64{1: 10},
+                       nullCounts:  map[int]int64{1: 0},
+                       nanCounts:   map[int]int64{1: 0},
+                       lowerBounds: map[int][]byte{1: lower},
+                       upperBounds: map[int][]byte{1: upper},
+               }}
+
+               slowSet := &inExtremaTrackingSet{Set: pred.Literals()}
+               require.True(t, visitor.VisitIn(pred.Term(), slowSet))
+               require.Equal(t, 1, slowSet.memberCalls)
+
+               // Binding always supplies real extrema. Deliberately incorrect 
values
+               // distinguish the fast-path contract from recomputing or 
scanning members.
+               fastSet := &inExtremaTrackingSet{Set: pred.Literals()}
+               require.False(t, visitor.VisitInWithExtrema(
+                       pred.Term(), fastSet, iceberg.NewLiteral(int32(1)), 
iceberg.NewLiteral(int32(2)),
+               ))
+               require.Zero(t, fastSet.memberCalls)
+       })
+}
+
+func TestInclusiveMetricsEvaluatorInPredicateSlowPathLowerBeforeUpper(t 
*testing.T) {
+       tests := []struct {
+               name           string
+               typ            iceberg.Type
+               members        []iceberg.Literal
+               lower          iceberg.Literal
+               upper          iceberg.Literal
+               malformedUpper bool
+       }{
+               {
+                       name:           "malformed upper",
+                       typ:            iceberg.PrimitiveTypes.Int32,
+                       members:        
[]iceberg.Literal{iceberg.NewLiteral(int32(1)), iceberg.NewLiteral(int32(2))},
+                       lower:          iceberg.NewLiteral(int32(3)),
+                       malformedUpper: true,
+               },
+               {
+                       name:    "NaN upper",
+                       typ:     iceberg.PrimitiveTypes.Float64,
+                       members: 
[]iceberg.Literal{iceberg.NewLiteral(float64(1)), 
iceberg.NewLiteral(float64(2))},
+                       lower:   iceberg.NewLiteral(float64(3)),
+                       upper:   iceberg.NewLiteral(math.NaN()),
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 
1, Name: "value", Type: tt.typ})
+                       bound, err := iceberg.BindExpr(schema, 
iceberg.SetPredicate(iceberg.OpIn, iceberg.Reference("value"), tt.members), 
true)
+                       require.NoError(t, err)
+                       pred, ok := bound.(iceberg.BoundSetPredicate)
+                       require.True(t, ok)
+
+                       lower, err := tt.lower.MarshalBinary()
+                       require.NoError(t, err)
+                       upper := []byte{1}
+                       if !tt.malformedUpper {
+                               upper, err = tt.upper.MarshalBinary()
+                               require.NoError(t, err)
+                       }
+                       newVisitor := func() *inclusiveMetricsEval {
+                               return &inclusiveMetricsEval{metricsEvaluator: 
metricsEvaluator{
+                                       valueCounts: map[int]int64{1: 10},
+                                       nullCounts:  map[int]int64{1: 0},
+                                       nanCounts:   map[int]int64{1: 0},
+                                       lowerBounds: map[int][]byte{1: lower},
+                                       upperBounds: map[int][]byte{1: upper},
+                               }}
+                       }
+
+                       // Both slow entry points must prune without even 
decoding the upper bound.
+                       require.False(t, newVisitor().VisitIn(pred.Term(), 
pred.Literals()))
+                       require.False(t, 
newVisitor().VisitInWithExtrema(pred.Term(), pred.Literals(), tt.lower, nil))
+                       require.False(t, 
newVisitor().VisitInWithExtrema(pred.Term(), pred.Literals(), nil, tt.lower))
+               })
+       }
+}
+
+func TestInclusiveMetricsEvaluatorInPredicateExtremaPartialNilUsesSlowPath(t 
*testing.T) {
+       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: 
"value", Type: iceberg.PrimitiveTypes.Int32})
+       bound, err := iceberg.BindExpr(
+               schema, iceberg.IsIn(iceberg.Reference("value"), int32(1), 
int32(100)), true,
+       )
+       require.NoError(t, err)
+       pred := bound.(iceberg.BoundSetPredicate)
+       lower, err := iceberg.NewLiteral(int32(40)).MarshalBinary()
+       require.NoError(t, err)
+       upper, err := iceberg.NewLiteral(int32(60)).MarshalBinary()
+       require.NoError(t, err)
+
+       newVisitor := func() *inclusiveMetricsEval {
+               return &inclusiveMetricsEval{metricsEvaluator: metricsEvaluator{
+                       valueCounts: map[int]int64{1: 10},
+                       nullCounts:  map[int]int64{1: 0},
+                       nanCounts:   map[int]int64{1: 0},
+                       lowerBounds: map[int][]byte{1: lower},
+                       upperBounds: map[int][]byte{1: upper},
+               }}
+       }
+       slow := newVisitor().VisitIn(pred.Term(), pred.Literals())
+       minLit := iceberg.NewLiteral(int32(1))
+       maxLit := iceberg.NewLiteral(int32(100))
+       require.Equal(t, slow, newVisitor().VisitInWithExtrema(pred.Term(), 
pred.Literals(), minLit, nil))
+       require.Equal(t, slow, newVisitor().VisitInWithExtrema(pred.Term(), 
pred.Literals(), nil, maxLit))
+}
+
+func TestInclusiveMetricsEvaluatorGeoInUsesFallback(t *testing.T) {
+       geometry := iceberg.GeometryType{}
+       first, err := iceberg.LiteralFromBytes(geometry, 
[]byte("1234567890abcdef"))
+       require.NoError(t, err)
+       second, err := iceberg.LiteralFromBytes(geometry, 
[]byte("fedcba9876543210"))
+       require.NoError(t, err)
+
+       schema := iceberg.NewSchema(1, iceberg.NestedField{ID: 1, Name: 
"value", Type: geometry})
+       pred := iceberg.SetPredicate(iceberg.OpIn, iceberg.Reference("value"), 
[]iceberg.Literal{first, second})
+       eval, err := newInclusiveMetricsEvaluator(schema, pred, true, true)
+       require.NoError(t, err)
+
+       // Geo has no total ordering, so bound dispatch must avoid extrema 
comparisons.
+       // Missing bounds leave metrics pruning conservative.
+       mightMatch, err := eval(inclusiveMetricsInTestFile(t, nil, nil))
+       require.NoError(t, err)
+       require.True(t, mightMatch)
+}
+
+func TestInclusiveMetricsEvaluatorInPredicateExtremaVariantExtract(t 
*testing.T) {
+       schema := iceberg.NewSchema(1, iceberg.NestedField{
+               ID: 1, Name: "payload", Type: iceberg.VariantType{},
+       })
+       expr := iceberg.IsIn(
+               iceberg.Extract("payload", "$.a", iceberg.PrimitiveTypes.Int64),
+               int64(1), int64(2),
+       )
+       bound, err := iceberg.BindExpr(schema, expr, true)
+       require.NoError(t, err)
+       pred, ok := bound.(iceberg.BoundSetPredicate)
+       require.True(t, ok)
+
+       newVisitor := func() *inclusiveMetricsEval {
+               return &inclusiveMetricsEval{metricsEvaluator: metricsEvaluator{
+                       valueCounts: map[int]int64{1: 10},
+                       nullCounts:  map[int]int64{1: 0},
+                       nanCounts:   map[int]int64{1: 0},
+                       lowerBounds: map[int][]byte{1: 
variantMetricBoundInt64(t, "$['a']", 10)},
+                       upperBounds: map[int][]byte{1: 
variantMetricBoundInt64(t, "$['a']", 20)},
+               }}
+       }
+
+       slow := newVisitor().VisitIn(pred.Term(), pred.Literals())
+       fast := newVisitor().VisitInWithExtrema(
+               pred.Term(), pred.Literals(), iceberg.NewLiteral(int64(1)), 
iceberg.NewLiteral(int64(2)),
+       )
+       require.False(t, slow)
+       require.Equal(t, slow, fast)
+}
+
+func variantMetricBoundInt64(t *testing.T, path string, value int64) []byte {
+       t.Helper()
+
+       var builder variant.Builder
+       start := builder.Offset()
+       entries := []variant.FieldEntry{builder.NextField(start, path)}
+       require.NoError(t, builder.AppendInt(value))
+       require.NoError(t, builder.FinishObject(start, entries))
+       v, err := builder.Build()
+       require.NoError(t, err)
+
+       return append(append([]byte{}, v.Metadata().Bytes()...), v.Bytes()...)
+}
+
+func inclusiveMetricsInTestFile(t *testing.T, lowerBound, upperBound []byte) 
iceberg.DataFile {
+       t.Helper()
+
+       builder, err := iceberg.NewDataFileBuilder(
+               *iceberg.UnpartitionedSpec, iceberg.EntryContentData, 
"file.parquet", iceberg.ParquetFile,
+               nil, nil, nil, 10, 100,
+       )
+       require.NoError(t, err)
+
+       lowerBounds := map[int][]byte{}
+       if lowerBound != nil {
+               lowerBounds[1] = lowerBound
+       }
+       upperBounds := map[int][]byte{}
+       if upperBound != nil {
+               upperBounds[1] = upperBound
+       }
+
+       return builder.
+               ValueCounts(map[int]int64{1: 10}).
+               NullValueCounts(map[int]int64{1: 0}).
+               NaNValueCounts(map[int]int64{1: 0}).
+               LowerBoundValues(lowerBounds).
+               UpperBoundValues(upperBounds).
+               Build()
+}
+
 func TestRemoveBoundCheckSupportsTimestampNanos(t *testing.T) {
        bound := iceberg.NewLiteral(iceberg.TimestampNano(2))
        values := []iceberg.Literal{
diff --git a/visitors.go b/visitors.go
index 896cb2b65..da3c266d9 100644
--- a/visitors.go
+++ b/visitors.go
@@ -83,8 +83,8 @@ type BoundGeospatialExprVisitor[T any] interface {
 }
 
 // boundSetExtremaExprVisitor is consulted only by the borrowed
-// VisitBoundPredicateRef path. The table package's manifest evaluator is
-// currently the only implementation.
+// VisitBoundPredicateRef path. It is used by trusted evaluators that can prune
+// an IN predicate from its precomputed minimum and maximum literals.
 type boundSetExtremaExprVisitor[T any] interface {
        VisitInWithExtrema(BoundTerm, Set[Literal], Literal, Literal) T
 }

Reply via email to