This is an automated email from the ASF dual-hosted git repository.
xuzifu666 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/main by this push:
new 81ec38a191 [CALCITE-5845] Select a very large predicate throw threw
'java.lang.StackOverflowError' exception in SqlNode to RelNode phase
81ec38a191 is described below
commit 81ec38a19198b2e410d3fee443512f0c9142c8f1
Author: Yu Xu <[email protected]>
AuthorDate: Sun Nov 1 12:50:47 2026 +0800
[CALCITE-5845] Select a very large predicate throw threw
'java.lang.StackOverflowError' exception in SqlNode to RelNode phase
---
.../java/org/apache/calcite/plan/RelOptUtil.java | 126 +++++++++++++--------
.../main/java/org/apache/calcite/rex/RexUtil.java | 65 ++++++++---
.../calcite/sql2rel/StandardConvertletTable.java | 75 ++++++++++++
.../org/apache/calcite/plan/RelOptUtilTest.java | 82 ++++++++++++++
.../java/org/apache/calcite/rex/RexUtilTest.java | 92 +++++++++++++++
.../java/org/apache/calcite/test/SmallStack.java | 62 ++++++++++
.../apache/calcite/test/SqlToRelConverterTest.java | 99 ++++++++++++++++
7 files changed, 540 insertions(+), 61 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
index d92a436595..a9e0db57f6 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -2559,15 +2559,27 @@ public static List<RelDataTypeField> deduplicateColumns(
public static void decomposeConjunction(
@Nullable RexNode rexPredicate,
List<RexNode> rexList) {
- if (rexPredicate == null || rexPredicate.isAlwaysTrue()) {
+ // Use an explicit stack to avoid StackOverflowError on deeply nested ANDs
+ // (e.g. CALCITE-5845)
+ if (rexPredicate == null) {
return;
}
- if (rexPredicate.isA(SqlKind.AND)) {
- for (RexNode operand : ((RexCall) rexPredicate).getOperands()) {
- decomposeConjunction(operand, rexList);
+ final Deque<RexNode> stack = new ArrayDeque<>();
+ stack.push(rexPredicate);
+ while (!stack.isEmpty()) {
+ final RexNode node = stack.pop();
+ if (node.isAlwaysTrue()) {
+ continue;
+ }
+ if (node.isA(SqlKind.AND)) {
+ final List<RexNode> operands = ((RexCall) node).getOperands();
+ // Push in reverse order so that the leftmost operand is processed
first
+ for (int i = operands.size() - 1; i >= 0; i--) {
+ stack.push(operands.get(i));
+ }
+ } else {
+ rexList.add(node);
}
- } else {
- rexList.add(rexPredicate);
}
}
@@ -2593,47 +2605,59 @@ public static void decomposeConjunction(
@Nullable RexNode rexPredicate,
List<RexNode> rexList,
List<RexNode> notList) {
- if (rexPredicate == null || rexPredicate.isAlwaysTrue()) {
+ // Use an explicit stack to avoid StackOverflowError on deeply nested
+ // expressions (e.g. CALCITE-5845)
+ if (rexPredicate == null) {
return;
}
- switch (rexPredicate.getKind()) {
- case AND:
- for (RexNode operand : ((RexCall) rexPredicate).getOperands()) {
- decomposeConjunction(operand, rexList, notList);
- }
- break;
- case NOT:
- final RexNode e = ((RexCall) rexPredicate).getOperands().get(0);
- if (e.isAlwaysFalse()) {
- return;
+ final Deque<RexNode> stack = new ArrayDeque<>();
+ stack.push(rexPredicate);
+ while (!stack.isEmpty()) {
+ final RexNode node = stack.pop();
+ if (node.isAlwaysTrue()) {
+ continue;
}
- switch (e.getKind()) {
- case OR:
- final List<RexNode> ors = new ArrayList<>();
- decomposeDisjunction(e, ors);
- for (RexNode or : ors) {
- switch (or.getKind()) {
- case NOT:
- rexList.add(((RexCall) or).operands.get(0));
- break;
- default:
- notList.add(or);
+ switch (node.getKind()) {
+ case AND:
+ final List<RexNode> operands = ((RexCall) node).getOperands();
+ // Push in reverse order so that the leftmost operand is processed
first
+ for (int i = operands.size() - 1; i >= 0; i--) {
+ stack.push(operands.get(i));
+ }
+ break;
+ case NOT:
+ final RexNode e = ((RexCall) node).getOperands().get(0);
+ if (e.isAlwaysFalse()) {
+ break;
+ }
+ switch (e.getKind()) {
+ case OR:
+ final List<RexNode> ors = new ArrayList<>();
+ decomposeDisjunction(e, ors);
+ for (RexNode or : ors) {
+ switch (or.getKind()) {
+ case NOT:
+ rexList.add(((RexCall) or).operands.get(0));
+ break;
+ default:
+ notList.add(or);
+ }
}
+ break;
+ default:
+ notList.add(e);
}
break;
+ case LITERAL:
+ if (!RexLiteral.isNullLiteral(node)
+ && RexLiteral.booleanValue(node)) {
+ break; // ignore TRUE
+ }
+ // fall through
default:
- notList.add(e);
- }
- break;
- case LITERAL:
- if (!RexLiteral.isNullLiteral(rexPredicate)
- && RexLiteral.booleanValue(rexPredicate)) {
- return; // ignore TRUE
+ rexList.add(node);
+ break;
}
- // fall through
- default:
- rexList.add(rexPredicate);
- break;
}
}
@@ -2647,15 +2671,27 @@ public static void decomposeConjunction(
public static void decomposeDisjunction(
@Nullable RexNode rexPredicate,
List<RexNode> rexList) {
- if (rexPredicate == null || rexPredicate.isAlwaysFalse()) {
+ // Use an explicit stack to avoid StackOverflowError on deeply nested ORs
+ // (e.g. CALCITE-5845)
+ if (rexPredicate == null) {
return;
}
- if (rexPredicate.isA(SqlKind.OR)) {
- for (RexNode operand : ((RexCall) rexPredicate).getOperands()) {
- decomposeDisjunction(operand, rexList);
+ final Deque<RexNode> stack = new ArrayDeque<>();
+ stack.push(rexPredicate);
+ while (!stack.isEmpty()) {
+ final RexNode node = stack.pop();
+ if (node.isAlwaysFalse()) {
+ continue;
+ }
+ if (node.isA(SqlKind.OR)) {
+ final List<RexNode> operands = ((RexCall) node).getOperands();
+ // Push in reverse order so that the leftmost operand is processed
first
+ for (int i = operands.size() - 1; i >= 0; i--) {
+ stack.push(operands.get(i));
+ }
+ } else {
+ rexList.add(node);
}
- } else {
- rexList.add(rexPredicate);
}
}
diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
index 71f4acfef9..b340e1ba78 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
@@ -67,10 +67,12 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@@ -1519,15 +1521,22 @@ public static ImmutableList<RexNode> flattenAnd(
private static void addAnd(ImmutableList.Builder<RexNode> builder,
Set<RexNode> digests, RexNode node) {
- switch (node.getKind()) {
- case AND:
- for (RexNode operand : ((RexCall) node).getOperands()) {
- addAnd(builder, digests, operand);
- }
- return;
- default:
- if (!node.isAlwaysTrue() && digests.add(node)) {
- builder.add(node);
+ // Use an explicit stack to avoid StackOverflowError on deeply nested ANDs
+ // (e.g. CALCITE-5845)
+ final Deque<RexNode> stack = new ArrayDeque<>();
+ stack.push(node);
+ while (!stack.isEmpty()) {
+ final RexNode current = stack.pop();
+ if (current.getKind() == SqlKind.AND) {
+ final List<RexNode> operands = ((RexCall) current).getOperands();
+ // Push in reverse order so that the leftmost operand is processed
first
+ for (int i = operands.size() - 1; i >= 0; i--) {
+ stack.push(operands.get(i));
+ }
+ } else {
+ if (!current.isAlwaysTrue() && digests.add(current)) {
+ builder.add(current);
+ }
}
}
}
@@ -1823,20 +1832,44 @@ private static boolean isFlat(
* calls to an associative operator such as AND and OR.
*/
public static boolean isFlat(RexNode expr) {
- if (!(expr instanceof RexCall)) {
- return true;
+ // Use an explicit stack to avoid StackOverflowError on deeply nested
+ // expressions (e.g. CALCITE-5845)
+ final Deque<RexNode> stack = new ArrayDeque<>();
+ stack.push(expr);
+ while (!stack.isEmpty()) {
+ final RexNode current = stack.pop();
+ if (!(current instanceof RexCall)) {
+ continue;
+ }
+ final RexCall call = (RexCall) current;
+ if (!isFlat(call.getOperands(), call.getOperator())) {
+ return false;
+ }
+ for (RexNode operand : call.getOperands()) {
+ stack.push(operand);
+ }
}
- final RexCall call = (RexCall) expr;
- return isFlat(call.getOperands(), call.getOperator())
- && all(call.getOperands(), RexUtil::isFlat);
+ return true;
}
private static void flattenRecurse(
List<RexNode> list, List<? extends RexNode> exprs, SqlOperator op) {
- for (RexNode expr : exprs) {
+ // Use an explicit stack to avoid StackOverflowError on deeply nested
+ // associative calls (e.g. CALCITE-5845)
+ final Deque<RexNode> stack = new ArrayDeque<>();
+ // Push in reverse order so that the leftmost operand is processed first
+ for (int i = exprs.size() - 1; i >= 0; i--) {
+ stack.push(exprs.get(i));
+ }
+ while (!stack.isEmpty()) {
+ final RexNode expr = stack.pop();
if (expr instanceof RexCall
&& ((RexCall) expr).getOperator() == op) {
- flattenRecurse(list, ((RexCall) expr).getOperands(), op);
+ final List<RexNode> operands = ((RexCall) expr).getOperands();
+ // Push in reverse order so that the leftmost operand is processed
first
+ for (int i = operands.size() - 1; i >= 0; i--) {
+ stack.push(operands.get(i));
+ }
} else {
list.add(expr);
}
diff --git
a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
index bff49f3924..05aa63fdf0 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
@@ -93,6 +93,7 @@
import java.math.RoundingMode;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
@@ -1204,6 +1205,17 @@ public RexNode convertCall(
SqlCall call) {
final SqlOperator op = call.getOperator();
final RexBuilder rexBuilder = cx.getRexBuilder();
+
+ // For associative operators (AND, OR), flatten the SqlNode tree
iteratively
+ // to avoid StackOverflowError on deeply nested expressions.
+ // The parser creates a left-deep binary tree for chains like
+ // "a AND b AND c AND d ...", and the default convertOperands would recurse
+ // into each nested level, causing stack overflow for large operand counts.
+ final SqlKind kind = op.getKind();
+ if (kind == SqlKind.AND || kind == SqlKind.OR) {
+ return convertAssociativeCall(cx, rexBuilder, call, op, kind);
+ }
+
SqlOperandTypeChecker operandTypeChecker = op.getOperandTypeChecker();
final SqlOperandTypeChecker.Consistency consistency =
operandTypeChecker == null
@@ -1276,6 +1288,69 @@ public RexNode convertCall(
return rexBuilder.makeCall(call.getParserPosition(), type, op,
RexUtil.flatten(exprs, op));
}
+ /**
+ * Converts a call to an associative operator (AND, OR) by iteratively
+ * flattening the nested binary SqlNode tree into a flat list of operands,
+ * then converting each operand. This avoids StackOverflowError on deeply
+ * nested expressions.
+ */
+ private static RexNode convertAssociativeCall(
+ SqlRexContext cx,
+ RexBuilder rexBuilder,
+ SqlCall call,
+ SqlOperator op,
+ SqlKind kind) {
+ // Step 1: Iteratively collect all leaf operands from the nested
+ // binary tree. For example, AND(AND(AND(a, b), c), d) becomes [a, b, c,
d].
+ final List<SqlNode> operands = new ArrayList<>();
+ final Deque<SqlNode> stack = new ArrayDeque<>();
+ stack.push(call);
+ while (!stack.isEmpty()) {
+ final SqlNode node = stack.pop();
+ if (node instanceof SqlCall && node.getKind() == kind) {
+ final SqlCall subCall = (SqlCall) node;
+ // Push right operand first, then left, so that left is processed first
+ final List<SqlNode> subOperands = subCall.getOperandList();
+ for (int i = subOperands.size() - 1; i >= 0; i--) {
+ stack.push(subOperands.get(i));
+ }
+ } else {
+ operands.add(node);
+ }
+ }
+
+ // Step 2: Convert each leaf operand.
+ final List<RexNode> exprs = new ArrayList<>();
+ for (SqlNode operand : operands) {
+ exprs.add(cx.convertExpression(operand));
+ }
+
+ // Step 3: Apply type coercion consistent with the original binary call.
+ // The validator has already checked the original binary call's operand
types.
+ // We replicate the same coercion for all operands.
+ final List<RelDataType> operandTypes =
+ cx.getValidator().getValidatedOperandTypes(call);
+ if (operandTypes != null && !operandTypes.isEmpty()) {
+ // Use the first operand's validated type as the template
+ // (both operands of AND/OR have the same validated type)
+ final RelDataType targetType = operandTypes.get(0);
+ final List<RexNode> coercedExprs = new ArrayList<>();
+ for (RexNode expr : exprs) {
+ coercedExprs.add(
+ rexBuilder.ensureType(call.getParserPosition(), targetType, expr,
true));
+ }
+ exprs.clear();
+ exprs.addAll(coercedExprs);
+ }
+
+ // Step 4: Flatten any nested AND/OR RexNodes that may have resulted from
+ // operand conversion (e.g. IN-to-OR expansion), then derive the return
type
+ // and create a flat n-ary RexCall.
+ final List<RexNode> flatExprs = RexUtil.flatten(exprs, op);
+ final RelDataType type = rexBuilder.deriveReturnType(op, flatExprs);
+ return rexBuilder.makeCall(call.getParserPosition(), type, op, flatExprs);
+ }
+
private static List<Integer> elseArgs(int count) {
// If list is odd, e.g. [0, 1, 2, 3, 4] we get [1, 3, 4]
// If list is even, e.g. [0, 1, 2, 3, 4, 5] we get [2, 4, 5]
diff --git a/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java
b/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java
index 3bdfbd2625..34c11115c8 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelOptUtilTest.java
@@ -49,6 +49,7 @@
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql2rel.RexRewritingRelShuttle;
import org.apache.calcite.test.CalciteAssert;
+import org.apache.calcite.test.SmallStack;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.Pair;
@@ -1122,6 +1123,87 @@ private void splitJoinConditionHelper(RexNode joinCond,
List<Integer> expLeftKey
assertThat(secondVar.getType(), is(empRow));
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ANDs</a>.
+ *
+ * <p>Verifies that {@link RelOptUtil#conjunctions} computes a deeply nested
+ * binary AND tree on a small-stack thread; the previous recursive
+ * implementation overflows. */
+ @Test void testConjunctionsDeeplyNested() {
+ final RexBuilder rexBuilder = relBuilder.getCluster().getRexBuilder();
+ final RelDataType booleanType =
+ relBuilder.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN);
+ final int depth = 20_000;
+ // Build AND(AND(AND(..., $1), $2), $3, ...) as a left-deep binary tree,
+ // iteratively.
+ RexNode node = rexBuilder.makeInputRef(booleanType, 0);
+ for (int i = 1; i < depth; i++) {
+ node =
+ rexBuilder.makeCall(SqlStdOperatorTable.AND, node,
+ rexBuilder.makeInputRef(booleanType, i));
+ }
+ final RexNode deepTree = node;
+ final List<RexNode> conjunctions =
+ SmallStack.run(() -> RelOptUtil.conjunctions(deepTree));
+ assertThat(conjunctions, hasSize(depth));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ANDs</a>.
+ *
+ * <p>Verifies that {@link RelOptUtil#decomposeConjunction(RexNode, List,
List)}
+ * computes a deeply nested binary AND tree on a small-stack thread; the
+ * previous recursive implementation overflows. */
+ @Test void testDecomposeConjunctionNotDeeplyNested() {
+ final RexBuilder rexBuilder = relBuilder.getCluster().getRexBuilder();
+ final RelDataType booleanType =
+ relBuilder.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN);
+ final int depth = 20_000;
+ RexNode node = rexBuilder.makeInputRef(booleanType, 0);
+ for (int i = 1; i < depth; i++) {
+ node =
+ rexBuilder.makeCall(SqlStdOperatorTable.AND, node,
+ rexBuilder.makeInputRef(booleanType, i));
+ }
+ final RexNode deepTree = node;
+ final List<RexNode> rexList = new ArrayList<>();
+ final List<RexNode> notList = new ArrayList<>();
+ SmallStack.run(() -> {
+ RelOptUtil.decomposeConjunction(deepTree, rexList, notList);
+ return null;
+ });
+ assertThat(rexList, hasSize(depth));
+ assertThat(notList, hasSize(0));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ANDs</a>.
+ *
+ * <p>Verifies that {@link RelOptUtil#disjunctions} computes a deeply nested
+ * binary OR tree on a small-stack thread; the previous recursive
+ * implementation overflows. */
+ @Test void testDisjunctionsDeeplyNested() {
+ final RexBuilder rexBuilder = relBuilder.getCluster().getRexBuilder();
+ final RelDataType booleanType =
+ relBuilder.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN);
+ final int depth = 20_000;
+ // Build OR(OR(OR(..., $1), $2), $3, ...) as a left-deep binary tree,
+ // iteratively.
+ RexNode node = rexBuilder.makeInputRef(booleanType, 0);
+ for (int i = 1; i < depth; i++) {
+ node =
+ rexBuilder.makeCall(SqlStdOperatorTable.OR, node,
+ rexBuilder.makeInputRef(booleanType, i));
+ }
+ final RexNode deepTree = node;
+ final List<RexNode> disjunctions =
+ SmallStack.run(() -> RelOptUtil.disjunctions(deepTree));
+ assertThat(disjunctions, hasSize(depth));
+ }
+
/** Dummy sub-class of ConverterRule, to check whether generated descriptions
* are OK. */
private static class MyConverterRule extends ConverterRule {
diff --git a/core/src/test/java/org/apache/calcite/rex/RexUtilTest.java
b/core/src/test/java/org/apache/calcite/rex/RexUtilTest.java
index 229886f6d0..f4fefa5043 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexUtilTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexUtilTest.java
@@ -17,13 +17,23 @@
package org.apache.calcite.rex;
import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.test.SmallStack;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.RelBuilder;
+import com.google.common.collect.ImmutableList;
+
import org.junit.jupiter.api.Test;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
@@ -43,4 +53,86 @@ class RexUtilTest {
RexUtil.SubQueryFinder.find(expression, SqlKind.SCALAR_QUERY));
assertNull(RexUtil.SubQueryFinder.find(expression, SqlKind.EXISTS));
}
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ANDs</a>.
+ *
+ * <p>Verifies that {@link RexUtil#flattenAnd} computes a deeply nested
+ * binary AND tree on a small-stack thread; the previous recursive
+ * implementation overflows. */
+ @Test void testFlattenAndDeeplyNested() {
+ final RelBuilder builder =
+ RelBuilder.create(Frameworks.newConfigBuilder().build());
+ final RexBuilder rexBuilder = builder.getCluster().getRexBuilder();
+ final RelDataType booleanType =
+ builder.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN);
+ final int depth = 20_000;
+ // Build AND(AND(AND(..., $1), $2), $3, ...) as a left-deep binary tree,
+ // iteratively. Hash codes are pre-warmed bottom-up so that the
+ // de-duplication set inside flattenAnd does not itself recurse deeply.
+ RexNode node = rexBuilder.makeInputRef(booleanType, 0);
+ node.hashCode();
+ for (int i = 1; i < depth; i++) {
+ node =
+ rexBuilder.makeCall(SqlStdOperatorTable.AND, node,
+ rexBuilder.makeInputRef(booleanType, i));
+ node.hashCode();
+ }
+ final RexNode deepTree = node;
+ final List<RexNode> flat =
+ SmallStack.run(() -> RexUtil.flattenAnd(ImmutableList.of(deepTree)));
+ assertThat(flat, hasSize(depth));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ANDs</a>.
+ *
+ * <p>Verifies that {@link RexUtil#flatten} computes a deeply nested binary
+ * AND tree on a small-stack thread; the previous recursive implementation
+ * overflows. */
+ @Test void testFlattenDeeplyNested() {
+ final RelBuilder builder =
+ RelBuilder.create(Frameworks.newConfigBuilder().build());
+ final RexBuilder rexBuilder = builder.getCluster().getRexBuilder();
+ final RelDataType booleanType =
+ builder.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN);
+ final int depth = 20_000;
+ RexNode node = rexBuilder.makeInputRef(booleanType, 0);
+ for (int i = 1; i < depth; i++) {
+ node =
+ rexBuilder.makeCall(SqlStdOperatorTable.AND, node,
+ rexBuilder.makeInputRef(booleanType, i));
+ }
+ final RexNode deepTree = node;
+ final List<RexNode> flat =
+ SmallStack.run(
+ () -> RexUtil.flatten(ImmutableList.of(deepTree),
+ SqlStdOperatorTable.AND));
+ assertThat(flat, hasSize(depth));
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ANDs</a>.
+ *
+ * <p>Verifies that {@link RexUtil#isFlat(RexNode)} computes a deeply nested
+ * tree of a non-associative operator (NOT) on a small-stack thread, where
+ * short-circuiting at the top level is not possible; the previous recursive
+ * implementation overflows. */
+ @Test void testIsFlatDeeplyNested() {
+ final RelBuilder builder =
+ RelBuilder.create(Frameworks.newConfigBuilder().build());
+ final RexBuilder rexBuilder = builder.getCluster().getRexBuilder();
+ final RelDataType booleanType =
+ builder.getTypeFactory().createSqlType(SqlTypeName.BOOLEAN);
+ final int depth = 20_000;
+ RexNode node = rexBuilder.makeInputRef(booleanType, 0);
+ for (int i = 0; i < depth; i++) {
+ node = rexBuilder.makeCall(SqlStdOperatorTable.NOT, node);
+ }
+ final RexNode deepTree = node;
+ assertThat(SmallStack.run(() -> RexUtil.isFlat(deepTree)), is(true));
+ }
}
diff --git a/core/src/test/java/org/apache/calcite/test/SmallStack.java
b/core/src/test/java/org/apache/calcite/test/SmallStack.java
new file mode 100644
index 0000000000..4ae950fdfa
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/test/SmallStack.java
@@ -0,0 +1,62 @@
+/*
+ * 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 org.apache.calcite.test;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Executes code on a thread with a small stack.
+ *
+ * <p>Useful for verifying that an implementation is iterative: a recursive
+ * implementation overflows quickly, while an iterative one completes. */
+public class SmallStack {
+ private SmallStack() {
+ }
+
+ /** Runs {@code callable} on a thread with a 256KB stack, waits for it to
+ * finish, and returns its result, rethrowing any exception or error that it
+ * threw. */
+ public static <T> T run(Callable<T> callable) {
+ final AtomicReference<T> result = new AtomicReference<>();
+ final AtomicReference<Throwable> error = new AtomicReference<>();
+ final Thread thread = new Thread(null, () -> {
+ try {
+ result.set(callable.call());
+ } catch (Throwable t) {
+ error.set(t);
+ }
+ }, "small-stack", 256 * 1024);
+ thread.start();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
+ final Throwable t = error.get();
+ if (t == null) {
+ return result.get();
+ }
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ }
+ if (t instanceof Error) {
+ throw (Error) t;
+ }
+ throw new RuntimeException(t);
+ }
+}
diff --git
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index f93370a6b1..ef093bd92f 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -51,6 +51,7 @@
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexShuttle;
import org.apache.calcite.sql.SqlExplainLevel;
+import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.fun.SqlLibrary;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.validate.SqlConformance;
@@ -7120,4 +7121,102 @@ void checkUserDefinedOrderByOver(NullCollation
nullCollation) {
+ " LIMIT 2) AS t";
sql(sql).withConformance(SqlConformanceEnum.LENIENT).ok();
}
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ANDs</a>.
+ *
+ * <p>Verifies that converting a SQL with many AND-connected predicates
+ * from SqlNode to RelNode does not cause StackOverflowError. */
+ @Test void testLargeAndPredicate() {
+ final int count = 1000;
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SELECT * FROM emp WHERE 1 = 1");
+ for (int i = 0; i < count; i++) {
+ sb.append(" AND 1 = 1");
+ }
+ final String sql = sb.toString();
+ // Should not throw StackOverflowError
+ final RelNode rel = sql(sql).toRel();
+ assertThat(rel, notNullValue());
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-5845">[CALCITE-5845]
+ * StackOverflowError when SQL has a very large predicate with many ORs</a>.
+ *
+ * <p>Verifies that converting a SQL with many OR-connected predicates
+ * from SqlNode to RelNode does not cause StackOverflowError. */
+ @Test void testLargeOrPredicate() {
+ final int count = 1000;
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SELECT * FROM emp WHERE 1 = 0");
+ for (int i = 0; i < count; i++) {
+ sb.append(" OR 1 = 0");
+ }
+ final String sql = sb.toString();
+ // Should not throw StackOverflowError
+ final RelNode rel = sql(sql).toRel();
+ assertThat(rel, notNullValue());
+ }
+
+ /** Verifies that a small AND chain produces a flat RexCall with correct
+ * operand count and structure (CALCITE-5845 correctness check). */
+ @Test void testSmallAndChainPlanStructure() {
+ final String sql = "SELECT * FROM emp"
+ + " WHERE deptno = 10 AND sal > 1000 AND empno > 0 AND ename = 'x'";
+ final RelNode rel = sql(sql).toRel();
+ // The filter condition should be a flat AND with 4 operands
+ assertThat(rel, notNullValue());
+ final LogicalFilter filter = (LogicalFilter) rel.getInput(0);
+ final RexNode condition = filter.getCondition();
+ assertThat(condition, notNullValue());
+ // After flattening, the condition should be a single AND call
+ // with all 4 terms as direct operands (not nested ANDs)
+ assertThat(condition, instanceOf(RexCall.class));
+ final RexCall andCall = (RexCall) condition;
+ assertThat(andCall.getKind(), is(SqlKind.AND));
+ assertThat(andCall.getOperands(), hasSize(4));
+ // Verify none of the operands is itself an AND (i.e., it's flat)
+ for (RexNode operand : andCall.getOperands()) {
+ if (operand instanceof RexCall) {
+ assertThat(((RexCall) operand).getKind(), not(is(SqlKind.AND)));
+ }
+ }
+ }
+
+ /** Verifies that mixed AND/OR with IN expansion produces correct flat
+ * structure (CALCITE-5845 correctness check). */
+ @Test void testMixedAndOrWithIn() {
+ final String sql = "SELECT * FROM emp"
+ + " WHERE deptno IN (10, 20) AND sal > 1000 AND empno < 100";
+ final RelNode rel = sql(sql).toRel();
+ assertThat(rel, notNullValue());
+ // Just verify the plan is generated without error and the filter exists
+ final String plan = RelOptUtil.toString(rel);
+ assertThat(plan, notNullValue());
+ assertThat(plan.contains("Filter"), is(true));
+ }
+
+ /** Verifies that a large AND chain with actual column references
+ * produces a correct plan (CALCITE-5845 stress test). */
+ @Test void testLargeAndPredicateWithColumns() {
+ final int count = 500;
+ final StringBuilder sb = new StringBuilder();
+ sb.append("SELECT * FROM emp WHERE deptno > 0");
+ for (int i = 0; i < count; i++) {
+ // Use unique constants to avoid deduplication by simplifyPredicate
+ sb.append(" AND sal > ").append(i);
+ }
+ final String sql = sb.toString();
+ final RelNode rel = sql(sql).toRel();
+ assertThat(rel, notNullValue());
+ final LogicalFilter filter = (LogicalFilter) rel.getInput(0);
+ final RexNode condition = filter.getCondition();
+ assertThat(condition, instanceOf(RexCall.class));
+ final RexCall andCall = (RexCall) condition;
+ assertThat(andCall.getKind(), is(SqlKind.AND));
+ // Should have 501 operands: 1 deptno > 0 + 500 unique sal > i
+ assertThat(andCall.getOperands(), hasSize(501));
+ }
}