This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch PHOENIX-7876-feature in repository https://gitbox.apache.org/repos/asf/phoenix.git
commit 73b76967653d337f9eeda376f2ea7b1254859685 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.
