This is an automated email from the ASF dual-hosted git repository.

yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 5b6c7a2983c Fix missing UNNEST input columns in multi-stage EXPLAIN 
when asking servers (#19771)
5b6c7a2983c is described below

commit 5b6c7a2983c305c486901a9e1010a18608496d66
Author: Yash Mayya <[email protected]>
AuthorDate: Wed Oct 7 08:35:17 2026 -0700

    Fix missing UNNEST input columns in multi-stage EXPLAIN when asking servers 
(#19771)
---
 .../planner/logical/PlanNodeToRelConverter.java    | 47 +++++++++++++++++++
 .../logical/PlanNodeToRelConverterTest.java        | 52 ++++++++++++++++++++++
 2 files changed, 99 insertions(+)

diff --git 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverter.java
 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverter.java
index 2ff2e9289bb..5c7dd4346ae 100644
--- 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverter.java
+++ 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverter.java
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
@@ -39,10 +40,15 @@ import org.apache.calcite.rel.logical.LogicalIntersect;
 import org.apache.calcite.rel.logical.LogicalMinus;
 import org.apache.calcite.rel.logical.LogicalSort;
 import org.apache.calcite.rel.logical.LogicalUnion;
+import org.apache.calcite.rel.logical.LogicalValues;
 import org.apache.calcite.rel.logical.LogicalWindow;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCorrelVariable;
+import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.rex.RexWindowBound;
 import org.apache.calcite.rex.RexWindowBounds;
 import org.apache.calcite.rex.RexWindowExclusion;
@@ -502,6 +508,13 @@ public final class PlanNodeToRelConverter {
         if (arrayProjects.isEmpty()) {
           arrayProjects.add(_builder.field(0));
         }
+        // CROSS JOIN UNNEST also outputs the input columns, which parent 
nodes may reference
+        int numPassthrough =
+            node.getDataSchema().size() - arrayProjects.size() - 
(node.isWithOrdinality() ? 1 : 0);
+        if (numPassthrough > 0) {
+          pushCorrelatedUncollect(node, input, arrayProjects);
+          return null;
+        }
         _builder.project(arrayProjects);
         RelNode project = _builder.build();
 
@@ -517,6 +530,40 @@ public final class PlanNodeToRelConverter {
       return null;
     }
 
+    /// Replaces the input on top of the builder stack with the logical plan 
shape of CROSS JOIN UNNEST: a Correlate of
+    /// the input with an Uncollect that reads the arrays through the 
correlation variable. Like the [UnnestNode], it
+    /// outputs the input columns followed by the element (and ordinality) 
columns.
+    private void pushCorrelatedUncollect(UnnestNode node, RelNode input, 
List<RexNode> arrayProjects) {
+      RexBuilder rexBuilder = _builder.getRexBuilder();
+      RexCorrelVariable correlVariable =
+          (RexCorrelVariable) rexBuilder.makeCorrel(input.getRowType(), 
_builder.getCluster().createCorrel());
+      RexShuttle toCorrelated = new RexShuttle() {
+        @Override
+        public RexNode visitInputRef(RexInputRef inputRef) {
+          return rexBuilder.makeFieldAccess(correlVariable, 
inputRef.getIndex());
+        }
+      };
+      List<String> inputNames = input.getRowType().getFieldNames();
+      List<String> arrayNames = new ArrayList<>(arrayProjects.size());
+      for (RexNode arrayProject : arrayProjects) {
+        arrayNames.add(
+            arrayProject instanceof RexInputRef ? 
inputNames.get(((RexInputRef) arrayProject).getIndex()) : null);
+      }
+      List<RexNode> requiredColumns = 
_builder.fields(RelOptUtil.InputFinder.bits(arrayProjects, null));
+      _builder.push(LogicalValues.createOneRow(_builder.getCluster()))
+          .project(toCorrelated.apply(arrayProjects), arrayNames)
+          .uncollect(List.of(), node.isWithOrdinality())
+          .correlate(JoinRelType.INNER, correlVariable.id, requiredColumns);
+
+      if (node.isPrunedPassthrough()) {
+        // Keep only the passthrough input columns, followed by the element 
(and ordinality) columns
+        List<RexNode> fields = _builder.fields();
+        List<RexNode> projects = new 
ArrayList<>(_builder.fields(node.getPassthroughInputIndexes()));
+        projects.addAll(fields.subList(inputNames.size(), fields.size()));
+        _builder.project(projects);
+      }
+    }
+
     @Override
     public Void visitExchange(ExchangeNode exchangeNode, Void context) {
       visitChildren(exchangeNode);
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverterTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverterTest.java
index 646d2ce4911..8f9f63285bc 100644
--- 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverterTest.java
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/planner/logical/PlanNodeToRelConverterTest.java
@@ -30,6 +30,7 @@ import org.apache.pinot.query.planner.plannode.AggregateNode;
 import org.apache.pinot.query.planner.plannode.ExplainedNode;
 import org.apache.pinot.query.planner.plannode.PlanNode;
 import org.apache.pinot.query.planner.plannode.ProjectNode;
+import org.apache.pinot.query.planner.plannode.UnnestNode;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
@@ -81,4 +82,55 @@ public class PlanNodeToRelConverterTest {
         "LogicalAggregate(group=[{1, 2}], groups=[[{1, 2}, {1}, {}]], 
agg#0=[MAX($3)])\n"
             + "  Leaf\n");
   }
+
+  @Test
+  public void testAggregateOverUnnest() {
+    // COUNT(*) ... CROSS JOIN UNNEST(b) AS u(elem) GROUP BY a, elem, where 
the unnest output is [a, b, elem]
+    PlanNode unnest = new UnnestNode(0, new DataSchema(new String[]{"a", "b", 
"elem"},
+        new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.STRING_ARRAY, 
ColumnDataType.STRING}), null,
+        List.of(unnestLeaf()), List.of(new RexExpression.InputRef(1)),
+        new UnnestNode.TableFunctionContext(false, List.of(2), 
UnnestNode.UNSPECIFIED_INDEX));
+    DataSchema aggregateSchema = new DataSchema(new String[]{"a", "elem", 
"cnt"},
+        new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.STRING, 
ColumnDataType.LONG});
+    RexExpression.FunctionCall count = new 
RexExpression.FunctionCall(ColumnDataType.LONG, "COUNT", List.of());
+    PlanNode aggregate = new AggregateNode(0, aggregateSchema, null, 
List.of(unnest), List.of(count), List.of(-1),
+        List.of(0, 2), AggregateNode.AggType.LEAF, false, null, 0);
+
+    RelBuilder relBuilder = 
RelBuilder.create(Frameworks.newConfigBuilder().build());
+    RelNode relNode = PlanNodeToRelConverter.convert(relBuilder, aggregate);
+    assertEquals(RelOptUtil.toString(relNode),
+        "LogicalAggregate(group=[{0, 2}], agg#0=[COUNT()])\n"
+            + "  LogicalCorrelate(correlation=[$cor0], joinType=[inner], 
requiredColumns=[{1}])\n"
+            + "    Leaf\n"
+            + "    Uncollect\n"
+            + "      LogicalProject(b=[$cor0.b])\n"
+            + "        LogicalValues(tuples=[[{ 0 }]])\n");
+  }
+
+  @Test
+  public void testProjectOverPrunedUnnest() {
+    // SELECT a, elem, ord ... CROSS JOIN UNNEST(b) WITH ORDINALITY AS u(elem, 
ord), where the unnest output drops b
+    PlanNode unnest = new UnnestNode(0, new DataSchema(new String[]{"a", 
"elem", "ord"},
+        new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.STRING, 
ColumnDataType.INT}), null,
+        List.of(unnestLeaf()), List.of(new RexExpression.InputRef(1)),
+        new UnnestNode.TableFunctionContext(true, List.of(1), 2, List.of(0), 
true));
+    PlanNode project = new ProjectNode(0, unnest.getDataSchema(), null, 
List.of(unnest),
+        List.of(new RexExpression.InputRef(0), new RexExpression.InputRef(1), 
new RexExpression.InputRef(2)));
+
+    RelBuilder relBuilder = 
RelBuilder.create(Frameworks.newConfigBuilder().build());
+    RelNode relNode = PlanNodeToRelConverter.convert(relBuilder, project);
+    assertEquals(RelOptUtil.toString(relNode),
+        "LogicalProject(a=[$0], b=[$2], ORDINALITY=[$3])\n"
+            + "  LogicalCorrelate(correlation=[$cor0], joinType=[inner], 
requiredColumns=[{1}])\n"
+            + "    Leaf\n"
+            + "    Uncollect(withOrdinality=[true])\n"
+            + "      LogicalProject(b=[$cor0.b])\n"
+            + "        LogicalValues(tuples=[[{ 0 }]])\n");
+  }
+
+  private static PlanNode unnestLeaf() {
+    DataSchema leafSchema = new DataSchema(new String[]{"a", "b"},
+        new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.STRING_ARRAY});
+    return new ExplainedNode(0, leafSchema, null, List.of(), "Leaf", Map.of());
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to