This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 4ac91ffa57 [flink] Build balanced binary tree for OR/AND predicates to
avoid StackOverflowError (#8696)
4ac91ffa57 is described below
commit 4ac91ffa578c238ec25a5d4b17f45dc1cbc8d7b8
Author: yunfengzhou-hub <[email protected]>
AuthorDate: Thu Jul 16 22:08:29 2026 +0800
[flink] Build balanced binary tree for OR/AND predicates to avoid
StackOverflowError (#8696)
---
.../apache/paimon/predicate/PredicateBuilder.java | 23 ++-
.../paimon/predicate/PredicateBuilderTest.java | 78 +++++++++
.../paimon/predicate/PredicateJsonSerdeTest.java | 6 +-
.../org/apache/paimon/predicate/PredicateTest.java | 4 +-
.../apache/paimon/flink/PredicateConverter.java | 41 ++++-
.../paimon/flink/PredicateConverterTest.java | 66 ++++++++
.../paimon/flink/source/FlinkTableSourceTest.java | 185 +++++++++++++++++++++
.../format/orc/filter/OrcFilterConverterTest.java | 32 ++--
.../paimon/format/parquet/ParquetFiltersTest.java | 38 ++---
9 files changed, 424 insertions(+), 49 deletions(-)
diff --git
a/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateBuilder.java
b/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateBuilder.java
index 05acce1729..b26baa2511 100644
---
a/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateBuilder.java
+++
b/paimon-common/src/main/java/org/apache/paimon/predicate/PredicateBuilder.java
@@ -304,9 +304,7 @@ public class PredicateBuilder {
return optimized.get(0);
}
- return optimized.stream()
- .reduce((a, b) -> new CompoundPredicate(And.INSTANCE,
Arrays.asList(a, b)))
- .get();
+ return buildBinaryTree(And.INSTANCE, optimized);
}
@Nullable
@@ -359,9 +357,22 @@ public class PredicateBuilder {
return noFalsePredicates.get(0);
}
- return noFalsePredicates.stream()
- .reduce((a, b) -> new CompoundPredicate(Or.INSTANCE,
Arrays.asList(a, b)))
- .get();
+ return buildBinaryTree(Or.INSTANCE, noFalsePredicates);
+ }
+
+ private static Predicate buildBinaryTree(CompoundFunction func,
List<Predicate> predicates) {
+ if (predicates.size() == 1) {
+ return predicates.get(0);
+ }
+ if (predicates.size() == 2) {
+ return new CompoundPredicate(func,
Arrays.asList(predicates.get(0), predicates.get(1)));
+ }
+ int mid = predicates.size() / 2;
+ return new CompoundPredicate(
+ func,
+ Arrays.asList(
+ buildBinaryTree(func, predicates.subList(0, mid)),
+ buildBinaryTree(func, predicates.subList(mid,
predicates.size()))));
}
private static boolean isAlwaysFalse(Predicate predicate) {
diff --git
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateBuilderTest.java
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateBuilderTest.java
index 4b6ac01626..fe66f95241 100644
---
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateBuilderTest.java
+++
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateBuilderTest.java
@@ -39,6 +39,9 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
import static org.apache.paimon.predicate.SimpleColStatsTestUtils.test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -217,6 +220,81 @@ public class PredicateBuilderTest {
assertThat(predicate.test(GenericRow.of(10))).isEqualTo(false);
}
+ // ---- or()/and() binary tree structure tests ----
+
+ @Test
+ public void testOrBinaryTree() {
+ PredicateBuilder builder = new PredicateBuilder(RowType.of(new
IntType()));
+ List<Predicate> predicates =
+ IntStream.range(0, 25)
+ .mapToObj(i -> builder.equal(0, i))
+ .collect(Collectors.toList());
+
+ Predicate result = PredicateBuilder.or(predicates);
+
+ assertThat(result).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate root = (CompoundPredicate) result;
+ assertThat(root.function()).isEqualTo(Or.INSTANCE);
+ assertThat(root.children()).hasSize(2);
+ assertThat(countLeaves(root)).isEqualTo(25);
+ assertThat(maxDepth(root)).isLessThanOrEqualTo(5);
+ }
+
+ @Test
+ public void testOrBinaryTreeEvaluation() {
+ PredicateBuilder builder = new PredicateBuilder(RowType.of(new
IntType()));
+ List<Predicate> predicates =
+ IntStream.range(0, 25)
+ .mapToObj(i -> builder.equal(0, i))
+ .collect(Collectors.toList());
+
+ Predicate result = PredicateBuilder.or(predicates);
+
+ assertThat(result.test(GenericRow.of(0))).isTrue();
+ assertThat(result.test(GenericRow.of(24))).isTrue();
+ assertThat(result.test(GenericRow.of(25))).isFalse();
+ }
+
+ @Test
+ public void testAndBinaryTree() {
+ PredicateBuilder builder = new PredicateBuilder(RowType.of(new
IntType()));
+ List<Predicate> predicates =
+ IntStream.range(0, 25)
+ .mapToObj(i -> builder.greaterThan(0, i))
+ .collect(Collectors.toList());
+
+ Predicate result = PredicateBuilder.and(predicates);
+
+ assertThat(result).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate root = (CompoundPredicate) result;
+ assertThat(root.function()).isEqualTo(And.INSTANCE);
+ assertThat(root.children()).hasSize(2);
+ assertThat(countLeaves(root)).isEqualTo(25);
+ assertThat(maxDepth(root)).isLessThanOrEqualTo(5);
+ }
+
+ private static int countLeaves(Predicate predicate) {
+ if (!(predicate instanceof CompoundPredicate)) {
+ return 1;
+ }
+ int count = 0;
+ for (Predicate child : ((CompoundPredicate) predicate).children()) {
+ count += countLeaves(child);
+ }
+ return count;
+ }
+
+ private static int maxDepth(Predicate predicate) {
+ if (!(predicate instanceof CompoundPredicate)) {
+ return 0;
+ }
+ int max = 0;
+ for (Predicate child : ((CompoundPredicate) predicate).children()) {
+ max = Math.max(max, maxDepth(child));
+ }
+ return 1 + max;
+ }
+
@Test
public void testConvertToJavaObjectRoundTrip() {
// VARCHAR
diff --git
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
index 66f0b1f4d1..4837b9863f 100644
---
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
+++
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateJsonSerdeTest.java
@@ -87,12 +87,12 @@ class PredicateJsonSerdeTest {
// LeafPredicate - In
TestSpec.forPredicate(builder.in(0, Arrays.asList(1, 2, 3)))
.expectJson(
-
"{\"kind\":\"COMPOUND\",\"function\":\"OR\",\"children\":[{\"kind\":\"COMPOUND\",\"function\":\"OR\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"EQUAL\",\"literals\":[1]},{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"EQUAL\",\"literals\":[2]}]},{\"kind\":\"LEAF\",\"tran
[...]
+
"{\"kind\":\"COMPOUND\",\"function\":\"OR\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"EQUAL\",\"literals\":[1]},{\"kind\":\"COMPOUND\",\"function\":\"OR\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"EQUAL\",\"literals\":[2]},{\"kind\":\"LEAF\",\"transf
[...]
// LeafPredicate - NotIn
TestSpec.forPredicate(builder.notIn(0, Arrays.asList(1, 2, 3)))
.expectJson(
-
"{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"NOT_EQUAL\",\"literals\":[1]},{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"NOT_EQUAL\",\"literals\":[2]}]},{\"kind\":\"LEA
[...]
+
"{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"NOT_EQUAL\",\"literals\":[1]},{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"NOT_EQUAL\",\"literals\":[2]},{\"kind\":\"LEAF\
[...]
// LeafPredicate - CastTransform
TestSpec.forPredicate(
@@ -211,7 +211,7 @@ class PredicateJsonSerdeTest {
PredicateBuilder.or(
builder.equal(0, 7),
builder.isNotNull(2))))
.expectJson(
-
"{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"EQUAL\",\"literals\":[1]},{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\
[...]
+
"{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"COMPOUND\",\"function\":\"AND\",\"children\":[{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":0,\"name\":\"f0\",\"type\":\"INT\"}},\"function\":\"EQUAL\",\"literals\":[1]},{\"kind\":\"LEAF\",\"transform\":{\"name\":\"FIELD_REF\",\"fieldRef\":{\"index\":3,\"name\":\"f3\",\"type\":\"INT\"}},\"function\":\"IN\",\"literals\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,
[...]
// error message testing
TestSpec.forJson("{\"kind\":\"invalid\"}")
diff --git
a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateTest.java
b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateTest.java
index a33112d340..f09c263429 100644
--- a/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateTest.java
+++ b/paimon-common/src/test/java/org/apache/paimon/predicate/PredicateTest.java
@@ -736,13 +736,13 @@ public class PredicateTest {
Predicate p6 = builder6.in(0, Arrays.asList(1, null, 3, 4));
assertThat(p6.toString())
.isEqualTo(
- "Or([Or([Or([Equal(f0, 1), Equal(f0, null)]),
Equal(f0, 3)]), Equal(f0, 4)])");
+ "Or([Or([Equal(f0, 1), Equal(f0, null)]),
Or([Equal(f0, 3), Equal(f0, 4)])])");
PredicateBuilder builder7 = new PredicateBuilder(RowType.of(new
IntType()));
Predicate p7 = builder7.notIn(0, Arrays.asList(1, null, 3, 4));
assertThat(p7.toString())
.isEqualTo(
- "And([And([And([NotEqual(f0, 1), NotEqual(f0, null)]),
NotEqual(f0, 3)]), NotEqual(f0, 4)])");
+ "And([And([NotEqual(f0, 1), NotEqual(f0, null)]),
And([NotEqual(f0, 3), NotEqual(f0, 4)])])");
PredicateBuilder builder8 = new PredicateBuilder(RowType.of(new
IntType()));
List<Object> literals = new ArrayList<>();
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
index a91b2720a3..d5413f0859 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
@@ -38,7 +38,9 @@ import org.apache.flink.table.types.logical.LogicalType;
import org.apache.flink.table.types.logical.LogicalTypeFamily;
import org.apache.flink.table.types.logical.RowType;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -76,9 +78,9 @@ public class PredicateConverter implements
ExpressionVisitor<Predicate> {
List<Expression> children = call.getChildren();
if (func == BuiltInFunctionDefinitions.AND) {
- return PredicateBuilder.and(children.get(0).accept(this),
children.get(1).accept(this));
+ return PredicateBuilder.and(flattenAndConvert(children, func));
} else if (func == BuiltInFunctionDefinitions.OR) {
- return PredicateBuilder.or(children.get(0).accept(this),
children.get(1).accept(this));
+ return PredicateBuilder.or(flattenAndConvert(children, func));
} else if (func == BuiltInFunctionDefinitions.EQUALS) {
return visitBiFunction(children, builder::equal, builder::equal);
} else if (func == BuiltInFunctionDefinitions.NOT_EQUALS) {
@@ -201,6 +203,41 @@ public class PredicateConverter implements
ExpressionVisitor<Predicate> {
throw new UnsupportedExpression();
}
+ /**
+ * Iteratively flattens a nested AND/OR expression tree into a flat list
of child predicates,
+ * avoiding stack overflow caused by recursive {@code accept} calls on
deeply nested trees (e.g.
+ * when Flink expands a large IN clause into nested OR expressions).
+ *
+ * @param children the children of the top-level AND/OR {@link
CallExpression}
+ * @param targetFunc the function definition to flatten ({@code AND} or
{@code OR})
+ * @return a flat list of converted child predicates in original order
+ */
+ private List<Predicate> flattenAndConvert(
+ List<Expression> children, FunctionDefinition targetFunc) {
+ List<Predicate> result = new ArrayList<>();
+ Deque<Expression> stack = new ArrayDeque<>();
+ for (int i = children.size() - 1; i >= 0; i--) {
+ stack.push(children.get(i));
+ }
+ while (!stack.isEmpty()) {
+ Expression expr = stack.pop();
+ if (expr instanceof CallExpression) {
+ CallExpression ce = (CallExpression) expr;
+ if (ce.getFunctionDefinition() == targetFunc) {
+ List<Expression> ceChildren = ce.getChildren();
+ for (int i = ceChildren.size() - 1; i >= 0; i--) {
+ stack.push(ceChildren.get(i));
+ }
+ } else {
+ result.add(ce.accept(this));
+ }
+ } else {
+ result.add(expr.accept(this));
+ }
+ }
+ return result;
+ }
+
private Predicate visitBiFunction(
List<Expression> children,
BiFunction<Integer, Object, Predicate> visit1,
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
index c304a70434..d86293277e 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
@@ -21,6 +21,8 @@ package org.apache.paimon.flink;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.format.SimpleColStats;
+import org.apache.paimon.predicate.CompoundPredicate;
+import org.apache.paimon.predicate.Or;
import org.apache.paimon.predicate.Predicate;
import org.apache.paimon.predicate.PredicateBuilder;
import org.apache.paimon.predicate.SimpleColStatsTestUtils;
@@ -808,6 +810,70 @@ public class PredicateConverterTest {
.isInstanceOf(PredicateConverter.UnsupportedExpression.class);
}
+ // ==================== Nested OR Conversion Tests ====================
+ //
+ // When Flink expands IN(v1,...,vN) it produces a deeply nested binary OR
tree:
+ // OR(=(f,v1), OR(=(f,v2), OR(..., =(f,vN))))
+ // PredicateConverter iteratively flattens this tree into a list of
predicates,
+ // which PredicateBuilder.or() combines into a binary tree.
+
+ @Test
+ public void testNestedOrOfEquals() {
+ // Build OR(=(long1, 0), OR(=(long1, 1), ...)) with 25 values
+ FieldReferenceExpression longRef =
+ new FieldReferenceExpression(
+ "long1", DataTypes.BIGINT(), Integer.MAX_VALUE,
Integer.MAX_VALUE);
+
+ ResolvedExpression orTree = null;
+ for (int i = 24; i >= 0; i--) {
+ CallExpression equal =
+ call(BuiltInFunctionDefinitions.EQUALS, longRef, new
ValueLiteralExpression(i));
+ if (orTree == null) {
+ orTree = equal;
+ } else {
+ orTree = call(BuiltInFunctionDefinitions.OR, equal, orTree);
+ }
+ }
+
+ Predicate result = CONVERTER.visit((CallExpression) orTree);
+
+ // OR-of-equals → flattened → PredicateBuilder.or() → binary tree
+ assertThat(result).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate compound = (CompoundPredicate) result;
+ assertThat(compound.function()).isEqualTo(Or.INSTANCE);
+ assertThat(compound.children()).hasSize(2);
+ }
+
+ @Test
+ public void testNestedOrOfDifferentPredicates() {
+ // Build OR(>(long1, 0), OR(>(long1, 1), ...)) with 25 values
+ FieldReferenceExpression longRef =
+ new FieldReferenceExpression(
+ "long1", DataTypes.BIGINT(), Integer.MAX_VALUE,
Integer.MAX_VALUE);
+
+ ResolvedExpression orTree = null;
+ for (int i = 24; i >= 0; i--) {
+ CallExpression greater =
+ call(
+ BuiltInFunctionDefinitions.GREATER_THAN,
+ longRef,
+ new ValueLiteralExpression(i));
+ if (orTree == null) {
+ orTree = greater;
+ } else {
+ orTree = call(BuiltInFunctionDefinitions.OR, greater, orTree);
+ }
+ }
+
+ Predicate result = CONVERTER.visit((CallExpression) orTree);
+
+ // General OR → binary tree (root has 2 children)
+ assertThat(result).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate compound = (CompoundPredicate) result;
+ assertThat(compound.function()).isEqualTo(Or.INSTANCE);
+ assertThat(compound.children()).hasSize(2);
+ }
+
private static FieldReferenceExpression field(int i, DataType type) {
return new FieldReferenceExpression("f" + i, type, Integer.MAX_VALUE,
Integer.MAX_VALUE);
}
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
index 4965cc4135..ba7dbd0893 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
@@ -21,6 +21,8 @@ package org.apache.paimon.flink.source;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.predicate.CompoundPredicate;
+import org.apache.paimon.predicate.Or;
import org.apache.paimon.schema.Schema;
import org.apache.paimon.schema.SchemaManager;
import org.apache.paimon.schema.TableSchema;
@@ -40,6 +42,7 @@ import
org.apache.flink.table.functions.BuiltInFunctionDefinitions;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.ArrayList;
import java.util.List;
/** Test for {@link FlinkTableSource}. */
@@ -136,6 +139,188 @@ public class FlinkTableSourceTest extends TableTestBase {
.isEqualTo(ImmutableList.of(filters.get(1)));
}
+ // ==================== Nested OR Tree Tests ====================
+ //
+ // These tests construct OR trees in various shapes — mimicking what
Flink's
+ // SQL Planner may produce when expanding IN(v1,...,vN) — and pass them
directly
+ // to applyFilters, bypassing Flink's ExpressionResolver.
+ //
+ // They verify that PredicateConverter flattens any nesting shape into a
flat list
+ // of predicates, which PredicateBuilder.or() combines into a binary tree.
+
+ @Test
+ public void testApplyFiltersLargeNestedOr() throws Exception {
+ Table table = createStringTable();
+
+ // 10000 values: the nested OR tree is flattened and combined into a
+ // binary tree (depth ~14), preventing StackOverflowError.
+ int size = 10000;
+ DataTableSource tableSource =
+ new DataTableSource(
+ ObjectIdentifier.of("catalog1", "db1", "T"), table,
false, null);
+ ResolvedExpression orTree = buildNestedOrTree(size);
+
+ tableSource.applyFilters(ImmutableList.of(orTree));
+
+ Assertions.assertThat(tableSource.predicate).isNotNull();
+
Assertions.assertThat(tableSource.predicate).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate compound = (CompoundPredicate) tableSource.predicate;
+ Assertions.assertThat(compound.function()).isEqualTo(Or.INSTANCE);
+ Assertions.assertThat(compound.children()).hasSize(2);
+ }
+
+ @Test
+ public void testApplyFiltersRightFoldOrTree() throws Exception {
+ Table table = createStringTable();
+ DataTableSource tableSource =
+ new DataTableSource(
+ ObjectIdentifier.of("catalog1", "db1", "T"), table,
false, null);
+
+ // Right-fold tree: OR(OR(OR(=(f,0), =(f,1)), =(f,2)), ...) with 25
values
+ ResolvedExpression orTree = buildRightFoldOrTree(25);
+
+ tableSource.applyFilters(ImmutableList.of(orTree));
+
+ // Regardless of tree shape → flattened → binary tree
+ Assertions.assertThat(tableSource.predicate).isNotNull();
+
Assertions.assertThat(tableSource.predicate).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate compound = (CompoundPredicate) tableSource.predicate;
+ Assertions.assertThat(compound.function()).isEqualTo(Or.INSTANCE);
+ Assertions.assertThat(compound.children()).hasSize(2);
+ }
+
+ @Test
+ public void testApplyFiltersBalancedOrTree() throws Exception {
+ Table table = createStringTable();
+ DataTableSource tableSource =
+ new DataTableSource(
+ ObjectIdentifier.of("catalog1", "db1", "T"), table,
false, null);
+
+ // Balanced tree: OR(OR(=(f,0), =(f,1)), OR(=(f,2), =(f,3)), ...) with
25 values
+ ResolvedExpression orTree = buildBalancedOrTree(25);
+
+ tableSource.applyFilters(ImmutableList.of(orTree));
+
+ Assertions.assertThat(tableSource.predicate).isNotNull();
+
Assertions.assertThat(tableSource.predicate).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate compound = (CompoundPredicate) tableSource.predicate;
+ Assertions.assertThat(compound.function()).isEqualTo(Or.INSTANCE);
+ Assertions.assertThat(compound.children()).hasSize(2);
+ }
+
+ @Test
+ public void testApplyFiltersFlatOrWithMultipleChildren() throws Exception {
+ Table table = createStringTable();
+ DataTableSource tableSource =
+ new DataTableSource(
+ ObjectIdentifier.of("catalog1", "db1", "T"), table,
false, null);
+
+ // Flat OR with >2 children in a single CallExpression
+ ResolvedExpression orExpr = buildFlatOrExpression(25);
+
+ tableSource.applyFilters(ImmutableList.of(orExpr));
+
+ Assertions.assertThat(tableSource.predicate).isNotNull();
+
Assertions.assertThat(tableSource.predicate).isInstanceOf(CompoundPredicate.class);
+ CompoundPredicate compound = (CompoundPredicate) tableSource.predicate;
+ Assertions.assertThat(compound.function()).isEqualTo(Or.INSTANCE);
+ Assertions.assertThat(compound.children()).hasSize(2);
+ }
+
+ private Table createStringTable() throws Exception {
+ FileIO fileIO = LocalFileIO.create();
+ Path tablePath = new Path(String.format("%s/%s.db/%s", warehouse,
database, "T"));
+ Schema schema = Schema.newBuilder().column("contract_address",
DataTypes.STRING()).build();
+ TableSchema tableSchema = new SchemaManager(fileIO,
tablePath).createTable(schema);
+ return FileStoreTableFactory.create(LocalFileIO.create(), tablePath,
tableSchema);
+ }
+
+ /**
+ * Build a nested binary OR tree mimicking Flink's IN-to-OR expansion:
OR(=(f, v1), OR(=(f, v2),
+ * OR(..., OR(=(f, vN-1), =(f, vN)))))
+ *
+ * <p>Built iteratively (inside-out) to avoid StackOverflow during
construction.
+ */
+ private ResolvedExpression buildNestedOrTree(int count) {
+ FieldReferenceExpression field =
+ new FieldReferenceExpression(
+ "contract_address",
org.apache.flink.table.api.DataTypes.STRING(), 0, 0);
+
+ // Start with innermost: =(contract_address, addr_{count-1})
+ ResolvedExpression result = equalExpr(field, count - 1);
+
+ // Wrap outward: OR(=(field, addr_i), result) for i = count-2 down to 0
+ for (int i = count - 2; i >= 0; i--) {
+ result = or(equalExpr(field, i), result);
+ }
+ return result;
+ }
+
+ /**
+ * Build a right-fold binary OR tree: OR(OR(OR(=(f, v0), =(f, v1)), =(f,
v2)), =(f, v3), ...).
+ */
+ private ResolvedExpression buildRightFoldOrTree(int count) {
+ FieldReferenceExpression field =
+ new FieldReferenceExpression(
+ "contract_address",
org.apache.flink.table.api.DataTypes.STRING(), 0, 0);
+ ResolvedExpression result = equalExpr(field, 0);
+ for (int i = 1; i < count; i++) {
+ result = or(result, equalExpr(field, i));
+ }
+ return result;
+ }
+
+ /** Build a balanced binary OR tree: OR(OR(=(f, v0), =(f, v1)), OR(=(f,
v2), =(f, v3)), ...). */
+ private ResolvedExpression buildBalancedOrTree(int count) {
+ FieldReferenceExpression field =
+ new FieldReferenceExpression(
+ "contract_address",
org.apache.flink.table.api.DataTypes.STRING(), 0, 0);
+ List<ResolvedExpression> leaves = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ leaves.add(equalExpr(field, i));
+ }
+ while (leaves.size() > 1) {
+ List<ResolvedExpression> next = new ArrayList<>();
+ for (int i = 0; i < leaves.size(); i += 2) {
+ if (i + 1 < leaves.size()) {
+ next.add(or(leaves.get(i), leaves.get(i + 1)));
+ } else {
+ next.add(leaves.get(i));
+ }
+ }
+ leaves = next;
+ }
+ return leaves.get(0);
+ }
+
+ /** Build a flat OR CallExpression with more than 2 children. */
+ private ResolvedExpression buildFlatOrExpression(int count) {
+ FieldReferenceExpression field =
+ new FieldReferenceExpression(
+ "contract_address",
org.apache.flink.table.api.DataTypes.STRING(), 0, 0);
+ List<ResolvedExpression> children = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ children.add(equalExpr(field, i));
+ }
+ return CallExpression.anonymous(
+ BuiltInFunctionDefinitions.OR,
+ children,
+ org.apache.flink.table.api.DataTypes.BOOLEAN());
+ }
+
+ private ResolvedExpression equalExpr(FieldReferenceExpression field, int
i) {
+ return CallExpression.anonymous(
+ BuiltInFunctionDefinitions.EQUALS,
+ ImmutableList.of(field, addressLiteral(i)),
+ org.apache.flink.table.api.DataTypes.BOOLEAN());
+ }
+
+ private ValueLiteralExpression addressLiteral(int i) {
+ return new ValueLiteralExpression(
+ String.format("0x%040x", i),
+ org.apache.flink.table.api.DataTypes.STRING().notNull());
+ }
+
private ResolvedExpression col1Equal1() {
return CallExpression.anonymous(
BuiltInFunctionDefinitions.EQUALS,
diff --git
a/paimon-format/src/test/java/org/apache/paimon/format/orc/filter/OrcFilterConverterTest.java
b/paimon-format/src/test/java/org/apache/paimon/format/orc/filter/OrcFilterConverterTest.java
index 28f2dfac75..a32a84304a 100644
---
a/paimon-format/src/test/java/org/apache/paimon/format/orc/filter/OrcFilterConverterTest.java
+++
b/paimon-format/src/test/java/org/apache/paimon/format/orc/filter/OrcFilterConverterTest.java
@@ -75,10 +75,10 @@ public class OrcFilterConverterTest {
test(
builder.in(0, Arrays.asList(1L, 2L, 3L)),
new OrcFilters.Or(
+ new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 1),
new OrcFilters.Or(
- new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 1),
- new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 2)),
- new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 3)),
+ new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 2),
+ new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 3))),
true);
test(
@@ -92,14 +92,14 @@ public class OrcFilterConverterTest {
test(
builder.notIn(0, Arrays.asList(1L, 2L, 3L)),
new OrcFilters.And(
+ new OrcFilters.Not(
+ new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 1)),
new OrcFilters.And(
new OrcFilters.Not(
- new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 1)),
+ new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 2)),
new OrcFilters.Not(
new OrcFilters.Equals(
- "long1",
PredicateLeaf.Type.LONG, 2))),
- new OrcFilters.Not(
- new OrcFilters.Equals("long1",
PredicateLeaf.Type.LONG, 3))),
+ "long1",
PredicateLeaf.Type.LONG, 3)))),
true);
assertThat(
@@ -186,29 +186,29 @@ public class OrcFilterConverterTest {
Collections.singletonList(
new DataField(0, "testField", new
BigIntType()))));
- // Test IN with multiple values (≤20 values should be converted to OR
of EQUALS)
+ // Test IN with multiple values
test(
builder.in(0, Arrays.asList(1L, 2L, 3L)),
new OrcFilters.Or(
+ new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 1L),
new OrcFilters.Or(
- new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 1L),
- new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 2L)),
- new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 3L)),
+ new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 2L),
+ new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 3L))),
true);
- // Test NOT IN with multiple values (should be converted to AND of NOT
EQUALS)
+ // Test NOT IN with multiple values
test(
builder.notIn(0, Arrays.asList(1L, 2L, 3L)),
new OrcFilters.And(
+ new OrcFilters.Not(
+ new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 1L)),
new OrcFilters.And(
new OrcFilters.Not(
new OrcFilters.Equals(
- "testField",
PredicateLeaf.Type.LONG, 1L)),
+ "testField",
PredicateLeaf.Type.LONG, 2L)),
new OrcFilters.Not(
new OrcFilters.Equals(
- "testField",
PredicateLeaf.Type.LONG, 2L))),
- new OrcFilters.Not(
- new OrcFilters.Equals("testField",
PredicateLeaf.Type.LONG, 3L))),
+ "testField",
PredicateLeaf.Type.LONG, 3L)))),
true);
}
diff --git
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFiltersTest.java
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFiltersTest.java
index 4fdd1e3927..a10efa2cf2 100644
---
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFiltersTest.java
+++
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetFiltersTest.java
@@ -68,7 +68,7 @@ class ParquetFiltersTest {
test(
builder.in(0, Arrays.asList(1L, 2L, 3L)),
- "or(or(eq(long1, 1), eq(long1, 2)), eq(long1, 3))",
+ "or(eq(long1, 1), or(eq(long1, 2), eq(long1, 3)))",
true);
test(builder.between(0, 1L, 3L), "and(gteq(long1, 1), lteq(long1,
3))", true);
@@ -92,7 +92,7 @@ class ParquetFiltersTest {
test(
builder.in(0, Arrays.asList("1", "2", "3")),
- "or(or(eq(string1, Binary{\"1\"}), eq(string1,
Binary{\"2\"})), eq(string1, Binary{\"3\"}))",
+ "or(eq(string1, Binary{\"1\"}), or(eq(string1, Binary{\"2\"}),
eq(string1, Binary{\"3\"})))",
true);
test(
builder.notIn(0, Arrays.asList("1", "2", "3")),
@@ -380,16 +380,15 @@ class ParquetFiltersTest {
Decimal v2 = Decimal.fromBigDecimal(new BigDecimal("200.00"),
precision, scale);
Decimal v3 = Decimal.fromBigDecimal(new BigDecimal("300.00"),
precision, scale);
- // For less than 21 elements, it expands to or(eq, eq, eq)
test(
builder.in(0, Arrays.asList(v1, v2, v3)),
- "or(or(eq(decimal1, "
+ "or(eq(decimal1, "
+ (int) v1.toUnscaledLong()
- + "), eq(decimal1, "
+ + "), or(eq(decimal1, "
+ (int) v2.toUnscaledLong()
- + ")), eq(decimal1, "
+ + "), eq(decimal1, "
+ (int) v3.toUnscaledLong()
- + "))",
+ + ")))",
true);
test(
@@ -421,16 +420,15 @@ class ParquetFiltersTest {
Decimal v2 = Decimal.fromBigDecimal(new
BigDecimal("20000000000.0000"), precision, scale);
Decimal v3 = Decimal.fromBigDecimal(new
BigDecimal("30000000000.0000"), precision, scale);
- // For less than 21 elements, it expands to or(eq, eq, eq)
test(
builder.in(0, Arrays.asList(v1, v2, v3)),
- "or(or(eq(decimal1, "
+ "or(eq(decimal1, "
+ v1.toUnscaledLong()
- + "), eq(decimal1, "
+ + "), or(eq(decimal1, "
+ v2.toUnscaledLong()
- + ")), eq(decimal1, "
+ + "), eq(decimal1, "
+ v3.toUnscaledLong()
- + "))",
+ + ")))",
true);
test(
@@ -554,13 +552,13 @@ class ParquetFiltersTest {
test(
builder.in(0, Arrays.asList(v1, v2, v3)),
- "or(or(eq(ts1, "
+ "or(eq(ts1, "
+ v1.getMillisecond()
- + "), eq(ts1, "
+ + "), or(eq(ts1, "
+ v2.getMillisecond()
- + ")), eq(ts1, "
+ + "), eq(ts1, "
+ v3.getMillisecond()
- + "))",
+ + ")))",
true);
test(
@@ -594,13 +592,13 @@ class ParquetFiltersTest {
test(
builder.in(0, Arrays.asList(v1, v2, v3)),
- "or(or(eq(ts1, "
+ "or(eq(ts1, "
+ micros1
- + "), eq(ts1, "
+ + "), or(eq(ts1, "
+ micros2
- + ")), eq(ts1, "
+ + "), eq(ts1, "
+ micros3
- + "))",
+ + ")))",
true);
test(