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: + * + *

+ * + *

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; + } + } +}