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 72ebec4fa21 Make logical EXPLAIN fail wherever query execution would 
fail (#19373)
72ebec4fa21 is described below

commit 72ebec4fa21582647ea5f7e73c66dbd5a408d565
Author: Yash Mayya <[email protected]>
AuthorDate: Thu Aug 27 12:05:05 2026 -0400

    Make logical EXPLAIN fail wherever query execution would fail (#19373)
---
 .../java/org/apache/pinot/query/QueryEnvironment.java   | 15 ++++++++++++++-
 .../org/apache/pinot/query/QueryCompilationTest.java    | 17 +++++++++++------
 .../query/queries/ResourceBasedQueryPlansTest.java      |  2 --
 .../src/test/resources/queries/JoinPlans.json           |  5 +++++
 .../src/test/resources/queries/ValidationErrorPlan.json |  5 +++++
 5 files changed, 35 insertions(+), 9 deletions(-)

diff --git 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
index 26fd33b32a2..bfa24cac481 100644
--- 
a/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
+++ 
b/pinot-query-planner/src/main/java/org/apache/pinot/query/QueryEnvironment.java
@@ -955,6 +955,9 @@ public class QueryEnvironment {
     /// Explain the query plan.
     /// The original query must be an EXPLAIN query and way it will be 
explained depends on the options of the EXPLAIN
     /// query and the [QueryEnvironment.Config] used to create the 
[QueryEnvironment] that compiled this query.
+    ///
+    /// Unless the query explicitly asks for a logical plan only (`EXPLAIN 
PLAN WITHOUT IMPLEMENTATION`), this builds
+    /// the dispatchable subplan and therefore fails wherever [#planQuery] 
would.
     public QueryEnvironment.QueryPlannerResult explain(long requestId,
         @Nullable AskingServerStageExplainer.OnServerExplainer 
onServerExplainer) {
       try {
@@ -971,9 +974,17 @@ public class QueryEnvironment {
           SqlExplainLevel level =
               explain.getDetailLevel() == null ? 
SqlExplainLevel.DIGEST_ATTRIBUTES : explain.getDetailLevel();
           Set<String> tableNames = 
RelToPlanNodeConverter.getTableNamesFromRelRoot(_relRoot.rel);
-          if (!explain.withImplementation() || onServerExplainer == null) {
+          if (!explain.withImplementation()) {
+            // The query explicitly asked to skip implementation planning, so 
render the logical plan as-is.
             return getQueryPlannerResult(_plannerContext, null, 
PlannerUtils.explainPlan(_relRoot.rel, format, level),
                 tableNames);
+          } else if (onServerExplainer == null) {
+            // Build the dispatchable subplan even though only the logical 
plan is rendered: some planning errors are
+            // raised while converting the plan or assigning workers (e.g. a 
`tableOptions` partition hint that
+            // disagrees with the table's actual partitioning), and EXPLAIN 
must fail wherever execution would.
+            DispatchableSubPlan dispatchableSubPlan = 
toDispatchableSubPlan(_relRoot, _plannerContext);
+            return getQueryPlannerResult(_plannerContext, dispatchableSubPlan,
+                PlannerUtils.explainPlan(_relRoot.rel, format, level), 
dispatchableSubPlan.getTableNames());
           } else {
             Map<String, String> options = _sqlNodeAndOptions.getOptions();
             boolean explainPlanVerbose = 
QueryOptionsUtils.isExplainPlanVerbose(options);
@@ -995,6 +1006,8 @@ public class QueryEnvironment {
                 PlannerUtils.explainPlan(explainedNode, format, level), 
dispatchableSubPlan.getTableNames());
           }
         }
+      } catch (QueryException e) {
+        throw e;
       } catch (Exception e) {
         throw new RuntimeException("Error explain query plan for: " + 
_textQuery, e);
       }
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
index 6376839d748..30f74e5d60c 100644
--- 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
@@ -568,9 +568,11 @@ public class QueryCompilationTest extends 
QueryEnvironmentTestBase {
   public void testJoinPushTransitivePredicateLookupJoin() {
     // PinotJoinPushTransitivePredicatesRule
     // should not push to the right under lookup join hint
+    // NOTE: Selects explicit columns because `*` leaves no Project at all 
over the right table scan, which a lookup
+    // join requires (see RelToPlanNodeConverter#convertLogicalJoin). That 
case is covered in JoinPlans.json.
     String query = "EXPLAIN PLAN FOR\n"
         + "SELECT /*+ joinOptions(join_strategy='lookup') */ \n"
-        + "* FROM a\n"
+        + "a.col1, b.col2 FROM a\n"
         + "JOIN b\n"
         + "ON a.col1 = b.col1\n"
         + "WHERE a.col1 = 1;\n";
@@ -579,11 +581,14 @@ public class QueryCompilationTest extends 
QueryEnvironmentTestBase {
     //@formatter:off
     assertEquals(explain,
         "Execution Plan\n"
-            + "LogicalJoin(condition=[=($0, $9)], joinType=[inner])\n"
-            + "  PinotLogicalExchange(distribution=[single])\n"
-            + "    LogicalFilter(condition=[=(CAST($0):INTEGER NOT NULL, 
1)])\n"
-            + "      PinotLogicalTableScan(table=[[default, a]])\n"
-            + "  PinotLogicalTableScan(table=[[default, b]])\n");
+            + "LogicalProject(col1=[$0], col2=[$2])\n"
+            + "  LogicalJoin(condition=[=($0, $1)], joinType=[inner])\n"
+            + "    PinotLogicalExchange(distribution=[single])\n"
+            + "      LogicalProject(col1=[$0])\n"
+            + "        LogicalFilter(condition=[=(CAST($0):INTEGER NOT NULL, 
1)])\n"
+            + "          PinotLogicalTableScan(table=[[default, a]])\n"
+            + "    LogicalProject(col1=[$0], col2=[$1])\n"
+            + "      PinotLogicalTableScan(table=[[default, b]])\n");
     //@formatter:on
   }
 
diff --git 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/queries/ResourceBasedQueryPlansTest.java
 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/queries/ResourceBasedQueryPlansTest.java
index 33f5977bc02..28bd8af259a 100644
--- 
a/pinot-query-planner/src/test/java/org/apache/pinot/query/queries/ResourceBasedQueryPlansTest.java
+++ 
b/pinot-query-planner/src/test/java/org/apache/pinot/query/queries/ResourceBasedQueryPlansTest.java
@@ -71,8 +71,6 @@ public class ResourceBasedQueryPlansTest extends 
QueryEnvironmentTestBase {
     try {
       long requestId = RANDOM_REQUEST_ID_GEN.nextLong();
       _queryEnvironment.explainQuery(query, requestId);
-      String queryWithoutExplainPlan = query.replaceFirst(EXPLAIN_REGEX, "");
-      _queryEnvironment.planQuery(queryWithoutExplainPlan);
       Assert.fail("Query compilation should have failed with exception message 
pattern: " + expectedException);
     } catch (Exception e) {
       if (expectedException == null) {
diff --git a/pinot-query-planner/src/test/resources/queries/JoinPlans.json 
b/pinot-query-planner/src/test/resources/queries/JoinPlans.json
index a96520741ea..77a234e39f7 100644
--- a/pinot-query-planner/src/test/resources/queries/JoinPlans.json
+++ b/pinot-query-planner/src/test/resources/queries/JoinPlans.json
@@ -1133,6 +1133,11 @@
         "description": "Lookup join with transformation on right table joined 
key",
         "sql": "EXPLAIN PLAN FOR SELECT /*+ joinOptions(join_strategy = 
'lookup') */ a.col1, b.col2 FROM a JOIN b ON a.col1 = upper(b.col1)",
         "expectedException": "Right input for lookup join must be an 
identifier.*"
+      },
+      {
+        "description": "Lookup join selecting all columns leaves no Project 
over the right table scan",
+        "sql": "EXPLAIN PLAN FOR SELECT /*+ joinOptions(join_strategy = 
'lookup') */ * FROM a JOIN b ON a.col1 = b.col1 WHERE a.col1 = 1",
+        "expectedException": "Right input for lookup join must be a Project.*"
       }
     ]
   }
diff --git 
a/pinot-query-planner/src/test/resources/queries/ValidationErrorPlan.json 
b/pinot-query-planner/src/test/resources/queries/ValidationErrorPlan.json
index 980f521586d..494f1cf138f 100644
--- a/pinot-query-planner/src/test/resources/queries/ValidationErrorPlan.json
+++ b/pinot-query-planner/src/test/resources/queries/ValidationErrorPlan.json
@@ -11,6 +11,11 @@
         "sql": "EXPLAIN PLAN FOR SELECT SUM(a.col3) as sumCol3, 
arrayToMv(e.mcol1), a.col2  FROM a JOIN e on a.col1=e.col1 GROUP BY 
arrayToMv(e.mcol1), a.col2",
         "expectedException": ".*'ArrayToMv' is not supported.*"
       },
+      {
+        "description": "arrayToMV validation error 2 (physical optimizer)",
+        "sql": "SET usePhysicalOptimizer=true; EXPLAIN PLAN FOR SELECT 
SUM(a.col3) as sumCol3, arrayToMv(e.mcol1), a.col2  FROM a JOIN e on 
a.col1=e.col1 GROUP BY arrayToMv(e.mcol1), a.col2",
+        "expectedException": ".*'ArrayToMv' is not supported.*"
+      },
       {
         "description": "Select * with negative limit -1",
         "sql": "EXPLAIN PLAN FOR SELECT * FROM d LIMIT -1",


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

Reply via email to