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

apurtell pushed a commit to branch PHOENIX-7876-feature
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/PHOENIX-7876-feature by this 
push:
     new a4b24f1f1e PHOENIX-7917 Expand the EXPLAIN WITH options list grammar 
(#2525)
a4b24f1f1e is described below

commit a4b24f1f1e4cfd3106a0ffa60a449eb9b1e16f45
Author: Andrew Purtell <[email protected]>
AuthorDate: Fri Jun 12 17:31:03 2026 -0700

    PHOENIX-7917 Expand the EXPLAIN WITH options list grammar (#2525)
    
    Co-authored-by: Claude Opus 4.8[1m] <[email protected]>
    Co-authored-by: Copilot <[email protected]>
---
 phoenix-core-client/src/main/antlr3/PhoenixSQL.g   |  55 ++++++-
 .../apache/phoenix/compile/StatementContext.java   |  15 ++
 .../org/apache/phoenix/iterate/ExplainTable.java   |   5 +
 .../org/apache/phoenix/jdbc/PhoenixStatement.java  |  40 +++---
 .../org/apache/phoenix/parse/ExplainOptions.java   | 126 ++++++++++++++++
 .../org/apache/phoenix/parse/ExplainStatement.java |  10 +-
 .../java/org/apache/phoenix/parse/ExplainType.java |  26 ----
 .../org/apache/phoenix/parse/ParseNodeFactory.java |   4 +-
 .../apache/phoenix/end2end/BaseAggregateIT.java    |  15 +-
 .../phoenix/end2end/FlappingLocalIndexIT.java      |   5 +-
 .../org/apache/phoenix/end2end/QueryLoggerIT.java  |   6 +-
 .../apache/phoenix/end2end/index/BaseIndexIT.java  |   7 +-
 .../phoenix/parse/ExplainOptionsParserTest.java    | 159 +++++++++++++++++++++
 .../phoenix/query/explain/ExplainPlanTestUtil.java |  27 ++++
 14 files changed, 425 insertions(+), 75 deletions(-)

diff --git a/phoenix-core-client/src/main/antlr3/PhoenixSQL.g 
b/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
index bb7d3d8a89..c84d8f3b54 100644
--- a/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
+++ b/phoenix-core-client/src/main/antlr3/PhoenixSQL.g
@@ -233,7 +233,7 @@ import org.apache.phoenix.util.SchemaUtil;
 import org.apache.phoenix.parse.LikeParseNode.LikeType;
 import org.apache.phoenix.trace.util.Tracing;
 import org.apache.phoenix.parse.AddJarsStatement;
-import org.apache.phoenix.parse.ExplainType;
+import org.apache.phoenix.parse.ExplainOptions;
 }
 
 @lexer::header {
@@ -318,6 +318,38 @@ package org.apache.phoenix.parse;
          }
     }
 
+    /**
+     * Parse a single EXPLAIN option into the given builder. The option name 
is matched
+     * case-insensitively against the closed set {REGIONS, VERBOSE, FORMAT}. 
REGIONS and VERBOSE are
+     * flags and must not carry a value; FORMAT requires a value of TEXT or 
JSON.
+     */
+    private void parseExplainOption(ExplainOptions.Builder opts, String name, 
String value) {
+        if ("REGIONS".equalsIgnoreCase(name)) {
+            if (value != null) {
+                throw new RuntimeException("EXPLAIN option REGIONS does not 
take a value");
+            }
+            opts.setRegions(true);
+        } else if ("VERBOSE".equalsIgnoreCase(name)) {
+            if (value != null) {
+                throw new RuntimeException("EXPLAIN option VERBOSE does not 
take a value");
+            }
+            opts.setVerbose(true);
+        } else if ("FORMAT".equalsIgnoreCase(name)) {
+            if (value == null) {
+                throw new RuntimeException("EXPLAIN option FORMAT requires a 
value: TEXT or JSON");
+            }
+            if ("TEXT".equalsIgnoreCase(value)) {
+                opts.setFormat(ExplainOptions.Format.TEXT);
+            } else if ("JSON".equalsIgnoreCase(value)) {
+                opts.setFormat(ExplainOptions.Format.JSON);
+            } else {
+                throw new RuntimeException("Unknown EXPLAIN FORMAT: " + value);
+            }
+        } else {
+            throw new RuntimeException("Unknown EXPLAIN option: " + name);
+        }
+    }
+
     protected Object recoverFromMismatchedToken(IntStream input, int ttype, 
BitSet follow)
         throws RecognitionException {
         RecognitionException e = null;
@@ -477,16 +509,25 @@ oneStatement returns [BindableStatement ret]
 finally{ contextStack.pop(); }
     
 explain_node returns [BindableStatement ret]
-    :   EXPLAIN (w=WITH)? (r=REGIONS)? q=oneStatement
+@init { ExplainOptions.Builder opts = new ExplainOptions.Builder(); }
+    :   EXPLAIN
+        (
+            (LPAREN explain_option[opts] (COMMA explain_option[opts])* RPAREN)
+          | (WITH REGIONS                   { opts.setRegions(true); })
+        )?
+        q=oneStatement
      {
-        if ((w==null && r!=null) || (w!=null && r==null)) {
-            throw new RuntimeException("Valid usage: EXPLAIN {query} OR 
EXPLAIN WITH REGIONS {query}");
-        }
-        ret = (w==null && r==null) ? factory.explain(q, ExplainType.DEFAULT)
-         : factory.explain(q, ExplainType.WITH_REGIONS);
+        ret = factory.explain(q, opts.build());
      }
     ;
 
+// A single EXPLAIN option. REGIONS is a global keyword token. The remaining 
option keywords
+// (VERBOSE, FORMAT, TEXT, JSON) are not reserved and arrive as NAME tokens, 
validated in the action.
+explain_option[ExplainOptions.Builder opts]
+    :   REGIONS                 { opts.setRegions(true); }
+    |   k=NAME (v=NAME)?        { parseExplainOption(opts, k.getText(), v == 
null ? null : v.getText()); }
+    ;
+
 // Parse a create table statement.
 create_table_node returns [CreateTableStatement ret]
     :   CREATE (im=IMMUTABLE)? TABLE (IF NOT ex=EXISTS)? t=from_table_name 
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
index c2ad404027..0c89d1c82c 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
@@ -41,6 +41,7 @@ import org.apache.phoenix.monitoring.ReadMetricQueue;
 import org.apache.phoenix.monitoring.ScanMetricsHolder;
 import org.apache.phoenix.monitoring.SlowestScanMetricsQueue;
 import org.apache.phoenix.monitoring.TopNTreeMultiMap;
+import org.apache.phoenix.parse.ExplainOptions;
 import org.apache.phoenix.parse.ParseNode;
 import org.apache.phoenix.parse.SelectStatement;
 import org.apache.phoenix.query.QueryConstants;
@@ -112,6 +113,7 @@ public class StatementContext {
   private Set<Pair<String, String>> partialIndexCheckedSet;
   private Map<String, List<Expression>> serverParsedProjections;
   private StatementContext parentContext;
+  private ExplainOptions explainOptions = ExplainOptions.DEFAULT;
 
   public StatementContext(PhoenixStatement statement) {
     this(statement, new Scan());
@@ -156,6 +158,7 @@ public class StatementContext {
     this.partialIndexCheckedSet = context.partialIndexCheckedSet;
     this.serverParsedProjections = context.serverParsedProjections;
     this.parentContext = context.parentContext;
+    this.explainOptions = context.explainOptions;
   }
 
   /**
@@ -607,6 +610,18 @@ public class StatementContext {
     return parentContext;
   }
 
+  /**
+   * The options parsed from the {@code EXPLAIN} statement's option list. 
Defaults to
+   * {@link ExplainOptions#DEFAULT}.
+   */
+  public ExplainOptions getExplainOptions() {
+    return explainOptions;
+  }
+
+  public void setExplainOptions(ExplainOptions explainOptions) {
+    this.explainOptions = explainOptions == null ? ExplainOptions.DEFAULT : 
explainOptions;
+  }
+
   /** Returns true if this is the top-level (root) statement context, i.e. it 
has no parent. */
   public boolean isRoot() {
     return parentContext == null;
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
index 62b8ed556f..a096fd574c 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java
@@ -571,6 +571,11 @@ public abstract class ExplainTable {
   private void getRegionLocations(List<String> planSteps,
     ExplainPlanAttributesBuilder explainPlanAttributesBuilder,
     List<HRegionLocation> regionLocations) {
+    // Region locations are emitted as text and in the structured attributes 
only when the
+    // EXPLAIN statement requested them via the REGIONS option (or the legacy 
WITH REGIONS alias).
+    if (!context.getExplainOptions().isRegions()) {
+      return;
+    }
     String regionLocationPlan =
       getRegionLocationsForExplainPlan(explainPlanAttributesBuilder, 
regionLocations);
     if (regionLocationPlan.length() > 0) {
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
index 0b982288a4..b6b89aa646 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
@@ -162,8 +162,8 @@ import org.apache.phoenix.parse.DropSchemaStatement;
 import org.apache.phoenix.parse.DropSequenceStatement;
 import org.apache.phoenix.parse.DropTableStatement;
 import org.apache.phoenix.parse.ExecuteUpgradeStatement;
+import org.apache.phoenix.parse.ExplainOptions;
 import org.apache.phoenix.parse.ExplainStatement;
-import org.apache.phoenix.parse.ExplainType;
 import org.apache.phoenix.parse.FetchStatement;
 import org.apache.phoenix.parse.FilterableStatement;
 import org.apache.phoenix.parse.HintNode;
@@ -393,8 +393,8 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
             // Send mutations to hbase, so they are visible to subsequent 
reads.
             // Use original plan for data table so that data and immutable 
indexes will be sent
             // TODO: for joins, we need to iterate through all tables, but we 
need the original
-            // table,
-            // not the projected table, so 
plan.getContext().getResolver().getTables() won't work.
+            // table, not the projected table, so 
plan.getContext().getResolver().getTables()
+            // won't work.
             if (plan.getContext().getScanRanges().isPointLookup()) {
               pointLookup = true;
             }
@@ -445,12 +445,10 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
             overallQuerymetrics.startQuery();
             
overallQuerymetrics.setQueryParsingTimeMS(getSqlQueryParsingTime());
             rs = newResultSet(resultIterator, plan.getProjector(), 
plan.getContext());
-            // newResultset sets lastResultset
-            // 
ExecutableShowCreateTable/ExecutableShowTablesStatement/ExecutableShowSchemasStatement
-            // using a delegateStmt
+            // newResultset sets lastResultset ExecutableShowCreateTable /
+            // ExecutableShowTablesStatement / ExecutableShowSchemasStatement 
using a delegateStmt
             // to compile a queryPlan, the resultSet will set to the 
delegateStmt, so need set
-            // resultSet
-            // to the origin statement.
+            // resultSet to the origin statement.
             setLastResultSet(rs);
             setLastQueryPlan(plan);
             setLastUpdateCount(NO_UPDATE);
@@ -771,11 +769,9 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
                   ) {
                     
TableMetricsManager.updateLatencyHistogramForMutations(tableName,
                       executeMutationTimeSpent, false);
-                    // We won't have size histograms for delete mutations when 
auto commit is set to
-                    // true and
-                    // if plan is of ServerSelectDeleteMutationPlan or
-                    // ServerUpsertSelectMutationPlan
-                    // since the update happens on server.
+                    // We won't have size histograms for delete mutations when 
auto commit is set
+                    // to true and if plan is of 
ServerSelectDeleteMutationPlan or
+                    // ServerUpsertSelectMutationPlan since the update happens 
on server.
                   } else {
                     state.addExecuteMutationTime(executeMutationTimeSpent, 
tableName);
                   }
@@ -963,8 +959,8 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
   private static class ExecutableExplainStatement extends ExplainStatement
     implements CompilableStatement {
 
-    ExecutableExplainStatement(BindableStatement statement, ExplainType 
explainType) {
-      super(statement, explainType);
+    ExecutableExplainStatement(BindableStatement statement, ExplainOptions 
options) {
+      super(statement, options);
     }
 
     @Override
@@ -1003,16 +999,16 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
           
stmt.getConnection().getQueryServices().getOptimizer().optimize(stmt, dataPlan);
       }
       final StatementPlan plan = compilePlan;
+      // Propagate the parsed EXPLAIN options onto the resolved plan's context 
so the renderer has
+      // access to the requested options.
+      if (plan.getContext() != null) {
+        plan.getContext().setExplainOptions(getOptions());
+      }
       ExplainPlan explainPlan = plan.getExplainPlan();
       // Prepend the top-of-plan disclosure blocks. This is the only place the 
disclosure text is
       // emitted.
       List<String> planSteps = new ArrayList<>(explainPlan.getPlanSteps());
       ExplainTable.renderTopOfPlanText(planSteps, 
explainPlan.getPlanStepsAsAttributes());
-      ExplainType explainType = getExplainType();
-      if (explainType == ExplainType.DEFAULT) {
-        planSteps.removeIf(
-          planStep -> planStep != null && 
planStep.contains(ExplainTable.REGION_LOCATIONS));
-      }
       planSteps = Collections.unmodifiableList(planSteps);
       List<Tuple> tuples = 
Lists.newArrayListWithExpectedSize(planSteps.size());
       Long estimatedBytesToScan = plan.getEstimatedBytesToScan();
@@ -2397,8 +2393,8 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
     }
 
     @Override
-    public ExplainStatement explain(BindableStatement statement, ExplainType 
explainType) {
-      return new ExecutableExplainStatement(statement, explainType);
+    public ExplainStatement explain(BindableStatement statement, 
ExplainOptions options) {
+      return new ExecutableExplainStatement(statement, options);
     }
 
     @Override
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainOptions.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainOptions.java
new file mode 100644
index 0000000000..ca8f4bc4c3
--- /dev/null
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainOptions.java
@@ -0,0 +1,126 @@
+/*
+ * 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.phoenix.parse;
+
+import java.util.Objects;
+
+/**
+ * POJO carrying the options parsed from an {@code EXPLAIN} statement's option 
list
+ * ({@code EXPLAIN [(<opt> [, <opt>]*)] <stmt>}).
+ */
+public final class ExplainOptions {
+
+  /** Output format selected by the {@code FORMAT} option. */
+  public enum Format {
+    TEXT,
+    JSON
+  }
+
+  public static final ExplainOptions DEFAULT = new ExplainOptions(false, 
false, Format.TEXT);
+  public static final ExplainOptions WITH_REGIONS = new ExplainOptions(true, 
false, Format.TEXT);
+
+  private final boolean regions;
+  private final boolean verbose;
+  private final Format format;
+
+  public ExplainOptions(boolean regions, boolean verbose, Format format) {
+    this.regions = regions;
+    this.verbose = verbose;
+    this.format = format == null ? Format.TEXT : format;
+  }
+
+  public boolean isRegions() {
+    return regions;
+  }
+
+  public boolean isVerbose() {
+    return verbose;
+  }
+
+  public Format getFormat() {
+    return format;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof ExplainOptions)) {
+      return false;
+    }
+    ExplainOptions that = (ExplainOptions) o;
+    return regions == that.regions && verbose == that.verbose && format == 
that.format;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(regions, verbose, format);
+  }
+
+  @Override
+  public String toString() {
+    return "ExplainOptions{regions=" + regions + ", verbose=" + verbose + ", 
format=" + format
+      + "}";
+  }
+
+  /**
+   * Mutable builder used by the parser to accumulate options as they are 
encountered in the option
+   * list. Rejects duplicate options.
+   */
+  public static final class Builder {
+    private boolean regions;
+    private boolean regionsSet;
+    private boolean verbose;
+    private boolean verboseSet;
+    private Format format;
+
+    public Builder setRegions(boolean regions) {
+      if (regionsSet) {
+        throw new RuntimeException("Duplicate EXPLAIN option: REGIONS");
+      }
+      this.regions = regions;
+      this.regionsSet = true;
+      return this;
+    }
+
+    public Builder setVerbose(boolean verbose) {
+      if (verboseSet) {
+        throw new RuntimeException("Duplicate EXPLAIN option: VERBOSE");
+      }
+      this.verbose = verbose;
+      this.verboseSet = true;
+      return this;
+    }
+
+    public Builder setFormat(Format format) {
+      if (format == null) {
+        throw new RuntimeException("EXPLAIN option FORMAT requires a value: 
TEXT or JSON");
+      }
+      if (this.format != null) {
+        throw new RuntimeException("Duplicate EXPLAIN option: FORMAT");
+      }
+      this.format = format;
+      return this;
+    }
+
+    public ExplainOptions build() {
+      return new ExplainOptions(regions, verbose, format == null ? Format.TEXT 
: format);
+    }
+  }
+}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainStatement.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainStatement.java
index a92696f67b..981cff6ef6 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainStatement.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainStatement.java
@@ -21,11 +21,11 @@ import org.apache.phoenix.jdbc.PhoenixStatement.Operation;
 
 public class ExplainStatement implements BindableStatement {
   private final BindableStatement statement;
-  private final ExplainType explainType;
+  private final ExplainOptions options;
 
-  public ExplainStatement(BindableStatement statement, ExplainType 
explainType) {
+  public ExplainStatement(BindableStatement statement, ExplainOptions options) 
{
     this.statement = statement;
-    this.explainType = explainType;
+    this.options = options == null ? ExplainOptions.DEFAULT : options;
   }
 
   public BindableStatement getStatement() {
@@ -42,7 +42,7 @@ public class ExplainStatement implements BindableStatement {
     return Operation.QUERY;
   }
 
-  public ExplainType getExplainType() {
-    return explainType;
+  public ExplainOptions getOptions() {
+    return options;
   }
 }
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainType.java 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainType.java
deleted file mode 100644
index 883e3c6a90..0000000000
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ExplainType.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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.phoenix.parse;
-
-/**
- * Explain type attributes used to differentiate output of the explain plan.
- */
-public enum ExplainType {
-  WITH_REGIONS,
-  DEFAULT
-}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
index a5070c7923..2a36dc4035 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
@@ -219,8 +219,8 @@ public class ParseNodeFactory {
     return "$" + tempAliasCounter.incrementAndGet();
   }
 
-  public ExplainStatement explain(BindableStatement statement, ExplainType 
explainType) {
-    return new ExplainStatement(statement, explainType);
+  public ExplainStatement explain(BindableStatement statement, ExplainOptions 
options) {
+    return new ExplainStatement(statement, options);
   }
 
   public AliasedNode aliasedNode(String alias, ParseNode expression) {
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java
index 3a40ef5818..1087420679 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/BaseAggregateIT.java
@@ -18,6 +18,7 @@
 package org.apache.phoenix.end2end;
 
 import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan;
+import static 
org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlanWithRegions;
 import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
 import static org.apache.phoenix.util.TestUtil.assertResultSet;
 import static org.junit.Assert.assertEquals;
@@ -556,8 +557,9 @@ public abstract class BaseAggregateIT extends 
ParallelStatsDisabledIT {
     assertEquals("a", rs.getString(1));
     assertEquals(4, rs.getLong(2));
     assertFalse(rs.next());
-    assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 
1-WAY").clientSortedBy("REVERSE")
-      .scanType("FULL 
SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
+    assertPlanWithRegions(conn, queryBuilder.build()).iteratorType("PARALLEL 
1-WAY")
+      .clientSortedBy("REVERSE").scanType("FULL SCAN").table(tableName)
+      .serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]")
       .regionLocationsNotEmpty();
   }
@@ -605,8 +607,9 @@ public abstract class BaseAggregateIT extends 
ParallelStatsDisabledIT {
     assertEquals("a", rs.getString(1));
     assertEquals(10, rs.getLong(2));
     assertFalse(rs.next());
-    assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 
1-WAY").clientSortedBy("REVERSE")
-      .scanType("FULL 
SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
+    assertPlanWithRegions(conn, queryBuilder.build()).iteratorType("PARALLEL 
1-WAY")
+      .clientSortedBy("REVERSE").scanType("FULL SCAN").table(tableName)
+      .serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]")
       .regionLocationsNotEmpty();
   }
@@ -664,8 +667,8 @@ public abstract class BaseAggregateIT extends 
ParallelStatsDisabledIT {
     assertEquals("n", rs.getString(1));
     assertEquals(2, rs.getDouble(2), 1e-6);
     assertFalse(rs.next());
-    assertPlan(conn, queryBuilder.build()).iteratorType("PARALLEL 
1-WAY").scanType("FULL SCAN")
-      .table(tableName).serverFirstKeyOnlyProjection(true)
+    assertPlanWithRegions(conn, queryBuilder.build()).iteratorType("PARALLEL 
1-WAY")
+      .scanType("FULL 
SCAN").table(tableName).serverFirstKeyOnlyProjection(true)
       .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [K1]")
       .regionLocationsNotEmpty();
     TestUtil.analyzeTable(conn, tableName);
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java
index b0adc333fc..f54b1d04d7 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/FlappingLocalIndexIT.java
@@ -18,6 +18,7 @@
 package org.apache.phoenix.end2end;
 
 import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan;
+import static 
org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlanWithRegions;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -49,7 +50,9 @@ import org.apache.phoenix.query.QueryServicesOptions;
 import org.apache.phoenix.util.SchemaUtil;
 import org.apache.phoenix.util.TestUtil;
 import org.junit.Test;
+import org.junit.experimental.categories.Category;
 
+@Category(NeedsOwnMiniClusterTest.class)
 public class FlappingLocalIndexIT extends BaseLocalIndexIT {
 
   public FlappingLocalIndexIT(boolean isNamespaceMapped) {
@@ -167,7 +170,7 @@ public class FlappingLocalIndexIT extends BaseLocalIndexIT {
 
       // MAX_REGION_LOCATIONS_SIZE_EXPLAIN_PLAN is set as 2 so 
getRegionLocations() is trimmed. The
       // full number of regions is reported via regionLocationsTotalSize.
-      assertPlan(conn1, query).iteratorType("PARALLEL " + numRegions + "-WAY")
+      assertPlanWithRegions(conn1, query).iteratorType("PARALLEL " + 
numRegions + "-WAY")
         .scanType("RANGE SCAN").table(indexTableName + "(" + 
indexPhysicalTableName + ")")
         .keyRanges(" [1,'a'] - [1,'b']").serverFirstKeyOnlyProjection(true)
         .clientSortAlgo("CLIENT MERGE 
SORT").regionLocationCount(trimmedRegionLocations)
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryLoggerIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryLoggerIT.java
index cdfe3ab2f1..5769da066d 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryLoggerIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryLoggerIT.java
@@ -117,7 +117,7 @@ public class QueryLoggerIT extends BaseTest {
 
     // sleep for sometime to let query log committed
     Thread.sleep(delay);
-    try (ResultSet explainRS = conn.createStatement().executeQuery("Explain 
with regions " + query);
+    try (ResultSet explainRS = conn.createStatement().executeQuery("Explain " 
+ query);
       ResultSet rs = conn.createStatement().executeQuery(logQuery)) {
       boolean foundQueryLog = false;
 
@@ -301,8 +301,8 @@ public class QueryLoggerIT extends BaseTest {
 
       // sleep for sometime to let query log committed
       Thread.sleep(delay);
-      String explainQuery =
-        "EXPLAIN WITH REGIONS " + "SELECT * FROM " + tableName + " where V = 
'value5'";
+      // Compare against a plain EXPLAIN (no REGIONS) to match the logged plan.
+      String explainQuery = "EXPLAIN " + "SELECT * FROM " + tableName + " 
where V = 'value5'";
       try (ResultSet explainRS = 
conn.createStatement().executeQuery(explainQuery);
         ResultSet rs = conn.createStatement().executeQuery(logQuery)) {
         boolean foundQueryLog = false;
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java
index a29b8c5028..f076229df2 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/BaseIndexIT.java
@@ -19,6 +19,7 @@ package org.apache.phoenix.end2end.index;
 
 import static org.apache.phoenix.query.QueryConstants.MILLIS_IN_DAY;
 import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan;
+import static 
org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlanWithRegions;
 import static org.apache.phoenix.util.TestUtil.ROW5;
 import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
 import static org.junit.Assert.assertEquals;
@@ -151,7 +152,7 @@ public abstract class BaseIndexIT extends 
ParallelStatsDisabledIT {
       String query = "SELECT d.char_col1, int_col1 from " + fullTableName + " 
as d";
 
       ExplainPlanTestUtil.ExplainPlanAssert basePlan =
-        assertPlan(conn, query).iteratorType("PARALLEL 1-WAY");
+        assertPlanWithRegions(conn, query).iteratorType("PARALLEL 1-WAY");
       if (!uncovered) {
         // Optimizer would not select the uncovered index for this query
         basePlan.serverProjectionFilter(columnEncoded);
@@ -571,7 +572,7 @@ public abstract class BaseIndexIT extends 
ParallelStatsDisabledIT {
       String query =
         "SELECT" + (uncovered ? " /*+ INDEX(" + fullTableName + " " + 
indexName + ")*/ " : " ")
           + "int_pk from " + fullTableName;
-      ExplainPlanTestUtil.ExplainPlanAssert basePlan = assertPlan(conn, query)
+      ExplainPlanTestUtil.ExplainPlanAssert basePlan = 
assertPlanWithRegions(conn, query)
         .iteratorType("PARALLEL 1-WAY").serverProjectionFilter(columnEncoded);
       if (localIndex) {
         basePlan.scanType("RANGE SCAN").table(fullIndexName + "(" + 
fullTableName + ")")
@@ -653,7 +654,7 @@ public abstract class BaseIndexIT extends 
ParallelStatsDisabledIT {
       query = "SELECT" + (uncovered ? " /*+ INDEX(" + fullTableName + " " + 
indexName + ")*/" : "")
         + " * FROM " + fullTableName;
       ExplainPlanTestUtil.ExplainPlanAssert basePlan =
-        assertPlan(conn, query).iteratorType("PARALLEL 1-WAY");
+        assertPlanWithRegions(conn, query).iteratorType("PARALLEL 1-WAY");
       if (localIndex) {
         basePlan.scanType("RANGE SCAN").table(fullIndexName + "(" + 
fullTableName + ")")
           .clientSortAlgo("CLIENT MERGE SORT").keyRanges(" [1]");
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/parse/ExplainOptionsParserTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/parse/ExplainOptionsParserTest.java
new file mode 100644
index 0000000000..238ceb0891
--- /dev/null
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/parse/ExplainOptionsParserTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.phoenix.parse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.sql.SQLException;
+import org.apache.phoenix.parse.ExplainOptions.Format;
+import org.junit.Test;
+
+/**
+ * Parser-level tests for the {@code EXPLAIN [(<opt> [, <opt>]*)] <stmt>} 
option-list grammar and
+ * the legacy {@code EXPLAIN WITH REGIONS} alias.
+ */
+public class ExplainOptionsParserTest {
+
+  private static final String STMT = " SELECT * FROM atable";
+
+  private ExplainOptions parseOptions(String sql) throws SQLException, 
IOException {
+    SQLParser parser = new SQLParser(new StringReader(sql));
+    BindableStatement stmt = parser.parseStatement();
+    assertTrue("Expected an ExplainStatement, got " + stmt.getClass(),
+      stmt instanceof ExplainStatement);
+    return ((ExplainStatement) stmt).getOptions();
+  }
+
+  private void assertParseError(String sql) {
+    try {
+      new SQLParser(new StringReader(sql)).parseStatement();
+      fail("Expected a parse error for: " + sql);
+    } catch (SQLException | IOException e) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testPlainExplain() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN" + STMT);
+    assertFalse(opts.isRegions());
+    assertFalse(opts.isVerbose());
+    assertEquals(Format.TEXT, opts.getFormat());
+  }
+
+  @Test
+  public void testRegionsOption() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (REGIONS)" + STMT);
+    assertTrue(opts.isRegions());
+    assertFalse(opts.isVerbose());
+    assertEquals(Format.TEXT, opts.getFormat());
+  }
+
+  @Test
+  public void testVerboseOption() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (VERBOSE)" + STMT);
+    assertFalse(opts.isRegions());
+    assertTrue(opts.isVerbose());
+    assertEquals(Format.TEXT, opts.getFormat());
+  }
+
+  @Test
+  public void testFormatText() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (FORMAT TEXT)" + STMT);
+    assertFalse(opts.isRegions());
+    assertEquals(Format.TEXT, opts.getFormat());
+  }
+
+  @Test
+  public void testFormatJson() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (FORMAT JSON)" + STMT);
+    assertEquals(Format.JSON, opts.getFormat());
+  }
+
+  @Test
+  public void testMultipleOptions() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (REGIONS, FORMAT JSON)" + 
STMT);
+    assertTrue(opts.isRegions());
+    assertEquals(Format.JSON, opts.getFormat());
+  }
+
+  @Test
+  public void testMultipleOptionsOrderIndependent() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (FORMAT JSON, REGIONS)" + 
STMT);
+    assertTrue(opts.isRegions());
+    assertEquals(Format.JSON, opts.getFormat());
+  }
+
+  @Test
+  public void testAllThreeOptions() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (REGIONS, VERBOSE, FORMAT 
JSON)" + STMT);
+    assertTrue(opts.isRegions());
+    assertTrue(opts.isVerbose());
+    assertEquals(Format.JSON, opts.getFormat());
+  }
+
+  @Test
+  public void testLegacyWithRegionsAlias() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN WITH REGIONS" + STMT);
+    assertTrue(opts.isRegions());
+    assertFalse(opts.isVerbose());
+    assertEquals(Format.TEXT, opts.getFormat());
+  }
+
+  @Test
+  public void testCaseInsensitiveOptions() throws Exception {
+    ExplainOptions opts = parseOptions("EXPLAIN (regions, format json)" + 
STMT);
+    assertTrue(opts.isRegions());
+    assertEquals(Format.JSON, opts.getFormat());
+  }
+
+  @Test
+  public void testUnknownOption() {
+    assertParseError("EXPLAIN (BOGUS)" + STMT);
+  }
+
+  @Test
+  public void testDuplicateOption() {
+    assertParseError("EXPLAIN (REGIONS, REGIONS)" + STMT);
+  }
+
+  @Test
+  public void testFormatWithoutValue() {
+    assertParseError("EXPLAIN (FORMAT)" + STMT);
+  }
+
+  @Test
+  public void testUnknownFormatKind() {
+    assertParseError("EXPLAIN (FORMAT XML)" + STMT);
+  }
+
+  @Test
+  public void testRegionsWithValue() {
+    assertParseError("EXPLAIN (REGIONS VALUE)" + STMT);
+  }
+
+  @Test
+  public void testMixingLegacyAndOptionList() {
+    assertParseError("EXPLAIN WITH REGIONS (REGIONS)" + STMT);
+  }
+}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
index f85b616dad..16f1f6c360 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java
@@ -33,6 +33,7 @@ import org.apache.phoenix.compile.ExplainPlanAttributes;
 import org.apache.phoenix.compile.QueryPlan;
 import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
 import org.apache.phoenix.optimize.RejectedIndexEntry;
+import org.apache.phoenix.parse.ExplainOptions;
 import org.apache.phoenix.parse.UpsertStatement.OnDuplicateKeyType;
 
 /**
@@ -60,6 +61,21 @@ public final class ExplainPlanTestUtil {
     return getExplainPlan(conn, query).getPlanStepsAsAttributes();
   }
 
+  /**
+   * Optimize {@code query} and return its structured {@link 
ExplainPlanAttributes} with the given
+   * {@link ExplainOptions} applied to the plan's {@code StatementContext}. 
Used to exercise region
+   * location information, which is only populated when the {@code REGIONS} 
option is requested.
+   */
+  public static ExplainPlanAttributes getExplainAttributes(Connection conn, 
String query,
+    ExplainOptions options) throws SQLException {
+    try (PhoenixPreparedStatement statement =
+      conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
+      QueryPlan plan = statement.optimizeQuery();
+      plan.getContext().setExplainOptions(options);
+      return plan.getExplainPlan().getPlanStepsAsAttributes();
+    }
+  }
+
   /** Optimize {@code query} and return its plan-steps text. */
   public static List<String> getPlanSteps(Connection conn, String query) 
throws SQLException {
     return getExplainPlan(conn, query).getPlanSteps();
@@ -91,6 +107,17 @@ public final class ExplainPlanTestUtil {
     return assertPlan(getExplainAttributes(conn, query));
   }
 
+  /**
+   * Optimize {@code query} on {@code conn} with the {@code REGIONS} option 
enabled and begin
+   * assertions on its plan attributes. Use this instead of {@link 
#assertPlan(Connection, String)}
+   * when asserting on region-location attributes, which are only populated 
when {@code REGIONS} is
+   * requested.
+   */
+  public static ExplainPlanAssert assertPlanWithRegions(Connection conn, 
String query)
+    throws SQLException {
+    return assertPlan(getExplainAttributes(conn, query, 
ExplainOptions.WITH_REGIONS));
+  }
+
   /**
    * Optimize an already-prepared and, if needed, parameter-bound {@link 
PhoenixPreparedStatement}
    * and begin assertions on its plan attributes.


Reply via email to