laskoviymishka commented on code in PR #1911:
URL: https://github.com/apache/iceberg-go/pull/1911#discussion_r3874737623
##########
exprs.go:
##########
@@ -1008,14 +1008,64 @@ func createBoundSetPredicate(op Operation, term
BoundTerm, lits Set[Literal]) (B
ErrType, term.Type())
}
-func newBoundSetPredicate[T LiteralType](op Operation, term BoundTerm, lits
Set[Literal]) *boundSetPredicate[T] {
- return &boundSetPredicate[T]{op: op, term: term, lits: lits}
+func newBoundSetPredicate[T LiteralType](op Operation, term BoundTerm, lits
literalSet) *boundSetPredicate[T] {
+ predicate := &boundSetPredicate[T]{op: op, term: term, lits: lits}
+ if op == OpIn {
+ predicate.minLiteral, predicate.maxLiteral,
predicate.hasExtrema = literalSetExtrema[T](lits)
+ }
+
+ return predicate
}
type boundSetPredicate[T LiteralType] struct {
- op Operation
- term BoundTerm
- lits Set[Literal]
+ op Operation
+ term BoundTerm
+ lits Set[Literal]
+ minLiteral Literal
+ maxLiteral Literal
+ hasExtrema bool
+}
+
+func literalSetExtrema[T LiteralType](lits literalSet) (min, max Literal, ok
bool) {
Review Comment:
`min` and `max` have been builtins since Go 1.21 and the module's on 1.25,
so these named returns shadow them — `golangci-lint`'s `predeclared` will flag
it (same for the `boundSetExtremaRef` signature in `bound_set_literal_view.go`
and the destructuring in `visitors.go`). I'd rename to `minLit`/`maxLit` to
match the `minLiteral`/`maxLiteral` struct fields, consistently across all
three sites.
##########
exprs.go:
##########
@@ -1031,8 +1081,12 @@ func (bsp *boundSetPredicate[T]) Equals(other
BooleanExpression) bool {
func (bsp *boundSetPredicate[T]) Op() Operation { return bsp.op }
func (bsp *boundSetPredicate[T]) Negate() BooleanExpression {
return &boundSetPredicate[T]{
- op: bsp.op.Negate(), term: bsp.term,
- lits: bsp.lits,
+ op: bsp.op.Negate(),
+ term: bsp.term,
Review Comment:
These extrema get carried onto the `NOT IN` result, but the dispatch in
`visitBoundPredicate` only consults them on `OpIn`, so for `OpNotIn` they're
stored and never read — 24 bytes per negated predicate for nothing. I'd set
`hasExtrema: false` here. (Minor gotcha the other way: a double-negate back to
`OpIn` loses them, since `Negate` doesn't recompute.)
##########
table/evaluators.go:
##########
@@ -162,7 +164,53 @@ func allBoundCheck(bound iceberg.Literal, set
iceberg.Set[iceberg.Literal], want
panic(iceberg.ErrType)
}
+func compareBound[T iceberg.LiteralType](bound, value iceberg.Literal) int {
+ val := bound.(iceberg.TypedLiteral[T])
+
+ return val.Comparator()(val.Value(),
value.(iceberg.TypedLiteral[T]).Value())
+}
+
+func boundCompare(bound, value iceberg.Literal) int {
Review Comment:
`boundCompare` is a second copy of the type dispatch `getCmpLiteral` already
does — `boundCompare(a, b)` is just `getCmpLiteral(a)(a, b)`. The only reason
it exists is that `getCmpLiteral` is missing the `TimestampNano` case, so
rather than adding it there the PR routes around it with a parallel switch.
The catch is that leaves the root gap in place: `getCmpLiteral` still panics
on nanosecond-timestamp columns, so `VisitEqual`/`VisitGreater`/etc. still blow
up for a TimestampNs partition column even though the IN path now handles it.
`removeBoundCheck` has the same hole for the inclusive/strict metrics IN paths.
So we've added TimestampNs coverage to three dispatchers but not the two that
share the same shape.
I'd add the `TimestampNano` case to `getCmpLiteral` (and
`removeBoundCheck`), then collapse `boundCompare` to
`getCmpLiteral(bound)(bound, value)` and drop `compareBound` — that finishes
the TimestampNano fix everywhere and removes ~28 lines of parallel dispatch.
wdyt?
##########
table/evaluators.go:
##########
@@ -179,7 +227,11 @@ func (m *manifestEvalVisitor) VisitIn(term
iceberg.BoundTerm, literals iceberg.S
panic(err)
}
- if allBoundCheck(lower, literals, 1) {
+ if max != nil {
Review Comment:
I think the extrema check never runs for the sets this is meant to speed up.
The `literals.Len() > inPredicateLimit` guard above still fires first and
returns `rowsMightMatch`, so any IN set past 200 values short-circuits before
we reach `if max != nil` — and extrema get computed at bind time for every set
size regardless. The O(1) win is exactly inert for the large predicates that
motivated the change.
That guard only makes sense for the O(n) `allBoundCheck` fallback. I'd move
it into the `else` arm so the extrema comparison runs unconditionally, and we
only fall through to the length guard + `allBoundCheck` when extrema aren't
available (`max == nil`).
The benchmark tops out at exactly 200 literals, right at the cutoff, so this
regime never gets exercised — a 201+ case with a disjoint partition range would
return `rowsMightMatch` where we'd want `rowsCannotMatch`. wdyt?
##########
table/in_predicate_extrema_test.go:
##########
@@ -0,0 +1,114 @@
+// 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 (
+ "testing"
+
+ "github.com/apache/arrow-go/v18/arrow/decimal128"
+ "github.com/apache/iceberg-go"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestManifestEvaluatorInPredicateExtrema(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
+ lower iceberg.Literal
+ upper iceberg.Literal
+ expectRead bool
+ }{
+ {
+ name: "decimal below lower bound",
+ typ: iceberg.DecimalTypeOf(12, 2),
+ expr: iceberg.IsIn(iceberg.Reference("value"),
decimal(100), decimal(200), decimal(300)),
+ lower: iceberg.NewLiteral(decimal(400)),
+ upper: iceberg.NewLiteral(decimal(500)),
+ expectRead: false,
+ },
+ {
+ name: "decimal above upper bound",
+ typ: iceberg.DecimalTypeOf(12, 2),
+ expr: iceberg.IsIn(iceberg.Reference("value"),
decimal(100), decimal(200), decimal(300)),
+ lower: iceberg.NewLiteral(decimal(-100)),
+ upper: iceberg.NewLiteral(decimal(0)),
+ expectRead: false,
+ },
+ {
+ name: "decimal overlaps bound",
+ typ: iceberg.DecimalTypeOf(12, 2),
+ expr: iceberg.IsIn(iceberg.Reference("value"),
decimal(100), decimal(200), decimal(300)),
+ lower: iceberg.NewLiteral(decimal(200)),
+ upper: iceberg.NewLiteral(decimal(250)),
+ expectRead: true,
+ },
+ {
+ name: "timestamp nanos below lower bound",
+ typ: iceberg.PrimitiveTypes.TimestampNs,
+ expr: iceberg.IsIn(iceberg.Reference("value"),
iceberg.TimestampNano(100), iceberg.TimestampNano(200),
iceberg.TimestampNano(300)),
+ lower:
iceberg.NewLiteral(iceberg.TimestampNano(400)),
+ upper:
iceberg.NewLiteral(iceberg.TimestampNano(500)),
+ expectRead: false,
+ },
+ {
+ name: "timestamp nanos above upper bound",
+ typ: iceberg.PrimitiveTypes.TimestampNs,
+ expr: iceberg.IsIn(iceberg.Reference("value"),
iceberg.TimestampNano(100), iceberg.TimestampNano(200),
iceberg.TimestampNano(300)),
+ lower:
iceberg.NewLiteral(iceberg.TimestampNano(-100)),
+ upper:
iceberg.NewLiteral(iceberg.TimestampNano(99)),
+ expectRead: false,
+ },
+ {
+ name: "timestamp nanos overlaps bound",
+ typ: iceberg.PrimitiveTypes.TimestampNs,
+ expr: iceberg.IsIn(iceberg.Reference("value"),
iceberg.TimestampNano(100), iceberg.TimestampNano(200),
iceberg.TimestampNano(300)),
+ lower:
iceberg.NewLiteral(iceberg.TimestampNano(200)),
+ upper:
iceberg.NewLiteral(iceberg.TimestampNano(250)),
+ expectRead: true,
+ },
Review Comment:
Could we add a case with 201+ literals and a clearly disjoint partition
range here? That's the one that would catch the `inPredicateLimit` ordering
issue — today it returns `rowsMightMatch` (read) where the extrema say
`rowsCannotMatch` (prune). A boundary case where `lower == max` (expecting a
read) would lock the `== 1` edge too.
##########
table/evaluators.go:
##########
@@ -179,7 +227,11 @@ func (m *manifestEvalVisitor) VisitIn(term
iceberg.BoundTerm, literals iceberg.S
panic(err)
}
- if allBoundCheck(lower, literals, 1) {
+ if max != nil {
+ if boundCompare(lower, max) == 1 {
Review Comment:
The `Comparator` contract in `literals.go` is defined as `< 0` / `> 0`, not
`±1`, so checking `== 1` here (and `== -1` on the upper bound) is a little
fragile — a conforming comparator that returns 2 would silently skip the prune
and we'd read the manifest anyway. I'd use `boundCompare(lower, max) > 0` and
`boundCompare(upper, min) < 0`.
There are pre-existing `== 1`/`== -1` sites elsewhere in this file so this
isn't new, but since we're adding two more I'd rather not extend the pattern.
##########
visitors.go:
##########
@@ -82,6 +82,10 @@ type BoundGeospatialExprVisitor[T any] interface {
VisitBBoxNotIntersects(BoundTerm, BoundingBox) T
}
+type boundSetExtremaExprVisitor[T any] interface {
Review Comment:
Worth a doc comment here: `VisitInWithExtrema` only gets called on the
`borrowed` path in `visitBoundPredicate`, so a visitor that implements it but
is driven through the public `VisitBoundPredicate` will silently never see it.
The geospatial extension interface right above documents its dispatch contract
explicitly; I'd mirror that.
Related, only `manifestEvalVisitor` implements it today — the other three
evaluators now go through `VisitBoundPredicateRef` and fall back to `VisitIn`,
so inclusive/strict metrics and row-group filtering still scan every literal.
Fine to leave for a follow-up, but a note here keeps it from reading as if all
four are optimized.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]