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.