diff --git a/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/ExpressionGuard.java b/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/ExpressionGuard.java
new file mode 100644
index 0000000000..6159d3fb89
--- /dev/null
+++ b/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/ExpressionGuard.java
@@ -0,0 +1,168 @@
+/*
+ * 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.arrow.gandiva.evaluator;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import org.apache.arrow.gandiva.exceptions.GandivaException;
+import org.apache.arrow.gandiva.ipc.GandivaTypes;
+
+/**
+ * Pre-flight check for Gandiva expression trees.
+ *
+ *
Gandiva compiles each expression into a single LLVM-emitted function. Two AST shapes can
+ * crash the JVM in native code:
+ *
+ *
+ * - A very deep tree (e.g. a long {@code CASE WHEN} chain becoming a nested {@code IfNode}
+ * chain) — the native AST visitors recurse per level and exhaust the C++ stack during
+ * {@code Filter.make}/{@code Projector.make}.
+ *
- A very wide tree with many nodes (e.g. a planner-expanded {@code OR(AND(...), AND(...),
+ * ...)} with O(N²) comparisons) — compilation succeeds but the JIT'd function reserves a
+ * stack frame too large for the executor thread's stack at first call to {@code evaluate}.
+ *
+ *
+ * This class walks the protobuf form of an expression iteratively (no Java-side recursion, so
+ * the guard itself never overflows on pathological input) and rejects trees whose depth or
+ * node-count exceed configured limits, converting what would be a JVM crash into a recoverable
+ * {@link GandivaException}.
+ *
+ *
Limits can be overridden via the system properties {@value #MAX_DEPTH_PROPERTY} and {@value
+ * #MAX_NODES_PROPERTY}; the defaults are sized to admit any plausible hand-written expression
+ * while rejecting the planner-generated pathological shapes that have been observed to crash.
+ */
+final class ExpressionGuard {
+
+ static final String MAX_DEPTH_PROPERTY = "org.apache.arrow.gandiva.expr.max_depth";
+ static final String MAX_NODES_PROPERTY = "org.apache.arrow.gandiva.expr.max_nodes";
+
+ static final int DEFAULT_MAX_DEPTH = 100;
+ static final int DEFAULT_MAX_NODES = 10_000;
+
+ private ExpressionGuard() {}
+
+ static int maxDepth() {
+ return Integer.getInteger(MAX_DEPTH_PROPERTY, DEFAULT_MAX_DEPTH);
+ }
+
+ static int maxNodes() {
+ return Integer.getInteger(MAX_NODES_PROPERTY, DEFAULT_MAX_NODES);
+ }
+
+ /** Validates a Condition's root tree. */
+ static void check(GandivaTypes.Condition condition) throws GandivaException {
+ if (condition.hasRoot()) {
+ check(condition.getRoot());
+ }
+ }
+
+ /**
+ * Validates every expression root in an ExpressionList independently. Gandiva's
+ * {@code LLVMGenerator::Add} compiles each {@code Projector} expression into its own LLVM
+ * function ({@code expr__}), so the per-function spill-slot budget — which is what
+ * the node-count limit defends — applies per expression, not in aggregate. A Projector with
+ * many small expressions is fine even if their combined node count exceeds the limit.
+ */
+ static void check(GandivaTypes.ExpressionList exprs) throws GandivaException {
+ for (GandivaTypes.ExpressionRoot root : exprs.getExprsList()) {
+ if (root.hasRoot()) {
+ check(root.getRoot());
+ }
+ }
+ }
+
+ /** Walks a single tree iteratively, throwing if depth or node-count exceed the limits. */
+ static void check(GandivaTypes.TreeNode root) throws GandivaException {
+ final int maxDepth = maxDepth();
+ final int maxNodes = maxNodes();
+
+ // Pair each node with its depth on a work-stack. ArrayDeque is the JDK's recommended
+ // non-recursive stack; per-entry cost is tiny so we can hold ~maxNodes entries without
+ // approaching the heap budget.
+ Deque stack = new ArrayDeque<>();
+ stack.push(new Frame(root, 1));
+
+ int nodes = 0;
+ while (!stack.isEmpty()) {
+ Frame frame = stack.pop();
+ nodes++;
+ if (nodes > maxNodes) {
+ throw new GandivaException(
+ "Gandiva expression exceeds node-count limit: > "
+ + maxNodes
+ + " nodes (override with -D"
+ + MAX_NODES_PROPERTY
+ + "=N)");
+ }
+ if (frame.depth > maxDepth) {
+ throw new GandivaException(
+ "Gandiva expression exceeds depth limit: depth "
+ + frame.depth
+ + " > "
+ + maxDepth
+ + " (override with -D"
+ + MAX_DEPTH_PROPERTY
+ + "=N)");
+ }
+
+ GandivaTypes.TreeNode node = frame.node;
+ int childDepth = frame.depth + 1;
+
+ if (node.hasIfNode()) {
+ GandivaTypes.IfNode ifNode = node.getIfNode();
+ if (ifNode.hasCond()) {
+ stack.push(new Frame(ifNode.getCond(), childDepth));
+ }
+ if (ifNode.hasThenNode()) {
+ stack.push(new Frame(ifNode.getThenNode(), childDepth));
+ }
+ if (ifNode.hasElseNode()) {
+ stack.push(new Frame(ifNode.getElseNode(), childDepth));
+ }
+ }
+ if (node.hasAndNode()) {
+ for (GandivaTypes.TreeNode child : node.getAndNode().getArgsList()) {
+ stack.push(new Frame(child, childDepth));
+ }
+ }
+ if (node.hasOrNode()) {
+ for (GandivaTypes.TreeNode child : node.getOrNode().getArgsList()) {
+ stack.push(new Frame(child, childDepth));
+ }
+ }
+ if (node.hasFnNode()) {
+ for (GandivaTypes.TreeNode child : node.getFnNode().getInArgsList()) {
+ stack.push(new Frame(child, childDepth));
+ }
+ }
+ if (node.hasInNode() && node.getInNode().hasNode()) {
+ stack.push(new Frame(node.getInNode().getNode(), childDepth));
+ }
+ // Leaf nodes (field, literals) have no children to enqueue.
+ }
+ }
+
+ private static final class Frame {
+ final GandivaTypes.TreeNode node;
+ final int depth;
+
+ Frame(GandivaTypes.TreeNode node, int depth) {
+ this.node = node;
+ this.depth = depth;
+ }
+ }
+}
diff --git a/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Filter.java b/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Filter.java
index 6c8540cde8..363dfd4072 100644
--- a/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Filter.java
+++ b/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Filter.java
@@ -115,6 +115,7 @@ public static synchronized Filter make(Schema schema, Condition condition, long
throws GandivaException {
// Invoke the JNI layer to create the LLVM module representing the filter.
GandivaTypes.Condition conditionBuf = condition.toProtobuf();
+ ExpressionGuard.check(conditionBuf);
GandivaTypes.Schema schemaBuf = ArrowTypeHelper.arrowSchemaToProtobuf(schema);
JniWrapper wrapper = JniLoader.getInstance().getWrapper();
long moduleId =
diff --git a/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Projector.java b/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Projector.java
index 9c5b22d659..23ce3710cc 100644
--- a/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Projector.java
+++ b/gandiva/src/main/java/org/apache/arrow/gandiva/evaluator/Projector.java
@@ -199,6 +199,8 @@ public static synchronized Projector make(
for (ExpressionTree expr : exprs) {
builder.addExprs(expr.toProtobuf());
}
+ GandivaTypes.ExpressionList exprList = builder.build();
+ ExpressionGuard.check(exprList);
// Invoke the JNI layer to create the LLVM module representing the expressions
GandivaTypes.Schema schemaBuf = ArrowTypeHelper.arrowSchemaToProtobuf(schema);
@@ -206,7 +208,7 @@ public static synchronized Projector make(
long moduleId =
wrapper.buildProjector(
schemaBuf.toByteArray(),
- builder.build().toByteArray(),
+ exprList.toByteArray(),
selectionVectorType.getNumber(),
configurationId);
logger.debug("Created module for the projector with id {}", moduleId);
diff --git a/gandiva/src/test/java/org/apache/arrow/gandiva/evaluator/ExpressionGuardTest.java b/gandiva/src/test/java/org/apache/arrow/gandiva/evaluator/ExpressionGuardTest.java
new file mode 100644
index 0000000000..a5571cac68
--- /dev/null
+++ b/gandiva/src/test/java/org/apache/arrow/gandiva/evaluator/ExpressionGuardTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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.arrow.gandiva.evaluator;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.arrow.gandiva.exceptions.GandivaException;
+import org.apache.arrow.gandiva.expression.Condition;
+import org.apache.arrow.gandiva.expression.ExpressionTree;
+import org.apache.arrow.gandiva.expression.TreeBuilder;
+import org.apache.arrow.gandiva.expression.TreeNode;
+import org.apache.arrow.gandiva.ipc.GandivaTypes;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Focused tests for the {@link ExpressionGuard} depth and node-count limits as wired into {@link
+ * Filter#make(Schema, Condition)} and {@link Projector#make(Schema, java.util.List)}. Each test
+ * builds the smallest expression that violates one limit, calls the relevant make() variant, and
+ * asserts the right kind of {@link GandivaException} is raised before reaching the JNI layer.
+ */
+public class ExpressionGuardTest extends BaseEvaluatorTest {
+
+ /**
+ * Builds a chain of nested {@link TreeBuilder#makeIf} nodes that is {@code numIfs} deep. With
+ * {@code numIfs > DEFAULT_MAX_DEPTH} the guard must reject this with a depth message.
+ */
+ private static TreeNode nestedIfChain(int numIfs, Field cond, Field then, Field els) {
+ TreeNode condNode = TreeBuilder.makeField(cond);
+ TreeNode thenNode = TreeBuilder.makeField(then);
+ TreeNode current = TreeBuilder.makeField(els);
+ ArrowType retType = els.getType();
+ for (int i = 0; i < numIfs; i++) {
+ current = TreeBuilder.makeIf(condNode, thenNode, current, retType);
+ }
+ return current;
+ }
+
+ /**
+ * Builds an OR of {@code numArgs} {@code isnotnull(field_i)} comparisons — a flat tree that
+ * pushes the node count up without adding any depth.
+ */
+ private static TreeNode wideOr(List fields) {
+ ArrowType bool = new ArrowType.Bool();
+ List args = new ArrayList<>(fields.size());
+ for (Field f : fields) {
+ args.add(
+ TreeBuilder.makeFunction(
+ "isnotnull", Lists.newArrayList(TreeBuilder.makeField(f)), bool));
+ }
+ return TreeBuilder.makeOr(args);
+ }
+
+ // ---------- depth limit ----------
+
+ @Test
+ public void filterRejectsExpressionAboveDepthLimit() {
+ Field cond = Field.nullable("c", new ArrowType.Bool());
+ Field then = Field.nullable("t", new ArrowType.Bool());
+ Field els = Field.nullable("e", new ArrowType.Bool());
+ Schema schema = new Schema(Lists.newArrayList(cond, then, els));
+
+ TreeNode root =
+ nestedIfChain(ExpressionGuard.DEFAULT_MAX_DEPTH + 50, cond, then, els);
+ Condition condition = TreeBuilder.makeCondition(root);
+
+ GandivaException ex =
+ assertThrows(GandivaException.class, () -> Filter.make(schema, condition));
+ assertTrue(ex.getMessage().contains("depth"), ex.getMessage());
+ }
+
+ @Test
+ public void projectorRejectsExpressionAboveDepthLimit() {
+ Field cond = Field.nullable("c", new ArrowType.Bool());
+ Field then = Field.nullable("t", new ArrowType.Bool());
+ Field els = Field.nullable("e", new ArrowType.Bool());
+ Schema schema = new Schema(Lists.newArrayList(cond, then, els));
+
+ TreeNode root =
+ nestedIfChain(ExpressionGuard.DEFAULT_MAX_DEPTH + 50, cond, then, els);
+ ExpressionTree expr =
+ TreeBuilder.makeExpression(root, Field.nullable("res", new ArrowType.Bool()));
+
+ GandivaException ex =
+ assertThrows(
+ GandivaException.class, () -> Projector.make(schema, Lists.newArrayList(expr)));
+ assertTrue(ex.getMessage().contains("depth"), ex.getMessage());
+ }
+
+ // ---------- node-count limit ----------
+
+ @Test
+ public void filterRejectsExpressionAboveNodeCountLimit() {
+ List fields = new ArrayList<>();
+ // numFields chosen so total nodes (1 OR + N functions + N fields) is comfortably past limit.
+ int numFields = ExpressionGuard.DEFAULT_MAX_NODES;
+ for (int i = 0; i < numFields; i++) {
+ fields.add(Field.nullable("f" + i, new ArrowType.Bool()));
+ }
+ Schema schema = new Schema(fields);
+ Condition condition = TreeBuilder.makeCondition(wideOr(fields));
+
+ GandivaException ex =
+ assertThrows(GandivaException.class, () -> Filter.make(schema, condition));
+ assertTrue(ex.getMessage().contains("node-count"), ex.getMessage());
+ }
+
+ @Test
+ public void projectorRejectsExpressionAboveNodeCountLimit() {
+ List fields = new ArrayList<>();
+ int numFields = ExpressionGuard.DEFAULT_MAX_NODES;
+ for (int i = 0; i < numFields; i++) {
+ fields.add(Field.nullable("f" + i, new ArrowType.Bool()));
+ }
+ Schema schema = new Schema(fields);
+ ExpressionTree expr =
+ TreeBuilder.makeExpression(
+ wideOr(fields), Field.nullable("res", new ArrowType.Bool()));
+
+ GandivaException ex =
+ assertThrows(
+ GandivaException.class, () -> Projector.make(schema, Lists.newArrayList(expr)));
+ assertTrue(ex.getMessage().contains("node-count"), ex.getMessage());
+ }
+
+ // ---------- sanity: small expressions still compile ----------
+
+ @Test
+ public void smallExpressionPassesGuard() throws GandivaException {
+ Field a = Field.nullable("a", new ArrowType.Bool());
+ Schema schema = new Schema(Lists.newArrayList(a));
+ Condition condition =
+ TreeBuilder.makeCondition(
+ TreeBuilder.makeFunction(
+ "isnotnull",
+ Lists.newArrayList(TreeBuilder.makeField(a)),
+ new ArrowType.Bool()));
+ Filter filter = Filter.make(schema, condition);
+ filter.close();
+ }
+
+ // ---------- boundary tests ----------
+ // The guard fires on `depth > maxDepth` and `nodes > maxNodes`, so the exact-limit value
+ // must pass and limit+1 must fail. These tests pin that boundary by calling the guard
+ // directly on a hand-built protobuf tree — going through Filter.make would also compile the
+ // expression natively, which for the node-count boundary (10 000-arg AND) takes ~9 minutes.
+ // The Filter.make/Projector.make wiring is covered by the other tests in this class.
+
+ /** Builds a protobuf TreeNode that is exactly {@code depth} deep. */
+ private static GandivaTypes.TreeNode protoChainOfDepth(int depth) throws GandivaException {
+ Field f = Field.nullable("a", new ArrowType.Bool());
+ TreeNode current = TreeBuilder.makeField(f);
+ for (int i = 1; i < depth; i++) {
+ current =
+ TreeBuilder.makeFunction("not", Lists.newArrayList(current), new ArrowType.Bool());
+ }
+ return current.toProtobuf();
+ }
+
+ /** Builds a protobuf TreeNode with exactly {@code totalNodes} nodes (flat AND of fields). */
+ private static GandivaTypes.TreeNode protoFlatAndOfSize(int totalNodes) throws GandivaException {
+ int numArgs = totalNodes - 1; // 1 AndNode + numArgs field nodes
+ List args = new ArrayList<>(numArgs);
+ for (int i = 0; i < numArgs; i++) {
+ args.add(TreeBuilder.makeField(Field.nullable("f" + i, new ArrowType.Bool())));
+ }
+ return TreeBuilder.makeAnd(args).toProtobuf();
+ }
+
+ @Test
+ public void guardPassesAtDepthOneBelowLimit() throws GandivaException {
+ ExpressionGuard.check(protoChainOfDepth(ExpressionGuard.DEFAULT_MAX_DEPTH - 1));
+ }
+
+ @Test
+ public void guardPassesAtDepthExactlyAtLimit() throws GandivaException {
+ ExpressionGuard.check(protoChainOfDepth(ExpressionGuard.DEFAULT_MAX_DEPTH));
+ }
+
+ @Test
+ public void guardRejectsAtDepthOneAboveLimit() throws GandivaException {
+ GandivaTypes.TreeNode root = protoChainOfDepth(ExpressionGuard.DEFAULT_MAX_DEPTH + 1);
+ GandivaException ex =
+ assertThrows(GandivaException.class, () -> ExpressionGuard.check(root));
+ assertTrue(ex.getMessage().contains("depth"), ex.getMessage());
+ }
+
+ @Test
+ public void guardPassesAtNodeCountExactlyAtLimit() throws GandivaException {
+ ExpressionGuard.check(protoFlatAndOfSize(ExpressionGuard.DEFAULT_MAX_NODES));
+ }
+
+ @Test
+ public void guardRejectsAtNodeCountOneAboveLimit() throws GandivaException {
+ GandivaTypes.TreeNode root = protoFlatAndOfSize(ExpressionGuard.DEFAULT_MAX_NODES + 1);
+ GandivaException ex =
+ assertThrows(GandivaException.class, () -> ExpressionGuard.check(root));
+ assertTrue(ex.getMessage().contains("node-count"), ex.getMessage());
+ }
+
+ // ---------- system-property override ----------
+
+ /**
+ * Raising the depth limit via {@link ExpressionGuard#MAX_DEPTH_PROPERTY} must let through a
+ * tree that would otherwise be rejected. Property is restored in finally so subsequent tests
+ * in the same JVM fork see the original behaviour.
+ */
+ @Test
+ public void depthLimitHonoursSystemPropertyOverride() throws GandivaException {
+ GandivaTypes.TreeNode root = protoChainOfDepth(ExpressionGuard.DEFAULT_MAX_DEPTH + 1);
+ // Without override: rejected.
+ assertThrows(GandivaException.class, () -> ExpressionGuard.check(root));
+
+ String previous = System.getProperty(ExpressionGuard.MAX_DEPTH_PROPERTY);
+ try {
+ System.setProperty(
+ ExpressionGuard.MAX_DEPTH_PROPERTY,
+ Integer.toString(ExpressionGuard.DEFAULT_MAX_DEPTH + 10));
+ // With override: accepted.
+ ExpressionGuard.check(root);
+ } finally {
+ if (previous == null) {
+ System.clearProperty(ExpressionGuard.MAX_DEPTH_PROPERTY);
+ } else {
+ System.setProperty(ExpressionGuard.MAX_DEPTH_PROPERTY, previous);
+ }
+ }
+ }
+}
diff --git a/gandiva/src/test/java/org/apache/arrow/gandiva/evaluator/UnpivotCaseStackOverflowTest.java b/gandiva/src/test/java/org/apache/arrow/gandiva/evaluator/UnpivotCaseStackOverflowTest.java
new file mode 100644
index 0000000000..f573641cd6
--- /dev/null
+++ b/gandiva/src/test/java/org/apache/arrow/gandiva/evaluator/UnpivotCaseStackOverflowTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.arrow.gandiva.evaluator;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.Lists;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.arrow.gandiva.exceptions.GandivaException;
+import org.apache.arrow.gandiva.expression.Condition;
+import org.apache.arrow.gandiva.expression.ExpressionTree;
+import org.apache.arrow.gandiva.expression.TreeBuilder;
+import org.apache.arrow.gandiva.expression.TreeNode;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Regression tests for the UNPIVOT-style nested-CASE expression that originally crashed the JVM
+ * in native Gandiva code. The expression shape is
+ *
+ *
+ * CAST(CASE WHEN key = 'lit_0' THEN v0
+ * WHEN key = 'lit_1' THEN v1
+ * ...
+ * WHEN key = 'lit_392' THEN v392
+ * ELSE NULL
+ * END AS VARCHAR(65536))
+ *
+ *
+ * Calcite/Gandiva lower this into nested {@code if/else} nodes, one per WHEN branch. Without a
+ * Java-side guard the resulting 393-deep AST blows the native stack during {@code Projector.make}
+ * / {@code Filter.make}. With {@link ExpressionGuard} in place these deep variants must be
+ * rejected cleanly with a {@link GandivaException} naming the depth limit; shallow variants must
+ * still compile and evaluate normally.
+ */
+public class UnpivotCaseStackOverflowTest extends BaseEvaluatorTest {
+
+ private static ProjectionBundle buildNestedCaseProjection(int numBranches) {
+ ArrowType utf8 = new ArrowType.Utf8();
+
+ Field keyField = Field.nullable("key", utf8);
+ TreeNode keyNode = TreeBuilder.makeField(keyField);
+
+ List schemaFields = new ArrayList<>();
+ schemaFields.add(keyField);
+
+ List valueNodes = new ArrayList<>(numBranches);
+ List literals = new ArrayList<>(numBranches);
+ for (int i = 0; i < numBranches; i++) {
+ Field vi = Field.nullable("v" + i, utf8);
+ schemaFields.add(vi);
+ valueNodes.add(TreeBuilder.makeField(vi));
+ literals.add("peg-krones.ns=3;s=V:0/3/" + i + ".value");
+ }
+
+ TreeNode current = TreeBuilder.makeNull(utf8);
+ for (int i = numBranches - 1; i >= 0; i--) {
+ TreeNode cond =
+ TreeBuilder.makeFunction(
+ "equal",
+ Lists.newArrayList(keyNode, TreeBuilder.makeStringLiteral(literals.get(i))),
+ new ArrowType.Bool());
+ current = TreeBuilder.makeIf(cond, valueNodes.get(i), current, utf8);
+ }
+
+ TreeNode casted =
+ TreeBuilder.makeFunction(
+ "castVARCHAR",
+ Lists.newArrayList(current, TreeBuilder.makeLiteral(65536L)),
+ utf8);
+
+ ExpressionTree expr = TreeBuilder.makeExpression(casted, Field.nullable("value", utf8));
+ return new ProjectionBundle(new Schema(schemaFields), expr);
+ }
+
+ private static FilterBundle buildNestedCaseFilter(int numBranches) {
+ ProjectionBundle proj = buildNestedCaseProjection(numBranches);
+ // The projector tree is utf8-typed; wrap its root in isnotnull(...) so the whole expression
+ // becomes a Bool condition usable by Filter. The if-chain depth — not the result type — is
+ // what blows the native stack.
+ TreeNode root =
+ TreeBuilder.makeFunction(
+ "isnotnull",
+ Lists.newArrayList(rebuildNestedCaseRoot(numBranches)),
+ new ArrowType.Bool());
+ return new FilterBundle(proj.schema, TreeBuilder.makeCondition(root));
+ }
+
+ /** Identical to the body of buildNestedCaseProjection but exposes the raw root TreeNode. */
+ private static TreeNode rebuildNestedCaseRoot(int numBranches) {
+ ArrowType utf8 = new ArrowType.Utf8();
+ Field keyField = Field.nullable("key", utf8);
+ TreeNode keyNode = TreeBuilder.makeField(keyField);
+
+ List valueNodes = new ArrayList<>(numBranches);
+ for (int i = 0; i < numBranches; i++) {
+ valueNodes.add(TreeBuilder.makeField(Field.nullable("v" + i, utf8)));
+ }
+
+ TreeNode current = TreeBuilder.makeNull(utf8);
+ for (int i = numBranches - 1; i >= 0; i--) {
+ TreeNode cond =
+ TreeBuilder.makeFunction(
+ "equal",
+ Lists.newArrayList(
+ keyNode,
+ TreeBuilder.makeStringLiteral(
+ "peg-krones.ns=3;s=V:0/3/" + i + ".value")),
+ new ArrowType.Bool());
+ current = TreeBuilder.makeIf(cond, valueNodes.get(i), current, utf8);
+ }
+ return TreeBuilder.makeFunction(
+ "castVARCHAR", Lists.newArrayList(current, TreeBuilder.makeLiteral(65536L)), utf8);
+ }
+
+ /**
+ * Reproduces the failing plan exactly: 393 WHEN branches (the count in the Dremio plan that
+ * blew up LLVM). Without the guard this crashes the JVM with a native SIGSEGV during
+ * {@code Projector.make}; with the guard wired in it must be rejected with a
+ * {@link GandivaException} naming the depth limit.
+ */
+ @Test
+ public void testUnpivotCaseStackOverflow() {
+ ProjectionBundle bundle = buildNestedCaseProjection(393);
+ GandivaException ex =
+ assertThrows(
+ GandivaException.class,
+ () -> Projector.make(bundle.schema, Lists.newArrayList(bundle.expression)));
+ assertTrue(ex.getMessage().contains("depth"), ex.getMessage());
+ }
+
+ /** A shallow version of the same shape compiles fine — sanity that ordinary CASEs still work. */
+ @Test
+ public void testUnpivotCaseShallow() throws GandivaException {
+ ProjectionBundle bundle = buildNestedCaseProjection(8);
+ Projector eval = Projector.make(bundle.schema, Lists.newArrayList(bundle.expression));
+ eval.close();
+ }
+
+ /** Filter variant of the same shape: rejected by the guard for the same reason. */
+ @Test
+ public void testUnpivotCaseStackOverflowFilter() {
+ FilterBundle bundle = buildNestedCaseFilter(393);
+ GandivaException ex =
+ assertThrows(
+ GandivaException.class, () -> Filter.make(bundle.schema, bundle.condition));
+ assertTrue(ex.getMessage().contains("depth"), ex.getMessage());
+ }
+
+ /** Shallow filter sanity check matching the projector shallow test. */
+ @Test
+ public void testUnpivotCaseShallowFilter() throws GandivaException {
+ FilterBundle bundle = buildNestedCaseFilter(8);
+ Filter filter = Filter.make(bundle.schema, bundle.condition);
+ filter.close();
+ }
+
+ private static final class FilterBundle {
+ final Schema schema;
+ final Condition condition;
+
+ FilterBundle(Schema schema, Condition condition) {
+ this.schema = schema;
+ this.condition = condition;
+ }
+ }
+
+ private static final class ProjectionBundle {
+ final Schema schema;
+ final ExpressionTree expression;
+
+ ProjectionBundle(Schema schema, ExpressionTree expression) {
+ this.schema = schema;
+ this.expression = expression;
+ }
+ }
+}