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 aff45ee17 fix: make ExpressionEvaluator ordering comparisons false for 
NaN (#2418)
aff45ee17 is described below

commit aff45ee17cd76c69f7847622913de5c066684651
Author: Revanth <[email protected]>
AuthorDate: Wed Nov 4 11:18:38 2026 -0600

    fix: make ExpressionEvaluator ordering comparisons false for NaN (#2418)
---
 table/expression_evaluator_nan_internal_test.go | 111 ++++++++++++++++++++++++
 visitors.go                                     |  12 ++-
 2 files changed, 119 insertions(+), 4 deletions(-)

diff --git a/table/expression_evaluator_nan_internal_test.go 
b/table/expression_evaluator_nan_internal_test.go
new file mode 100644
index 000000000..ff9ed2e7f
--- /dev/null
+++ b/table/expression_evaluator_nan_internal_test.go
@@ -0,0 +1,111 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package table
+
+import (
+       "context"
+       "math"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/iceberg-go"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+// TestExpressionEvaluatorMatchesRowFilterForNaN checks that the exported
+// ExpressionEvaluator selects the same rows as the Arrow row filter a scan
+// applies when the value is NaN. NULL rows are left out: the evaluator is
+// two-valued and orders NULL below every value, while the row filter treats a
+// comparison with NULL as unknown.
+func TestExpressionEvaluatorMatchesRowFilterForNaN(t *testing.T) {
+       sc := iceberg.NewSchema(1,
+               iceberg.NestedField{ID: 1, Name: "row", Type: 
iceberg.PrimitiveTypes.Int32, Required: true},
+               iceberg.NestedField{ID: 2, Name: "d", Type: 
iceberg.PrimitiveTypes.Float64, Required: true},
+               iceberg.NestedField{ID: 3, Name: "f", Type: 
iceberg.PrimitiveTypes.Float32, Required: true},
+       )
+       values := []float64{math.Inf(-1), 1, 5, 10, math.Inf(1), math.NaN()}
+
+       arrowSchema, err := SchemaToArrowSchema(sc, nil, false, false)
+       require.NoError(t, err)
+       bldr := array.NewRecordBuilder(memory.DefaultAllocator, arrowSchema)
+       defer bldr.Release()
+       rows := make([]partitionRecord, len(values))
+       for i, v := range values {
+               bldr.Field(0).(*array.Int32Builder).Append(int32(i))
+               bldr.Field(1).(*array.Float64Builder).Append(v)
+               bldr.Field(2).(*array.Float32Builder).Append(float32(v))
+               rows[i] = partitionRecord{int32(i), v, float32(v)}
+       }
+       rec := bldr.NewRecordBatch()
+       defer rec.Release()
+
+       var exprs []iceberg.BooleanExpression
+       for _, col := range []string{"d", "f"} {
+               ref := iceberg.Reference(col)
+               for _, pred := range []iceberg.BooleanExpression{
+                       iceberg.LessThan(ref, 5.0),
+                       iceberg.LessThanEqual(ref, 5.0),
+                       iceberg.GreaterThan(ref, 5.0),
+                       iceberg.GreaterThanEqual(ref, 5.0),
+                       iceberg.EqualTo(ref, 5.0),
+                       iceberg.NotEqualTo(ref, 5.0),
+               } {
+                       exprs = append(exprs, pred, iceberg.NewNot(pred))
+               }
+       }
+
+       for _, expr := range exprs {
+               t.Run(expr.String(), func(t *testing.T) {
+                       filter, err := prepareBatchFilter(expr, sc, true)
+                       require.NoError(t, err)
+                       rec.Retain()
+                       filtered, err := filter(context.Background(), rec)
+                       require.NoError(t, err)
+                       defer filtered.Release()
+                       want := filteredRowIDs(t, filtered)
+
+                       eval, err := iceberg.ExpressionEvaluator(sc, expr, true)
+                       require.NoError(t, err)
+                       got := []int32{}
+                       for _, row := range rows {
+                               ok, err := eval(row)
+                               require.NoError(t, err)
+                               if ok {
+                                       got = append(got, row[0].(int32))
+                               }
+                       }
+
+                       assert.Equal(t, want, got)
+               })
+       }
+}
+
+func filteredRowIDs(t *testing.T, rec arrow.RecordBatch) []int32 {
+       t.Helper()
+
+       ids := rec.Column(0).(*array.Int32)
+       out := make([]int32, ids.Len())
+       for i := range out {
+               out[i] = ids.Value(i)
+       }
+
+       return out
+}
diff --git a/visitors.go b/visitors.go
index ea4cd2943..27c1d4b7a 100644
--- a/visitors.go
+++ b/visitors.go
@@ -497,20 +497,24 @@ func (e *exprEvaluator) VisitNotEqual(term BoundTerm, lit 
Literal) bool {
        return doCmp(e.st, term, lit) != 0
 }
 
+// The ordering comparisons are false for a NaN value, following IEEE 754 as
+// the Arrow row filter does. doCmp can't decide them alone, because
+// cmp.Compare orders NaN below every number.
+
 func (e *exprEvaluator) VisitGreater(term BoundTerm, lit Literal) bool {
-       return doCmp(e.st, term, lit) > 0
+       return !e.VisitIsNan(term) && doCmp(e.st, term, lit) > 0
 }
 
 func (e *exprEvaluator) VisitGreaterEqual(term BoundTerm, lit Literal) bool {
-       return doCmp(e.st, term, lit) >= 0
+       return !e.VisitIsNan(term) && doCmp(e.st, term, lit) >= 0
 }
 
 func (e *exprEvaluator) VisitLess(term BoundTerm, lit Literal) bool {
-       return doCmp(e.st, term, lit) < 0
+       return !e.VisitIsNan(term) && doCmp(e.st, term, lit) < 0
 }
 
 func (e *exprEvaluator) VisitLessEqual(term BoundTerm, lit Literal) bool {
-       return doCmp(e.st, term, lit) <= 0
+       return !e.VisitIsNan(term) && doCmp(e.st, term, lit) <= 0
 }
 
 func (e *exprEvaluator) VisitStartsWith(term BoundTerm, lit Literal) bool {

Reply via email to