From d5189a4373a831f0685880a5006f516adc0c9c91 Mon Sep 17 00:00:00 2001 From: QuakeWang Date: Wed, 22 Jul 2026 12:38:27 +0800 Subject: [PATCH] [core] Fix unsupported predicate fallback for global indexes Global index readers inherited range and IS NAN visitor defaults that can throw when an index does not support those predicates. Return unsupported results conservatively, preserve wrapper and constant reader behavior, and add regression coverage. Signed-off-by: QuakeWang --- .../ConstantGlobalIndexReader.java | 11 +++ .../paimon/globalindex/GlobalIndexReader.java | 18 +++++ .../globalindex/OffsetGlobalIndexReader.java | 6 ++ .../globalindex/UnionGlobalIndexReader.java | 6 ++ .../globalindex/GlobalIndexEvaluatorTest.java | 69 +++++++++++++++++++ 5 files changed, 110 insertions(+) diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/ConstantGlobalIndexReader.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/ConstantGlobalIndexReader.java index fe610f966840..d30c758fd6d6 100644 --- a/paimon-common/src/main/java/org/apache/paimon/globalindex/ConstantGlobalIndexReader.java +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/ConstantGlobalIndexReader.java @@ -49,6 +49,11 @@ public CompletableFuture> visitIsNull(FieldRef field return result; } + @Override + public CompletableFuture> visitIsNaN(FieldRef fieldRef) { + return result; + } + @Override public CompletableFuture> visitStartsWith( FieldRef fieldRef, Object literal) { @@ -127,6 +132,12 @@ public CompletableFuture> visitBetween( return result; } + @Override + public CompletableFuture> visitNotBetween( + FieldRef fieldRef, Object from, Object to) { + return result; + } + @Override public void close() {} } diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexReader.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexReader.java index 5951d4072665..807ecb8aa08b 100644 --- a/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexReader.java +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/GlobalIndexReader.java @@ -19,6 +19,7 @@ package org.apache.paimon.globalindex; import org.apache.paimon.predicate.BatchVectorSearch; +import org.apache.paimon.predicate.FieldRef; import org.apache.paimon.predicate.FullTextSearch; import org.apache.paimon.predicate.FunctionVisitor; import org.apache.paimon.predicate.LeafPredicate; @@ -34,6 +35,23 @@ public interface GlobalIndexReader extends FunctionVisitor>>, Closeable { + @Override + default CompletableFuture> visitIsNaN(FieldRef fieldRef) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + @Override + default CompletableFuture> visitBetween( + FieldRef fieldRef, Object from, Object to) { + return CompletableFuture.completedFuture(Optional.empty()); + } + + @Override + default CompletableFuture> visitNotBetween( + FieldRef fieldRef, Object from, Object to) { + return CompletableFuture.completedFuture(Optional.empty()); + } + @Override default CompletableFuture> visitAnd( List>> children) { diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/OffsetGlobalIndexReader.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/OffsetGlobalIndexReader.java index 646e1f6ca2eb..7b513a2c3922 100644 --- a/paimon-common/src/main/java/org/apache/paimon/globalindex/OffsetGlobalIndexReader.java +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/OffsetGlobalIndexReader.java @@ -133,6 +133,12 @@ public CompletableFuture> visitBetween( return wrapped.visitBetween(fieldRef, from, to).thenApply(this::applyOffset); } + @Override + public CompletableFuture> visitNotBetween( + FieldRef fieldRef, Object from, Object to) { + return wrapped.visitNotBetween(fieldRef, from, to).thenApply(this::applyOffset); + } + @Override public CompletableFuture> visitVectorSearch( VectorSearch vectorSearch) { diff --git a/paimon-common/src/main/java/org/apache/paimon/globalindex/UnionGlobalIndexReader.java b/paimon-common/src/main/java/org/apache/paimon/globalindex/UnionGlobalIndexReader.java index 57eb372591b9..c901d4905c86 100644 --- a/paimon-common/src/main/java/org/apache/paimon/globalindex/UnionGlobalIndexReader.java +++ b/paimon-common/src/main/java/org/apache/paimon/globalindex/UnionGlobalIndexReader.java @@ -136,6 +136,12 @@ public CompletableFuture> visitBetween( return unionAsync(reader -> reader.visitBetween(fieldRef, from, to)); } + @Override + public CompletableFuture> visitNotBetween( + FieldRef fieldRef, Object from, Object to) { + return unionAsync(reader -> reader.visitNotBetween(fieldRef, from, to)); + } + @Override public CompletableFuture> visitVectorSearch( VectorSearch vectorSearch) { diff --git a/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java b/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java index b2e75a4b6e25..dcd36c8eaa88 100644 --- a/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java +++ b/paimon-common/src/test/java/org/apache/paimon/globalindex/GlobalIndexEvaluatorTest.java @@ -643,6 +643,75 @@ public CompletableFuture> visitIsNotNull( evaluator.close(); } + @Test + void testUnsupportedIsNaNFallsBack() { + RowType rowType = + new RowType(Collections.singletonList(new DataField(0, "a", DataTypes.DOUBLE()))); + GlobalIndexEvaluator evaluator = + new GlobalIndexEvaluator( + rowType, + fieldId -> Collections.singletonList(new StubGlobalIndexReader(null))); + + Optional result = + evaluator.evaluate(new PredicateBuilder(rowType).isNaN(0)); + + assertThat(result).isEmpty(); + evaluator.close(); + } + + @Test + void testNotBetweenThroughUnionAndOffset() { + RowType rowType = rowType(); + GlobalIndexReader delegate = + new StubGlobalIndexReader(null) { + @Override + public CompletableFuture> visitNotBetween( + FieldRef fieldRef, Object from, Object to) { + return CompletableFuture.completedFuture(Optional.of(resultOf(1, 3))); + } + }; + GlobalIndexReader wrapped = + new UnionGlobalIndexReader( + Collections.singletonList(new OffsetGlobalIndexReader(delegate, 10L, 20L))); + GlobalIndexEvaluator evaluator = + new GlobalIndexEvaluator(rowType, fieldId -> Collections.singletonList(wrapped)); + PredicateBuilder builder = new PredicateBuilder(rowType); + + Optional result = + evaluator.evaluate(builder.between(0, 1, 2).negate().get()); + + assertThat(result).isPresent(); + assertBitmapContainsExactly(result.get().results(), 11L, 13L); + evaluator.close(); + } + + @Test + void testWrappedUnsupportedRangePredicatesFallBack() { + RowType rowType = rowType(); + GlobalIndexReader wrapped = + new UnionGlobalIndexReader( + Collections.singletonList( + new OffsetGlobalIndexReader( + new StubGlobalIndexReader(null), 10L, 20L))); + GlobalIndexEvaluator evaluator = + new GlobalIndexEvaluator(rowType, fieldId -> Collections.singletonList(wrapped)); + PredicateBuilder builder = new PredicateBuilder(rowType); + + assertThat(evaluator.evaluate(builder.between(0, 1, 2).negate().get())).isEmpty(); + assertThat(evaluator.evaluate(builder.between(0, 1, 2))).isEmpty(); + evaluator.close(); + } + + @Test + void testConstantReaderReturnsFixedResultForIsNaNAndNotBetween() { + GlobalIndexResult expected = resultOf(1, 2); + GlobalIndexReader reader = new ConstantGlobalIndexReader(expected); + FieldRef fieldRef = new FieldRef(0, "a", DataTypes.DOUBLE()); + + assertThat(reader.visitIsNaN(fieldRef).join()).contains(expected); + assertThat(reader.visitNotBetween(fieldRef, 1, 2).join()).contains(expected); + } + @Test void testNullPredicate() { RowType rowType = rowType();