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 31e7f60089 PHOENIX-7896 EXPLAIN top-of-plan disclosures (#2517)
31e7f60089 is described below
commit 31e7f60089259033a84f134b57f0e176475481d3
Author: Andrew Purtell <[email protected]>
AuthorDate: Thu Jun 11 10:38:12 2026 -0700
PHOENIX-7896 EXPLAIN top-of-plan disclosures (#2517)
Co-authored-by: Claude Opus 4.8[1m] <[email protected]>
---
.../org/apache/phoenix/compile/DeleteCompiler.java | 27 +-
.../phoenix/compile/ExplainPlanAttributes.java | 177 ++++++++---
.../org/apache/phoenix/compile/HavingCompiler.java | 3 +
.../org/apache/phoenix/compile/JoinCompiler.java | 32 +-
.../apache/phoenix/compile/OrderByCompiler.java | 1 +
.../org/apache/phoenix/compile/QueryCompiler.java | 45 ++-
.../apache/phoenix/compile/RVCOffsetCompiler.java | 3 +
.../apache/phoenix/compile/StatementContext.java | 87 +++++-
.../apache/phoenix/compile/SubqueryRewriter.java | 45 ++-
.../apache/phoenix/compile/SubselectRewriter.java | 8 +
.../org/apache/phoenix/compile/UnionCompiler.java | 5 +
.../org/apache/phoenix/compile/UpsertCompiler.java | 26 +-
.../org/apache/phoenix/execute/BaseQueryPlan.java | 4 +
.../phoenix/execute/ClientAggregatePlan.java | 4 +
.../org/apache/phoenix/execute/ClientScanPlan.java | 4 +
.../org/apache/phoenix/execute/HashJoinPlan.java | 3 +
.../apache/phoenix/execute/SortMergeJoinPlan.java | 4 +
.../java/org/apache/phoenix/execute/UnionPlan.java | 4 +
.../org/apache/phoenix/iterate/ExplainTable.java | 100 ++++++-
.../org/apache/phoenix/jdbc/PhoenixStatement.java | 40 ++-
.../apache/phoenix/optimize/QueryOptimizer.java | 87 ++++--
.../parse/IndexExpressionParseNodeRewriter.java | 10 +
.../org/apache/phoenix/util/ParseNodeUtil.java | 13 +-
.../index/ChildViewsUseParentViewIndexIT.java | 1 -
.../phoenix/end2end/index/PartialIndexIT.java | 102 ++++---
.../TenantSpecificViewIndexCompileTest.java | 1 +
.../phoenix/query/explain/ExplainPlanTest.java | 326 ++++++++++++++++++---
.../phoenix/query/explain/ExplainPlanTestUtil.java | 110 ++++++-
28 files changed, 1078 insertions(+), 194 deletions(-)
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/DeleteCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/DeleteCompiler.java
index 05316748b9..dc78cae33b 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/DeleteCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/DeleteCompiler.java
@@ -59,6 +59,7 @@ import
org.apache.phoenix.hbase.index.covered.update.ColumnReference;
import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.apache.phoenix.index.IndexMaintainer;
import org.apache.phoenix.index.PhoenixIndexCodec;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.iterate.ResultIterator;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixResultSet;
@@ -595,9 +596,13 @@ public class DeleteCompiler {
delete.getLimit(), null, delete.getBindCount(), false, false,
Collections.<SelectStatement> emptyList(), delete.getUdfParseNodes());
select = StatementNormalizer.normalize(select, resolverToBe);
-
+ // Pre-build a context so the early rewrite pass records top of plan
breadcrumbs that are
+ // adopted by the DELETE data query plan's compilation context.
+ StatementContext rewriteContext =
+ new StatementContext(statement, FromCompiler.EMPTY_TABLE_RESOLVER,
+ new BindManager(statement.getParameters()), new Scan(), new
SequenceManager(statement));
SelectStatement transformedSelect =
- SubqueryRewriter.transform(select, resolverToBe, connection);
+ SubqueryRewriter.transform(select, resolverToBe, connection,
rewriteContext);
boolean hasPreProcessing = transformedSelect != select;
if (transformedSelect != select) {
resolverToBe = FromCompiler.getResolverForQuery(transformedSelect,
connection, false,
@@ -624,7 +629,8 @@ public class DeleteCompiler {
QueryOptimizer optimizer = new QueryOptimizer(services);
QueryCompiler compiler =
new QueryCompiler(statement, select, resolverToBe, Collections.<PColumn>
emptyList(),
- parallelIteratorFactoryToBe, new SequenceManager(statement));
+ parallelIteratorFactoryToBe, new SequenceManager(statement))
+ .withRewriteContext(rewriteContext);
final QueryPlan dataPlan = compiler.compile();
// TODO: the select clause should know that there's a sub query, but
doesn't seem to currently
queryPlans = Lists.newArrayList(!clientSideIndexes.isEmpty()
@@ -793,9 +799,12 @@ public class DeleteCompiler {
@Override
public ExplainPlan getExplainPlan() throws SQLException {
- ExplainPlanAttributes attributes =
- new ExplainPlanAttributesBuilder().setAbstractExplainPlan("DELETE
SINGLE ROW").build();
- return new ExplainPlan(Collections.singletonList("DELETE SINGLE ROW"),
attributes);
+ ExplainPlanAttributesBuilder builder =
+ new ExplainPlanAttributesBuilder().setAbstractExplainPlan("DELETE
SINGLE ROW");
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(builder, getContext(),
getTargetRef());
+ }
+ return new ExplainPlan(Collections.singletonList("DELETE SINGLE ROW"),
builder.build());
}
@Override
@@ -978,6 +987,9 @@ public class DeleteCompiler {
newBuilder.setAbstractExplainPlan("DELETE ROWS SERVER SELECT");
planSteps.add("DELETE ROWS SERVER SELECT");
planSteps.addAll(queryPlanSteps);
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(),
getTargetRef());
+ }
return new ExplainPlan(planSteps, newBuilder.build());
}
@@ -1113,6 +1125,9 @@ public class DeleteCompiler {
newBuilder.setAbstractExplainPlan("DELETE ROWS CLIENT SELECT");
planSteps.add("DELETE ROWS CLIENT SELECT");
planSteps.addAll(queryPlanSteps);
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(),
getTargetRef());
+ }
return new ExplainPlan(planSteps, newBuilder.build());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java
index 84012ddfa1..db729b014a 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ExplainPlanAttributes.java
@@ -35,20 +35,29 @@ import org.apache.phoenix.schema.PColumn;
* against. This also makes attribute retrieval easier as an API rather than
retrieving list of
* Strings containing entire plan.
*/
-@JsonPropertyOrder({ "abstractExplainPlan", "hint", "explainScanType",
"consistency", "tableName",
- "keyRanges", "indexName", "indexKind", "indexRule", "indexRejected",
"saltBuckets",
- "regionsPlanned", "scanTimeRangeMin", "scanTimeRangeMax", "splitsChunk",
"useRoundRobinIterator",
- "samplingRate", "hexStringRVCOffset", "iteratorTypeAndScanSize",
"estimatedRows",
- "estimatedSizeInBytes", "serverWhereFilter", "serverDistinctFilter",
"serverMergeColumns",
- "serverArrayElementProjection", "serverFirstKeyOnlyProjection",
"serverEmptyColumnOnlyProjection",
- "serverAggregate", "serverGroupByLimit", "serverSortedBy", "serverOffset",
"serverRowLimit",
- "clientFilterBy", "clientAggregate", "clientDistinctFilter",
"clientAfterAggregate",
- "clientSortAlgo", "clientSortedBy", "clientOffset", "clientRowLimit",
"clientSequenceCount",
- "clientCursorName", "clientSteps", "lhsJoinQueryExplainPlan",
"rhsJoinQueryExplainPlan",
- "subPlans", "dynamicServerFilter", "afterJoinFilter", "joinScannerLimit",
"sortMergeSkipMerge",
+@JsonPropertyOrder({ "tenantId", "viewName", "viewBaseName", "cdcScopes",
"txnProvider", "rewrites",
+ "abstractExplainPlan", "hint", "explainScanType", "consistency",
"tableName", "keyRanges",
+ "indexName", "indexKind", "indexRule", "indexRejected", "saltBuckets",
"regionsPlanned",
+ "scanTimeRangeMin", "scanTimeRangeMax", "splitsChunk",
"useRoundRobinIterator", "samplingRate",
+ "hexStringRVCOffset", "iteratorTypeAndScanSize", "estimatedRows",
"estimatedSizeInBytes",
+ "serverWhereFilter", "serverDistinctFilter", "serverMergeColumns",
"serverArrayElementProjection",
+ "serverFirstKeyOnlyProjection", "serverEmptyColumnOnlyProjection",
"serverAggregate",
+ "serverGroupByLimit", "serverSortedBy", "serverOffset", "serverRowLimit",
"clientFilterBy",
+ "clientAggregate", "clientDistinctFilter", "clientAfterAggregate",
"clientSortAlgo",
+ "clientSortedBy", "clientOffset", "clientRowLimit", "clientSequenceCount",
"clientCursorName",
+ "clientSteps", "lhsJoinQueryExplainPlan", "rhsJoinQueryExplainPlan",
"subPlans",
+ "dynamicServerFilter", "afterJoinFilter", "joinScannerLimit",
"sortMergeSkipMerge",
"regionLocations", "regionLocationsTotalSize", "numRegionLocationLookups" })
public class ExplainPlanAttributes {
+ // Top-of-plan disclosures (populated only on the root plan)
+ private final String tenantId;
+ private final String viewName;
+ private final String viewBaseName;
+ private final String cdcScopes;
+ private final String txnProvider;
+ private final List<String> rewrites;
+
// Plan identity and scan-level metadata
private final String abstractExplainPlan;
private final Hint hint;
@@ -117,6 +126,12 @@ public class ExplainPlanAttributes {
private static final ExplainPlanAttributes EXPLAIN_PLAN_INSTANCE = new
ExplainPlanAttributes();
private ExplainPlanAttributes() {
+ this.tenantId = null;
+ this.viewName = null;
+ this.viewBaseName = null;
+ this.cdcScopes = null;
+ this.txnProvider = null;
+ this.rewrites = null;
this.abstractExplainPlan = null;
this.hint = null;
this.explainScanType = null;
@@ -172,23 +187,33 @@ public class ExplainPlanAttributes {
this.numRegionLocationLookups = 0;
}
- public ExplainPlanAttributes(String abstractExplainPlan, Hint hint, String
explainScanType,
- Consistency consistency, String tableName, String keyRanges, String
indexName, String indexKind,
- String indexRule, List<RejectedIndexEntry> indexRejected, Integer
saltBuckets,
- Integer regionsPlanned, Long scanTimeRangeMin, Long scanTimeRangeMax,
Integer splitsChunk,
- boolean useRoundRobinIterator, Double samplingRate, String
hexStringRVCOffset,
- String iteratorTypeAndScanSize, Long estimatedRows, Long
estimatedSizeInBytes,
- String serverWhereFilter, String serverDistinctFilter, Set<PColumn>
serverMergeColumns,
- boolean serverArrayElementProjection, boolean serverFirstKeyOnlyProjection,
- boolean serverEmptyColumnOnlyProjection, String serverAggregate, Integer
serverGroupByLimit,
- String serverSortedBy, Integer serverOffset, Long serverRowLimit, String
clientFilterBy,
- String clientAggregate, String clientDistinctFilter, String
clientAfterAggregate,
- String clientSortAlgo, String clientSortedBy, Integer clientOffset,
Integer clientRowLimit,
- Integer clientSequenceCount, String clientCursorName, List<String>
clientSteps,
- ExplainPlanAttributes lhsJoinQueryExplainPlan, ExplainPlanAttributes
rhsJoinQueryExplainPlan,
- List<ExplainPlanAttributes> subPlans, String dynamicServerFilter, String
afterJoinFilter,
- Long joinScannerLimit, boolean sortMergeSkipMerge, List<HRegionLocation>
regionLocations,
+ public ExplainPlanAttributes(String tenantId, String viewName, String
viewBaseName,
+ String cdcScopes, String txnProvider, List<String> rewrites, String
abstractExplainPlan,
+ Hint hint, String explainScanType, Consistency consistency, String
tableName, String keyRanges,
+ String indexName, String indexKind, String indexRule,
List<RejectedIndexEntry> indexRejected,
+ Integer saltBuckets, Integer regionsPlanned, Long scanTimeRangeMin, Long
scanTimeRangeMax,
+ Integer splitsChunk, boolean useRoundRobinIterator, Double samplingRate,
+ String hexStringRVCOffset, String iteratorTypeAndScanSize, Long
estimatedRows,
+ Long estimatedSizeInBytes, String serverWhereFilter, String
serverDistinctFilter,
+ Set<PColumn> serverMergeColumns, boolean serverArrayElementProjection,
+ boolean serverFirstKeyOnlyProjection, boolean
serverEmptyColumnOnlyProjection,
+ String serverAggregate, Integer serverGroupByLimit, String serverSortedBy,
Integer serverOffset,
+ Long serverRowLimit, String clientFilterBy, String clientAggregate, String
clientDistinctFilter,
+ String clientAfterAggregate, String clientSortAlgo, String clientSortedBy,
Integer clientOffset,
+ Integer clientRowLimit, Integer clientSequenceCount, String
clientCursorName,
+ List<String> clientSteps, ExplainPlanAttributes lhsJoinQueryExplainPlan,
+ ExplainPlanAttributes rhsJoinQueryExplainPlan, List<ExplainPlanAttributes>
subPlans,
+ String dynamicServerFilter, String afterJoinFilter, Long joinScannerLimit,
+ boolean sortMergeSkipMerge, List<HRegionLocation> regionLocations,
Integer regionLocationsTotalSize, int numRegionLocationLookups) {
+ this.tenantId = tenantId;
+ this.viewName = viewName;
+ this.viewBaseName = viewBaseName;
+ this.cdcScopes = cdcScopes;
+ this.txnProvider = txnProvider;
+ this.rewrites = (rewrites == null || rewrites.isEmpty())
+ ? null
+ : Collections.unmodifiableList(new ArrayList<>(rewrites));
this.abstractExplainPlan = abstractExplainPlan;
this.hint = hint;
this.explainScanType = explainScanType;
@@ -248,6 +273,30 @@ public class ExplainPlanAttributes {
this.numRegionLocationLookups = numRegionLocationLookups;
}
+ public String getTenantId() {
+ return tenantId;
+ }
+
+ public String getViewName() {
+ return viewName;
+ }
+
+ public String getViewBaseName() {
+ return viewBaseName;
+ }
+
+ public String getCdcScopes() {
+ return cdcScopes;
+ }
+
+ public String getTxnProvider() {
+ return txnProvider;
+ }
+
+ public List<String> getRewrites() {
+ return rewrites;
+ }
+
public String getAbstractExplainPlan() {
return abstractExplainPlan;
}
@@ -467,6 +516,12 @@ public class ExplainPlanAttributes {
}
public static class ExplainPlanAttributesBuilder {
+ private String tenantId;
+ private String viewName;
+ private String viewBaseName;
+ private String cdcScopes;
+ private String txnProvider;
+ private List<String> rewrites;
private String abstractExplainPlan;
private HintNode.Hint hint;
private String explainScanType;
@@ -526,6 +581,13 @@ public class ExplainPlanAttributes {
}
public ExplainPlanAttributesBuilder(ExplainPlanAttributes
explainPlanAttributes) {
+ this.tenantId = explainPlanAttributes.getTenantId();
+ this.viewName = explainPlanAttributes.getViewName();
+ this.viewBaseName = explainPlanAttributes.getViewBaseName();
+ this.cdcScopes = explainPlanAttributes.getCdcScopes();
+ this.txnProvider = explainPlanAttributes.getTxnProvider();
+ List<String> srcRewrites = explainPlanAttributes.getRewrites();
+ this.rewrites = srcRewrites == null ? null : new
ArrayList<>(srcRewrites);
this.abstractExplainPlan =
explainPlanAttributes.getAbstractExplainPlan();
this.hint = explainPlanAttributes.getHint();
this.explainScanType = explainPlanAttributes.getExplainScanType();
@@ -584,6 +646,44 @@ public class ExplainPlanAttributes {
this.numRegionLocationLookups =
explainPlanAttributes.getNumRegionLocationLookups();
}
+ public ExplainPlanAttributesBuilder setTenantId(String tenantId) {
+ this.tenantId = tenantId;
+ return this;
+ }
+
+ public ExplainPlanAttributesBuilder setViewName(String viewName) {
+ this.viewName = viewName;
+ return this;
+ }
+
+ public ExplainPlanAttributesBuilder setViewBaseName(String viewBaseName) {
+ this.viewBaseName = viewBaseName;
+ return this;
+ }
+
+ public ExplainPlanAttributesBuilder setCdcScopes(String cdcScopes) {
+ this.cdcScopes = cdcScopes;
+ return this;
+ }
+
+ public ExplainPlanAttributesBuilder setTxnProvider(String txnProvider) {
+ this.txnProvider = txnProvider;
+ return this;
+ }
+
+ public ExplainPlanAttributesBuilder setRewrites(List<String> rewrites) {
+ this.rewrites = rewrites == null ? null : new ArrayList<>(rewrites);
+ return this;
+ }
+
+ public ExplainPlanAttributesBuilder addRewrite(String rewrite) {
+ if (this.rewrites == null) {
+ this.rewrites = new ArrayList<>();
+ }
+ this.rewrites.add(rewrite);
+ return this;
+ }
+
public ExplainPlanAttributesBuilder setAbstractExplainPlan(String
abstractExplainPlan) {
this.abstractExplainPlan = abstractExplainPlan;
return this;
@@ -864,17 +964,18 @@ public class ExplainPlanAttributes {
}
public ExplainPlanAttributes build() {
- return new ExplainPlanAttributes(abstractExplainPlan, hint,
explainScanType, consistency,
- tableName, keyRanges, indexName, indexKind, indexRule, indexRejected,
saltBuckets,
- regionsPlanned, scanTimeRangeMin, scanTimeRangeMax, splitsChunk,
useRoundRobinIterator,
- samplingRate, hexStringRVCOffset, iteratorTypeAndScanSize,
estimatedRows,
- estimatedSizeInBytes, serverWhereFilter, serverDistinctFilter,
serverMergeColumns,
- serverArrayElementProjection, serverFirstKeyOnlyProjection,
serverEmptyColumnOnlyProjection,
- serverAggregate, serverGroupByLimit, serverSortedBy, serverOffset,
serverRowLimit,
- clientFilterBy, clientAggregate, clientDistinctFilter,
clientAfterAggregate, clientSortAlgo,
- clientSortedBy, clientOffset, clientRowLimit, clientSequenceCount,
clientCursorName,
- clientSteps, lhsJoinQueryExplainPlan, rhsJoinQueryExplainPlan,
subPlans,
- dynamicServerFilter, afterJoinFilter, joinScannerLimit,
sortMergeSkipMerge, regionLocations,
+ return new ExplainPlanAttributes(tenantId, viewName, viewBaseName,
cdcScopes, txnProvider,
+ rewrites, abstractExplainPlan, hint, explainScanType, consistency,
tableName, keyRanges,
+ indexName, indexKind, indexRule, indexRejected, saltBuckets,
regionsPlanned,
+ scanTimeRangeMin, scanTimeRangeMax, splitsChunk,
useRoundRobinIterator, samplingRate,
+ hexStringRVCOffset, iteratorTypeAndScanSize, estimatedRows,
estimatedSizeInBytes,
+ serverWhereFilter, serverDistinctFilter, serverMergeColumns,
serverArrayElementProjection,
+ serverFirstKeyOnlyProjection, serverEmptyColumnOnlyProjection,
serverAggregate,
+ serverGroupByLimit, serverSortedBy, serverOffset, serverRowLimit,
clientFilterBy,
+ clientAggregate, clientDistinctFilter, clientAfterAggregate,
clientSortAlgo, clientSortedBy,
+ clientOffset, clientRowLimit, clientSequenceCount, clientCursorName,
clientSteps,
+ lhsJoinQueryExplainPlan, rhsJoinQueryExplainPlan, subPlans,
dynamicServerFilter,
+ afterJoinFilter, joinScannerLimit, sortMergeSkipMerge, regionLocations,
regionLocationsTotalSize, numRegionLocationLookups);
}
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
index f57a126f74..0d989a8957 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
@@ -75,6 +75,9 @@ public class HavingCompiler {
}
HavingClauseVisitor visitor = new HavingClauseVisitor(context, groupBy);
having.accept(visitor);
+ if (!visitor.getMoveToWhereClauseExpressions().isEmpty()) {
+ context.addAppliedRewrite("HAVING PREDICATE AS WHERE");
+ }
statement = SelectStatementRewriter.moveFromHavingToWhereClause(statement,
visitor.getMoveToWhereClauseExpressions());
return statement;
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
index 8a091fbdc0..c408d37259 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
@@ -128,9 +128,10 @@ public class JoinCompiler {
private final Map<ColumnRef, ColumnRefType> columnRefs;
private final Map<ColumnRef, ColumnParseNode> columnNodes;
private final boolean useSortMergeJoin;
+ private final StatementContext context;
- private JoinCompiler(PhoenixStatement statement, SelectStatement select,
- ColumnResolver resolver) {
+ private JoinCompiler(PhoenixStatement statement, SelectStatement select,
ColumnResolver resolver,
+ StatementContext context) {
this.phoenixStatement = statement;
this.originalJoinSelectStatement = select;
this.origResolver = resolver;
@@ -138,16 +139,25 @@ public class JoinCompiler {
this.columnRefs = new HashMap<ColumnRef, ColumnRefType>();
this.columnNodes = new HashMap<ColumnRef, ColumnParseNode>();
this.useSortMergeJoin = select.getHint().hasHint(Hint.USE_SORT_MERGE_JOIN);
+ this.context = context;
+ }
+
+ public static JoinTable compile(PhoenixStatement statement, SelectStatement
select,
+ ColumnResolver resolver) throws SQLException {
+ return compile(statement, select, resolver, null);
}
/**
* After this method is called, the inner state of the parameter resolver
may be changed by
* {@link FromCompiler#refreshDerivedTableNode} because of some sql
optimization, see also
* {@link Table#pruneSubselectAliasedNodes()}.
+ * <p>
+ * The supplied {@link StatementContext} receives top of plan rewrite
breadcrumbs recorded during
+ * join compilation. May be null.
*/
public static JoinTable compile(PhoenixStatement statement, SelectStatement
select,
- ColumnResolver resolver) throws SQLException {
- JoinCompiler compiler = new JoinCompiler(statement, select, resolver);
+ ColumnResolver resolver, StatementContext context) throws SQLException {
+ JoinCompiler compiler = new JoinCompiler(statement, select, resolver,
context);
JoinTableConstructor constructor = compiler.new JoinTableConstructor();
Pair<Table, List<JoinSpec>> res = select.getFrom().accept(constructor);
JoinTable joinTable = res.getSecond() == null
@@ -253,7 +263,7 @@ public class JoinCompiler {
private JoinTable(Table table) {
this.leftTable = table;
this.joinSpecs = Collections.<JoinSpec> emptyList();
- this.postFilters = Collections.EMPTY_LIST;
+ this.postFilters = Collections.emptyList();
this.allTables = Collections.<Table> singletonList(table);
this.allTableRefs = Collections.<TableRef>
singletonList(table.getTableRef());
this.allLeftJoin = false;
@@ -479,6 +489,10 @@ public class JoinCompiler {
}
}
+ if (context != null && vector.length >= 2) {
+ context.addAppliedRewrite("STAR JOIN ON " + vector.length + " RIGHT
LEGS");
+ }
+
return vector;
}
@@ -633,6 +647,7 @@ public class JoinCompiler {
return dependentTableRefs;
}
+ @SuppressWarnings("rawtypes")
public Pair<List<Expression>, List<Expression>>
compileJoinConditions(StatementContext lhsCtx,
StatementContext rhsCtx, Strategy strategy) throws SQLException {
if (onConditions.isEmpty()) {
@@ -704,6 +719,7 @@ public class JoinCompiler {
return new Pair<List<Expression>, List<Expression>>(lConditions,
rConditions);
}
+ @SuppressWarnings("rawtypes")
private PDataType getCommonType(PDataType lType, PDataType rType) throws
SQLException {
if (lType == rType) return lType;
@@ -823,7 +839,7 @@ public class JoinCompiler {
this.dynamicColumns = Collections.<ColumnDef> emptyList();
this.tableSamplingRate = ConcreteTableNode.DEFAULT_TABLE_SAMPLING_RATE;
this.subselectStatement =
- SubselectRewriter.flatten(tableNode.getSelect(),
phoenixStatement.getConnection());
+ SubselectRewriter.flatten(tableNode.getSelect(),
phoenixStatement.getConnection(), context);
this.tableRef = tableRef;
this.preFilterParseNodes = new ArrayList<ParseNode>();
this.postFilterParseNodes = new ArrayList<ParseNode>();
@@ -1056,8 +1072,10 @@ public class JoinCompiler {
}
SelectStatement selectStatementToUse = this.getAsSubquery(null);
+ // The rewrite result is only used to test a predicate, so any rewrite
breadcrumbs are
+ // recorded on a throwaway context and discarded.
RewriteResult rewriteResult =
- ParseNodeUtil.rewrite(selectStatementToUse,
phoenixStatement.getConnection());
+ ParseNodeUtil.rewrite(selectStatementToUse, new
StatementContext(phoenixStatement));
return JoinCompiler
.isCouldPushToServerAsHashJoinProbeSide(rewriteResult.getRewrittenSelectStatement());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/OrderByCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/OrderByCompiler.java
index 5630d22ec6..101eb03510 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/OrderByCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/OrderByCompiler.java
@@ -224,6 +224,7 @@ public class OrderByCompiler {
if (offset.getByteOffset().isPresent()) {
throw new SQLException("Do not allow non-pk ORDER BY with RVC
OFFSET");
}
+ context.addAppliedRewrite("REVERSE SCAN SUBSTITUTION");
return OrderBy.REV_ROW_KEY_ORDER_BY;
}
} else {
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
index 86f8b500c0..4bdbd9dbab 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/QueryCompiler.java
@@ -121,6 +121,7 @@ public class QueryCompiler {
private final Map<TableRef, QueryPlan> dataPlans;
private final boolean costBased;
private final StatementContext parentContext;
+ private StatementContext prebuiltContext;
public QueryCompiler(PhoenixStatement statement, SelectStatement select,
ColumnResolver resolver,
boolean projectTuples, boolean optimizeSubquery, Map<TableRef, QueryPlan>
dataPlans)
@@ -268,6 +269,9 @@ public class QueryCompiler {
ColumnResolver resolver = FromCompiler.getResolver(tableRef);
StatementContext context =
new StatementContext(statement, resolver, bindManager, scan,
sequenceManager);
+ if (prebuiltContext != null) {
+ context.adoptRewriteState(prebuiltContext);
+ }
plans = UnionCompiler.convertToTupleProjectionPlan(plans, tableRef,
context);
QueryPlan plan = compileSingleFlatQuery(context, select, false, false,
null, false, true);
plan = new UnionPlan(context, select, tableRef, plan.getProjector(),
plan.getLimit(),
@@ -290,7 +294,7 @@ public class QueryCompiler {
public QueryPlan compileSelect(SelectStatement select) throws SQLException {
StatementContext context = createStatementContext();
if (parentContext != null) {
- parentContext.addSubStatementContext(context);
+ parentContext.addSubStatementContext(context, false);
}
QueryPlan dataPlanForCDC = getExistingDataPlanForCDC();
if (dataPlanForCDC != null) {
@@ -313,15 +317,30 @@ public class QueryCompiler {
context.setCDCIncludeScopes(cdcIncludeScopes);
}
if (select.isJoin()) {
- JoinTable joinTable = JoinCompiler.compile(statement, select,
context.getResolver());
+ JoinTable joinTable = JoinCompiler.compile(statement, select,
context.getResolver(), context);
return compileJoinQuery(context, joinTable, false, false, null);
} else {
return compileSingleQuery(context, select, false, true);
}
}
+ /**
+ * Supplies the pre-built top level {@link StatementContext} used by
+ * {@link org.apache.phoenix.util.ParseNodeUtil#rewrite} so the breadcrumbs
recorded by the early
+ * rewrite pass are carried onto the compilation context.
+ */
+ public QueryCompiler withRewriteContext(StatementContext prebuiltContext) {
+ this.prebuiltContext = prebuiltContext;
+ return this;
+ }
+
private StatementContext createStatementContext() {
- return new StatementContext(statement, resolver, bindManager, scan,
sequenceManager);
+ StatementContext context =
+ new StatementContext(statement, resolver, bindManager, scan,
sequenceManager);
+ if (prebuiltContext != null) {
+ context.adoptRewriteState(prebuiltContext);
+ }
+ return context;
}
/**
@@ -384,6 +403,7 @@ public class QueryCompiler {
return bestPlan;
}
+ @SuppressWarnings("unchecked")
protected QueryPlan compileJoinQuery(JoinCompiler.Strategy strategy,
StatementContext context,
JoinTable joinTable, boolean asSubquery, boolean projectPKColumns,
List<OrderByNode> orderBy)
throws SQLException {
@@ -491,6 +511,9 @@ public class QueryCompiler {
case HASH_BUILD_LEFT: {
JoinSpec lastJoinSpec = joinSpecs.get(joinSpecs.size() - 1);
JoinType type = lastJoinSpec.getType();
+ if (type == JoinType.Right) {
+ context.addAppliedRewrite("RIGHT JOIN AS LEFT JOIN");
+ }
JoinTable rhsJoinTable = lastJoinSpec.getRhsJoinTable();
Table rhsTable = rhsJoinTable.getLeftTable();
JoinTable lhsJoin =
joinTable.createSubJoinTable(statement.getConnection());
@@ -571,6 +594,7 @@ public class QueryCompiler {
JoinType type = lastJoinSpec.getType();
JoinTable rhsJoin = lastJoinSpec.getRhsJoinTable();
if (type == JoinType.Right) {
+ context.addAppliedRewrite("RIGHT JOIN AS LEFT JOIN");
JoinTable temp = lhsJoin;
lhsJoin = rhsJoin;
rhsJoin = temp;
@@ -696,14 +720,19 @@ public class QueryCompiler {
protected QueryPlan compileSubquery(SelectStatement subquerySelectStatement,
boolean pushDownMaxRows, StatementContext parentContext) throws
SQLException {
- PhoenixConnection phoenixConnection = this.statement.getConnection();
- RewriteResult rewriteResult =
ParseNodeUtil.rewrite(subquerySelectStatement, phoenixConnection);
+ // Pre-build a context so the subquery's early rewrite pass records
breadcrumbs that are then
+ // adopted by the subquery's compilation context.
+ StatementContext rewriteContext =
+ new StatementContext(this.statement, FromCompiler.EMPTY_TABLE_RESOLVER,
bindManager,
+ new Scan(), new SequenceManager(this.statement));
+ RewriteResult rewriteResult =
ParseNodeUtil.rewrite(subquerySelectStatement, rewriteContext);
int maxRows = this.statement.getMaxRows();
- this.statement.setMaxRows(pushDownMaxRows ? maxRows : 0); // overwrite
maxRows to avoid its
- // impact on
inner queries.
+ // overwrite maxRows to avoid its impact on inner queries.
+ this.statement.setMaxRows(pushDownMaxRows ? maxRows : 0);
QueryPlan queryPlan =
new QueryCompiler(this.statement,
rewriteResult.getRewrittenSelectStatement(),
- rewriteResult.getColumnResolver(), bindManager, false,
optimizeSubquery, null).compile();
+ rewriteResult.getColumnResolver(), bindManager, false,
optimizeSubquery, null)
+ .withRewriteContext(rewriteContext).compile();
if (optimizeSubquery) {
queryPlan =
statement.getConnection().getQueryServices().getOptimizer().optimize(statement,
queryPlan);
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
index 739f7f019a..a7c124f256 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.hbase.util.Bytes;
import org.apache.phoenix.expression.AndExpression;
import org.apache.phoenix.expression.CoerceExpression;
import org.apache.phoenix.expression.ComparisonExpression;
@@ -289,6 +290,8 @@ public class RVCOffsetCompiler {
CompiledOffset compiledOffset =
new CompiledOffset(Optional.<Integer> absent(), Optional.of(key));
+ context.addAppliedRewrite("RVC OFFSET 0x" + Bytes.toHex(key));
+
return compiledOffset;
}
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 1053dac1ea..e329b4ee86 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
@@ -39,6 +39,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.ParseNode;
import org.apache.phoenix.parse.SelectStatement;
import org.apache.phoenix.query.QueryConstants;
import org.apache.phoenix.query.QueryServices;
@@ -101,6 +102,11 @@ public class StatementContext {
private boolean hasRawRowSizeFunction = false;
private final SlowestScanMetricsQueue slowestScanMetricsQueue;
private final int slowestScanMetricsCount;
+ private List<String> appliedRewrites;
+ private int derivedTableFlattenCount;
+ private List<Pair<ParseNode, String>> indexExpressionSubstitutions;
+ private Set<Pair<String, String>> partialIndexCheckedSet;
+ private StatementContext parentContext;
public StatementContext(PhoenixStatement statement) {
this(statement, new Scan());
@@ -132,11 +138,16 @@ public class StatementContext {
this.isClientSideUpsertSelect = context.isClientSideUpsertSelect;
this.isUncoveredIndex = context.isUncoveredIndex;
this.hasFirstValidResult = new
AtomicBoolean(context.getHasFirstValidResult());
- this.subStatementContexts = Sets.newHashSet();
+ this.subStatementContexts = Sets.newLinkedHashSet();
this.totalSegmentsFunction = context.totalSegmentsFunction;
this.totalSegmentsValue = context.totalSegmentsValue;
this.hasRowSizeFunction = context.hasRowSizeFunction;
this.hasRawRowSizeFunction = context.hasRawRowSizeFunction;
+ this.appliedRewrites = context.appliedRewrites;
+ this.derivedTableFlattenCount = context.derivedTableFlattenCount;
+ this.indexExpressionSubstitutions = context.indexExpressionSubstitutions;
+ this.partialIndexCheckedSet = context.partialIndexCheckedSet;
+ this.parentContext = context.parentContext;
}
/**
@@ -198,7 +209,12 @@ public class StatementContext {
: SlowestScanMetricsQueue.NOOP_SLOWEST_SCAN_METRICS_QUEUE;
this.retryingPersistentCache = Maps.<Long, Boolean> newHashMap();
this.hasFirstValidResult = new AtomicBoolean(false);
- this.subStatementContexts = Sets.newHashSet();
+ this.subStatementContexts = Sets.newLinkedHashSet();
+ this.appliedRewrites = new ArrayList<>();
+ this.derivedTableFlattenCount = 0;
+ this.indexExpressionSubstitutions = new ArrayList<>();
+ this.partialIndexCheckedSet = Sets.newHashSet();
+ this.parentContext = null;
}
/**
@@ -453,13 +469,80 @@ public class StatementContext {
}
public void addSubStatementContext(StatementContext sub) {
+ addSubStatementContext(sub, true);
+ }
+
+ public void addSubStatementContext(StatementContext sub, boolean
linkParentForDisclosure) {
subStatementContexts.add(sub);
+ if (linkParentForDisclosure) {
+ sub.parentContext = this;
+ }
}
public Set<StatementContext> getSubStatementContexts() {
return subStatementContexts;
}
+ /**
+ * Records a top-of-plan rewrite breadcrumb (e.g. "STAR JOIN ON 2 RIGHT
LEGS"). Breadcrumbs are
+ * diagnostic only and never affect the compiled plan.
+ */
+ public void addAppliedRewrite(String rewrite) {
+ appliedRewrites.add(rewrite);
+ }
+
+ public List<String> getAppliedRewrites() {
+ return appliedRewrites;
+ }
+
+ /**
+ * Adopt the rewrite breadcrumb accumulator (and related diagnostic state)
of another context by
+ * sharing its references. Used to carry breadcrumbs recorded by the early
+ * {@link SubselectRewriter}/{@link SubqueryRewriter} pass on a pre-built
context across the
+ * rewrite boundary onto the actual compilation context.
+ */
+ public void adoptRewriteState(StatementContext source) {
+ this.appliedRewrites = source.appliedRewrites;
+ this.derivedTableFlattenCount = source.derivedTableFlattenCount;
+ this.indexExpressionSubstitutions = source.indexExpressionSubstitutions;
+ this.partialIndexCheckedSet = source.partialIndexCheckedSet;
+ }
+
+ public void incrementDerivedTableFlattenCount() {
+ derivedTableFlattenCount++;
+ }
+
+ public int getDerivedTableFlattenCount() {
+ return derivedTableFlattenCount;
+ }
+
+ /** Structured pairs recorded when a functional index expression is
substituted. */
+ public List<Pair<ParseNode, String>> getIndexExpressionSubstitutions() {
+ return indexExpressionSubstitutions;
+ }
+
+ public void addIndexExpressionSubstitution(ParseNode source, String
indexColumnName) {
+ indexExpressionSubstitutions.add(new Pair<>(source, indexColumnName));
+ }
+
+ /**
+ * Dedup set used by the optimizer so a partial index applicability
breadcrumb is recorded only
+ * once per (table, index) pair even when the same index is scored across
multiple paths.
+ * @return true if the pair had not been recorded before
+ */
+ public boolean markPartialIndexChecked(String tableName, String indexName) {
+ return partialIndexCheckedSet.add(new Pair<>(tableName, indexName));
+ }
+
+ public StatementContext getParentContext() {
+ return parentContext;
+ }
+
+ /** Returns true if this is the top-level (root) statement context, i.e. it
has no parent. */
+ public boolean isRoot() {
+ return parentContext == null;
+ }
+
public boolean hasTotalSegmentsFunction() {
return totalSegmentsFunction;
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubqueryRewriter.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubqueryRewriter.java
index 3f7d3d22f8..19f2ddcc76 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubqueryRewriter.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubqueryRewriter.java
@@ -69,15 +69,21 @@ public class SubqueryRewriter extends ParseNodeRewriter {
private final ColumnResolver columnResolver;
private final PhoenixConnection connection;
+ private final StatementContext context;
private TableNode tableNode;
private ParseNode topNode;
public static SelectStatement transform(SelectStatement select,
ColumnResolver resolver,
PhoenixConnection connection) throws SQLException {
+ return transform(select, resolver, connection, null);
+ }
+
+ public static SelectStatement transform(SelectStatement select,
ColumnResolver resolver,
+ PhoenixConnection connection, StatementContext context) throws
SQLException {
ParseNode where = select.getWhere();
if (where == null) return select;
- SubqueryRewriter rewriter = new SubqueryRewriter(select, resolver,
connection);
+ SubqueryRewriter rewriter = new SubqueryRewriter(select, resolver,
connection, context);
ParseNode normWhere = rewrite(where, rewriter);
if (normWhere == where) return select;
@@ -86,12 +92,46 @@ public class SubqueryRewriter extends ParseNodeRewriter {
protected SubqueryRewriter(SelectStatement select, ColumnResolver resolver,
PhoenixConnection connection) {
+ this(select, resolver, connection, null);
+ }
+
+ protected SubqueryRewriter(SelectStatement select, ColumnResolver resolver,
+ PhoenixConnection connection, StatementContext context) {
this.columnResolver = resolver;
this.connection = connection;
+ this.context = context;
this.tableNode = select.getFrom();
this.topNode = null;
}
+ /**
+ * Records a subquery to join rewrite breadcrumb on the active context. The
wording is derived
+ * from the originating subquery kind and the chosen {@link JoinType}.
+ */
+ private void recordSubqueryRewrite(String subqueryKind, JoinType joinType) {
+ if (context == null) {
+ return;
+ }
+ final String joinName;
+ switch (joinType) {
+ case Semi:
+ joinName = "SEMI";
+ break;
+ case Anti:
+ joinName = "ANTI";
+ break;
+ case Inner:
+ joinName = "INNER";
+ break;
+ case Left:
+ joinName = "LEFT";
+ break;
+ default:
+ joinName = joinType.name().toUpperCase();
+ }
+ context.addAppliedRewrite(subqueryKind + " SUBQUERY AS " + joinName + "
JOIN");
+ }
+
@Override
protected void enterParseNode(ParseNode node) {
if (topNode == null) {
@@ -263,6 +303,7 @@ public class SubqueryRewriter extends ParseNodeRewriter {
newSubquerySelectAliasedNodes.get(0).getAlias(), null),
!inParseNode.isNegate());
tableNode = NODE_FACTORY.join(joinType, tableNode,
subqueryDerivedTableNode,
joinOnConditionParseNode, false);
+ recordSubqueryRewrite(inParseNode.isNegate() ? "NOT IN" : "IN", joinType);
return resultWhereParseNode;
}
@@ -392,6 +433,7 @@ public class SubqueryRewriter extends ParseNodeRewriter {
newSubquerySelectAliasedNodes.get(0).getAlias(), null),
!existsParseNode.isNegate());
tableNode = NODE_FACTORY.join(joinType, tableNode,
subqueryDerivedTableNode,
joinOnConditionParseNode, false);
+ recordSubqueryRewrite(existsParseNode.isNegate() ? "NOT EXISTS" :
"EXISTS", joinType);
return resultWhereParseNode;
}
@@ -459,6 +501,7 @@ public class SubqueryRewriter extends ParseNodeRewriter {
ParseNode ret = NODE_FACTORY.comparison(node.getFilterOp(), l.get(0),
NODE_FACTORY
.column(NODE_FACTORY.table(null, rhsTableAlias),
selectNodes.get(0).getAlias(), null));
tableNode = NODE_FACTORY.join(joinType, tableNode, rhsTable, onNode,
!isAggregate || isGroupby);
+ recordSubqueryRewrite("SCALAR", joinType);
return ret;
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubselectRewriter.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubselectRewriter.java
index 2282e2fd41..032370d2ad 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubselectRewriter.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/SubselectRewriter.java
@@ -300,6 +300,11 @@ public class SubselectRewriter extends ParseNodeRewriter {
public static SelectStatement flatten(SelectStatement select,
PhoenixConnection connection)
throws SQLException {
+ return flatten(select, connection, null);
+ }
+
+ public static SelectStatement flatten(SelectStatement select,
PhoenixConnection connection,
+ StatementContext context) throws SQLException {
TableNode from = select.getFrom();
while (from != null && from instanceof DerivedTableNode) {
DerivedTableNode derivedTable = (DerivedTableNode) from;
@@ -314,6 +319,9 @@ public class SubselectRewriter extends ParseNodeRewriter {
if (ret == select) {
break;
}
+ if (context != null) {
+ context.incrementDerivedTableFlattenCount();
+ }
select = ret;
from = select.getFrom();
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UnionCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UnionCompiler.java
index b40de288b3..f99c9346d6 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UnionCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UnionCompiler.java
@@ -118,6 +118,7 @@ public class UnionCompiler {
return new TableRef(null, tempTable, 0, false);
}
+ @SuppressWarnings("rawtypes")
private static void compareExperssions(int i, Expression expression,
List<TargetDataExpression> targetTypes) throws SQLException {
PDataType type = expression.getDataType();
@@ -164,6 +165,7 @@ public class UnionCompiler {
return new TupleProjector(exprs);
}
+ @SuppressWarnings("rawtypes")
private static class TargetDataExpression {
private PDataType type;
private Integer maxLength;
@@ -247,6 +249,9 @@ public class UnionCompiler {
// subquery) could not compile out the outer query's group by or order
by, we would
// not perform any special processing on the output of the subqueries.
innerUnionPlan.disableSupportOrderByOptimize();
+ } else {
+ // The order-by merge optimization is preserved. Record a top-of-plan
breadcrumb.
+ statementContextCreator.get().addAppliedRewrite("UNION ORDER BY MERGE");
}
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UpsertCompiler.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UpsertCompiler.java
index b9dfbc33e5..8a26cb8c8c 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UpsertCompiler.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UpsertCompiler.java
@@ -62,6 +62,7 @@ import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.apache.phoenix.index.IndexMaintainer;
import org.apache.phoenix.index.PhoenixIndexBuilderHelper;
import org.apache.phoenix.index.PhoenixIndexCodec;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.iterate.ResultIterator;
import org.apache.phoenix.jdbc.PhoenixConnection;
import org.apache.phoenix.jdbc.PhoenixResultSet;
@@ -352,6 +353,7 @@ public class UpsertCompiler {
this.operation = operation;
}
+ @SuppressWarnings("rawtypes")
private static LiteralParseNode getNodeForRowTimestampColumn(PColumn col) {
PDataType type = col.getDataType();
long dummyValue = 0L;
@@ -579,14 +581,19 @@ public class UpsertCompiler {
if (valueNodesList.isEmpty()) {
SelectStatement select = upsert.getSelect();
assert (select != null);
- select = SubselectRewriter.flatten(select, connection);
+ // Pre-build a context so the early rewrite pass records top-of-plan
breadcrumbs that are
+ // adopted by the UPSERT SELECT query plan's compilation context.
+ StatementContext rewriteContext =
+ new StatementContext(statement, FromCompiler.EMPTY_TABLE_RESOLVER,
+ new BindManager(statement.getParameters()), new Scan(), new
SequenceManager(statement));
+ select = SubselectRewriter.flatten(select, connection, rewriteContext);
ColumnResolver selectResolver =
FromCompiler.getResolverForQuery(select, connection, false,
upsert.getTable().getName());
select = StatementNormalizer.normalize(select, selectResolver);
select = prependTenantAndViewConstants(table, select, tenantIdStr,
addViewColumnsToBe,
useServerTimestampToBe);
SelectStatement transformedSelect =
- SubqueryRewriter.transform(select, selectResolver, connection);
+ SubqueryRewriter.transform(select, selectResolver, connection,
rewriteContext);
if (transformedSelect != select) {
selectResolver = FromCompiler.getResolverForQuery(transformedSelect,
connection, false,
upsert.getTable().getName());
@@ -649,7 +656,8 @@ public class UpsertCompiler {
// correctly
// Use optimizer to choose the best plan
QueryCompiler compiler = new QueryCompiler(statement, select,
selectResolver, targetColumns,
- parallelIteratorFactoryToBe, new SequenceManager(statement), true,
false, null);
+ parallelIteratorFactoryToBe, new SequenceManager(statement), true,
false, null)
+ .withRewriteContext(rewriteContext);
queryPlanToBe = compiler.compile();
if (sameTable) {
@@ -1246,6 +1254,9 @@ public class UpsertCompiler {
newBuilder.setAbstractExplainPlan("UPSERT ROWS");
planSteps.add("UPSERT ROWS");
planSteps.addAll(queryPlanSteps);
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(),
getTargetRef());
+ }
return new ExplainPlan(planSteps, newBuilder.build());
}
@@ -1430,6 +1441,12 @@ public class UpsertCompiler {
.add("CLIENT RESERVE " +
context.getSequenceManager().getSequenceCount() + " SEQUENCES");
}
planSteps.add("PUT SINGLE ROW");
+ if (getContext().isRoot()) {
+ ExplainPlanAttributesBuilder builder =
+ new
ExplainPlanAttributesBuilder(ExplainPlanAttributes.getDefaultExplainPlan());
+ ExplainTable.populateTopOfPlanAttributes(builder, getContext(),
getTargetRef());
+ return new ExplainPlan(planSteps, builder.build());
+ }
return new ExplainPlan(planSteps);
}
@@ -1554,6 +1571,9 @@ public class UpsertCompiler {
newBuilder.setAbstractExplainPlan("UPSERT SELECT");
planSteps.add("UPSERT SELECT");
planSteps.addAll(queryPlanSteps);
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(),
getTargetRef());
+ }
return new ExplainPlan(planSteps, newBuilder.build());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/BaseQueryPlan.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/BaseQueryPlan.java
index e7ca83c84d..d6839d6fa9 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/BaseQueryPlan.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/BaseQueryPlan.java
@@ -52,6 +52,7 @@ import org.apache.phoenix.hbase.index.util.ImmutableBytesPtr;
import org.apache.phoenix.index.IndexMaintainer;
import org.apache.phoenix.iterate.DefaultParallelScanGrouper;
import org.apache.phoenix.iterate.DelegateResultIterator;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.iterate.ParallelIteratorFactory;
import org.apache.phoenix.iterate.ParallelScanGrouper;
import org.apache.phoenix.iterate.ResultIterator;
@@ -550,6 +551,9 @@ public abstract class BaseQueryPlan implements QueryPlan {
builder.setIndexRule(decision.getRule());
builder.setIndexRejected(decision.getRejectedIndexes());
}
+ if (context.isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(builder, context,
getTableRef());
+ }
return Pair.of(planSteps, builder.build());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java
index 4a3d1b5d28..c0dc5ffe68 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientAggregatePlan.java
@@ -48,6 +48,7 @@ import org.apache.phoenix.iterate.AggregatingResultIterator;
import org.apache.phoenix.iterate.BaseGroupedAggregatingResultIterator;
import org.apache.phoenix.iterate.ClientHashAggregatingResultIterator;
import org.apache.phoenix.iterate.DistinctAggregatingResultIterator;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.iterate.FilterAggregatingResultIterator;
import org.apache.phoenix.iterate.FilterResultIterator;
import org.apache.phoenix.iterate.GroupedAggregatingResultIterator;
@@ -308,6 +309,9 @@ public class ClientAggregatePlan extends
ClientProcessingPlan {
newBuilder.addClientStep(step);
}
+ if (context.isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(newBuilder, context,
getTableRef());
+ }
return new ExplainPlan(planSteps, newBuilder.build());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java
index 9807ba8ef9..ae155b3a12 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/ClientScanPlan.java
@@ -31,6 +31,7 @@ import org.apache.phoenix.compile.StatementContext;
import org.apache.phoenix.execute.visitor.ByteCountVisitor;
import org.apache.phoenix.execute.visitor.QueryPlanVisitor;
import org.apache.phoenix.expression.Expression;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.iterate.FilterResultIterator;
import org.apache.phoenix.iterate.LimitingResultIterator;
import org.apache.phoenix.iterate.OffsetResultIterator;
@@ -165,6 +166,9 @@ public class ClientScanPlan extends ClientProcessingPlan {
newBuilder.addClientStep(step);
}
+ if (context.isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(newBuilder, context,
getTableRef());
+ }
return new ExplainPlan(planSteps, newBuilder.build());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/HashJoinPlan.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/HashJoinPlan.java
index a558c1832a..5a92f4adca 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/HashJoinPlan.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/HashJoinPlan.java
@@ -361,6 +361,9 @@ public class HashJoinPlan extends DelegateQueryPlan {
if (!subPlanAttributes.isEmpty()) {
builder.setSubPlans(subPlanAttributes);
}
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(builder, getContext(),
getTableRef());
+ }
return new ExplainPlan(planSteps, builder.build());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java
index bf36c03a58..e3ebae1779 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/SortMergeJoinPlan.java
@@ -57,6 +57,7 @@ import org.apache.phoenix.execute.visitor.QueryPlanVisitor;
import org.apache.phoenix.expression.Expression;
import org.apache.phoenix.expression.OrderByExpression;
import org.apache.phoenix.iterate.DefaultParallelScanGrouper;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.iterate.ParallelScanGrouper;
import org.apache.phoenix.iterate.PhoenixQueues;
import org.apache.phoenix.iterate.ResultIterator;
@@ -217,6 +218,9 @@ public class SortMergeJoinPlan implements QueryPlan {
rootBuilder.setSortMergeSkipMerge(rhsSchema.getFieldCount() == 0);
rootBuilder.setLhsJoinQueryExplainPlan(lhsPlanAttributes);
rootBuilder.setRhsJoinQueryExplainPlan(rhsPlanAttributes);
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(rootBuilder, getContext(),
getTableRef());
+ }
return new ExplainPlan(steps, rootBuilder.build());
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java
index 8d15872e7b..a704750319 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java
@@ -40,6 +40,7 @@ import org.apache.phoenix.execute.visitor.QueryPlanVisitor;
import org.apache.phoenix.expression.OrderByExpression;
import org.apache.phoenix.iterate.ConcatResultIterator;
import org.apache.phoenix.iterate.DefaultParallelScanGrouper;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.iterate.LimitingResultIterator;
import org.apache.phoenix.iterate.MergeSortTopNResultIterator;
import org.apache.phoenix.iterate.OffsetResultIterator;
@@ -242,6 +243,9 @@ public class UnionPlan implements QueryPlan {
// branch is preserved and explaining the union does not trigger sub-plan
execution.
UnionResultIterators.explainBranches(plans, steps, builder);
addUnionTailLines(steps, builder);
+ if (getContext().isRoot()) {
+ ExplainTable.populateTopOfPlanAttributes(builder, getContext(),
getTableRef());
+ }
return new ExplainPlan(steps, builder.build());
}
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 ce20eb4571..ca08b984fd 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
@@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.apache.hadoop.hbase.HRegionLocation;
@@ -34,6 +35,7 @@ import org.apache.hadoop.hbase.filter.PageFilter;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.io.TimeRange;
import org.apache.hadoop.hbase.util.Bytes;
+import org.apache.phoenix.compile.ExplainPlanAttributes;
import
org.apache.phoenix.compile.ExplainPlanAttributes.ExplainPlanAttributesBuilder;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.compile.OrderByCompiler.OrderBy;
@@ -55,10 +57,12 @@ import org.apache.phoenix.query.QueryServices;
import org.apache.phoenix.query.QueryServicesOptions;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.PTable;
+import org.apache.phoenix.schema.PTableType;
import org.apache.phoenix.schema.SortOrder;
import org.apache.phoenix.schema.TableRef;
import org.apache.phoenix.schema.types.PDataType;
import org.apache.phoenix.schema.types.PInteger;
+import org.apache.phoenix.util.CDCUtil;
import org.apache.phoenix.util.MetaDataUtil;
import org.apache.phoenix.util.ScanUtil;
import org.apache.phoenix.util.StringUtil;
@@ -173,6 +177,100 @@ public abstract class ExplainTable {
return indexName;
}
+ /**
+ * Populate the top-of-plan disclosure attributes on the supplied builder.
Should be invoked only
+ * for a root plan (i.e. when {@code context.isRoot()}).
+ * @param builder the attributes builder to populate (no-op when null)
+ * @param context the root statement context
+ * @param tableRef the plan's primary table reference (may be null)
+ */
+ public static void populateTopOfPlanAttributes(ExplainPlanAttributesBuilder
builder,
+ StatementContext context, TableRef tableRef) {
+ if (builder == null || context == null) {
+ return;
+ }
+ if (context.getConnection() != null &&
context.getConnection().getTenantId() != null) {
+ builder.setTenantId(context.getConnection().getTenantId().getString());
+ }
+ PTable table = tableRef == null ? null : tableRef.getTable();
+ if (table != null) {
+ if (table.getType() == PTableType.VIEW) {
+ builder.setViewName(table.getName().getString());
+ if (table.getBaseTableLogicalName() != null) {
+ builder.setViewBaseName(table.getBaseTableLogicalName().getString());
+ }
+ }
+ if (table.isTransactional() && table.getTransactionProvider() != null) {
+ builder.setTxnProvider(table.getTransactionProvider().name());
+ }
+ }
+ String cdcScopes = context.getEncodedCdcIncludeScopes();
+ if (cdcScopes == null && table != null && table.getType() ==
PTableType.CDC) {
+ Set<PTable.CDCChangeScope> scopes = table.getCDCIncludeScopes();
+ if (scopes != null && !scopes.isEmpty()) {
+ cdcScopes = CDCUtil.makeChangeScopeStringFromEnums(scopes);
+ }
+ }
+ if (cdcScopes != null) {
+ builder.setCdcScopes(cdcScopes);
+ }
+ // Aggregate rewrite breadcrumbs across the context and its sub-contexts,
deduping while
+ // preserving first occurrence order. The derived table flatten count is
rendered as a
+ // trailing breadcrumb when non-zero.
+ Set<String> rewrites = new LinkedHashSet<>();
+ int flattenCount = collectAppliedRewrites(context, rewrites);
+ if (flattenCount > 0) {
+ rewrites.add("DERIVED TABLE FLATTENED " + flattenCount);
+ }
+ if (!rewrites.isEmpty()) {
+ builder.setRewrites(new ArrayList<>(rewrites));
+ }
+ }
+
+ private static int collectAppliedRewrites(StatementContext context,
Set<String> out) {
+ int flattenCount = context.getDerivedTableFlattenCount();
+ out.addAll(context.getAppliedRewrites());
+ for (StatementContext sub : context.getSubStatementContexts()) {
+ flattenCount += collectAppliedRewrites(sub, out);
+ }
+ return flattenCount;
+ }
+
+ /**
+ * Render the top-of-plan disclosure lines from the populated attributes and
insert them at the
+ * head of {@code planSteps}.
+ * @param planSteps the rendered plan step lines to prepend onto (no-op when
null)
+ * @param attrs the already-populated root plan attributes (no-op when
null)
+ */
+ public static void renderTopOfPlanText(List<String> planSteps,
ExplainPlanAttributes attrs) {
+ if (planSteps == null || attrs == null) {
+ return;
+ }
+ List<String> lines = new ArrayList<>();
+ if (attrs.getTenantId() != null) {
+ lines.add("TENANT '" + attrs.getTenantId() + "'");
+ }
+ if (attrs.getViewName() != null) {
+ StringBuilder viewLine = new StringBuilder("VIEW
").append(attrs.getViewName());
+ if (attrs.getViewBaseName() != null) {
+ viewLine.append(" OVER ").append(attrs.getViewBaseName());
+ }
+ lines.add(viewLine.toString());
+ }
+ if (attrs.getCdcScopes() != null) {
+ lines.add("CDC SCOPE " + attrs.getCdcScopes());
+ }
+ if (attrs.getTxnProvider() != null) {
+ lines.add("TXN " + attrs.getTxnProvider());
+ }
+ if (attrs.getRewrites() != null) {
+ for (String rewrite : attrs.getRewrites()) {
+ lines.add("REWRITE " + rewrite);
+ }
+ }
+ planSteps.addAll(0, lines);
+ }
+
protected void explain(String prefix, List<String> planSteps,
ExplainPlanAttributesBuilder explainPlanAttributesBuilder,
List<HRegionLocation> regionLocations) {
@@ -384,7 +482,7 @@ public abstract class ExplainTable {
explainPlanAttributesBuilder.setServerOffset(offset);
// Populate the attribute whenever a "SERVER n ROW LIMIT" step is
emitted, including the
// uncovered-index/server-merge path where the limit originates from
the INDEX_LIMIT scan
- // attribute rather than a PageFilter.
+ // attribute.
if (limit != null) {
explainPlanAttributesBuilder.setServerRowLimit(limit);
}
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 2a76e8a6fa..173d8ef29b 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
@@ -80,6 +80,7 @@ import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.util.Pair;
import org.apache.phoenix.call.CallRunner;
import org.apache.phoenix.compile.BaseMutationPlan;
+import org.apache.phoenix.compile.BindManager;
import org.apache.phoenix.compile.CloseStatementCompiler;
import org.apache.phoenix.compile.ColumnProjector;
import org.apache.phoenix.compile.CreateFunctionCompiler;
@@ -93,6 +94,7 @@ import org.apache.phoenix.compile.DropSequenceCompiler;
import org.apache.phoenix.compile.ExplainPlan;
import org.apache.phoenix.compile.ExplainPlanAttributes;
import org.apache.phoenix.compile.ExpressionProjector;
+import org.apache.phoenix.compile.FromCompiler;
import org.apache.phoenix.compile.GroupByCompiler.GroupBy;
import org.apache.phoenix.compile.ListJarsQueryPlan;
import org.apache.phoenix.compile.MutationPlan;
@@ -865,12 +867,18 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
if (!getUdfParseNodes().isEmpty()) {
phoenixStatement.throwIfUnallowedUserDefinedFunctions(getUdfParseNodes());
}
-
- RewriteResult rewriteResult = ParseNodeUtil.rewrite(this,
phoenixStatement.getConnection());
+ // Pre-build the top-level StatementContext so the early
SubselectRewriter/SubqueryRewriter
+ // pass can record top of plan rewrite breadcrumbs onto it. The same
accumulator is then
+ // adopted by the compilation context via
QueryCompiler.withRewriteContext.
+ StatementContext rewriteContext = new StatementContext(phoenixStatement,
+ FromCompiler.EMPTY_TABLE_RESOLVER, new
BindManager(phoenixStatement.getParameters()),
+ new Scan(), new SequenceManager(phoenixStatement));
+ RewriteResult rewriteResult = ParseNodeUtil.rewrite(this,
rewriteContext);
QueryPlan queryPlan = new QueryCompiler(phoenixStatement,
rewriteResult.getRewrittenSelectStatement(),
rewriteResult.getColumnResolver(),
Collections.<PDatum> emptyList(),
phoenixStatement.getConnection().getIteratorFactory(),
- new SequenceManager(phoenixStatement), true, false, null).compile();
+ new SequenceManager(phoenixStatement), true, false,
null).withRewriteContext(rewriteContext)
+ .compile();
queryPlan.getContext().getSequenceManager().validateSequences(seqAction);
return queryPlan;
}
@@ -887,6 +895,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
return true;
}
+ @SuppressWarnings("rawtypes")
@Override
public PDataType getDataType() {
return PVarchar.INSTANCE;
@@ -994,14 +1003,17 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
stmt.getConnection().getQueryServices().getOptimizer().optimize(stmt, dataPlan);
}
final StatementPlan plan = compilePlan;
- List<String> planSteps = plan.getExplainPlan().getPlanSteps();
+ 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) {
- List<String> updatedExplainPlanSteps = new ArrayList<>(planSteps);
- updatedExplainPlanSteps.removeIf(
+ planSteps.removeIf(
planStep -> planStep != null &&
planStep.contains(ExplainTable.REGION_LOCATIONS));
- planSteps = Collections.unmodifiableList(updatedExplainPlanSteps);
}
+ planSteps = Collections.unmodifiableList(planSteps);
List<Tuple> tuples =
Lists.newArrayListWithExpectedSize(planSteps.size());
Long estimatedBytesToScan = plan.getEstimatedBytesToScan();
Long estimatedRowsToScan = plan.getEstimatedRowsToScan();
@@ -1303,6 +1315,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(cdcObjName, dataTable, includeScopes, props, ifNotExists,
bindCount);
}
+ @SuppressWarnings("unchecked")
@Override
public MutationPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1464,6 +1477,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(cursor, select);
}
+ @SuppressWarnings("unchecked")
@Override
public MutationPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1481,6 +1495,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(cursor);
}
+ @SuppressWarnings("unchecked")
@Override
public MutationPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1495,6 +1510,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(cursor);
}
+ @SuppressWarnings("unchecked")
@Override
public MutationPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1509,6 +1525,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(cursor, isNext, fetchLimit);
}
+ @SuppressWarnings("unchecked")
@Override
public QueryPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1604,6 +1621,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(schema, pattern);
}
+ @SuppressWarnings("unchecked")
@Override
public QueryPlan compilePlan(final PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1621,6 +1639,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(pattern);
}
+ @SuppressWarnings("unchecked")
@Override
public QueryPlan compilePlan(final PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1637,6 +1656,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
super(tableName);
}
+ @SuppressWarnings("unchecked")
@Override
public QueryPlan compilePlan(final PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -1801,6 +1821,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
isGrantStatement);
}
+ @SuppressWarnings("unchecked")
@Override
public MutationPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp
seqAction)
throws SQLException {
@@ -2428,6 +2449,7 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
}
}
+ @SuppressWarnings("rawtypes")
public Format getFormatter(PDataType type) {
return connection.getFormatter(type);
}
@@ -3002,10 +3024,6 @@ public class PhoenixStatement implements
PhoenixMonitoredStatement, SQLCloseable
this.lastUpdateCount = lastUpdateCount;
}
- private String getLastUpdateTable() {
- return lastUpdateTable;
- }
-
private void setLastUpdateTable(String lastUpdateTable) {
if (!Strings.isNullOrEmpty(lastUpdateTable)) {
this.lastUpdateTable = lastUpdateTable;
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
index 28372949d1..b2cea9c761 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
@@ -201,16 +201,18 @@ public class QueryOptimizer {
if (replacement != null) {
select = rewriteQueryWithIndexReplacement(statement.getConnection(),
resolver, select,
- replacement);
+ replacement, dataPlan.getContext());
}
}
// Re-compile the plan with option "optimizeSubquery" turned on, so that
enclosed
- // sub-queries can be optimized recursively.
+ // sub-queries can be optimized recursively. Carry forward the top-of-plan
rewrite breadcrumbs
+ // recorded on the data plan's context so they survive the recompile onto
the optimized plan's
+ // context.
QueryCompiler compiler = new QueryCompiler(statement, select,
FromCompiler.getResolverForQuery(select, statement.getConnection()),
targetColumns,
parallelIteratorFactory, dataPlan.getContext().getSequenceManager(),
true, true, dataPlans,
- dataPlan.getContext());
+ dataPlan.getContext()).withRewriteContext(dataPlan.getContext());
return Collections.singletonList(compiler.compile());
}
@@ -218,9 +220,23 @@ public class QueryOptimizer {
PTable index) throws SQLException {
StatementContext context = new StatementContext(dataPlan.getContext());
context.setResolver(FromCompiler.getResolver(dataPlan.getTableRef()));
- return WhereCompiler.contains(
- index.getIndexWhereExpression(dataPlan.getContext().getConnection()),
- WhereCompiler.transformDNF(select.getWhere(), context));
+ boolean usable =
+
WhereCompiler.contains(index.getIndexWhereExpression(dataPlan.getContext().getConnection()),
+ WhereCompiler.transformDNF(select.getWhere(), context));
+ StatementContext rootContext = dataPlan.getContext();
+ String tableName = dataPlan.getTableRef().getTable().getName().getString();
+ String indexName = index.getName().getString();
+ // Dedupe so the breadcrumb is recorded once per table and index pair even
when the optimizer
+ // scores the same index across multiple candidate paths.
+ if (rootContext.markPartialIndexChecked(tableName, indexName)) {
+ // Name the index in the breadcrumb so multiple partial indexes stay
distinct after the
+ // rewrite list is deduped by string. Otherwise they collapse into a
single ambiguous line.
+ rootContext.addAppliedRewrite(usable
+ ? "PARTIAL INDEX " + indexName + " APPLICABLE"
+ : "PARTIAL INDEX " + indexName
+ + " NOT APPLICABLE -- index WHERE not implied by query WHERE");
+ }
+ return usable;
}
private List<QueryPlan> getApplicablePlansForSingleFlatQuery(QueryPlan
dataPlan,
@@ -281,12 +297,9 @@ public class QueryOptimizer {
QueryPlan> singletonList(recordDecision(dataPlan,
OptimizerReasons.RULE_DATA_TABLE, state));
}
// The targetColumns is set for UPSERT SELECT to ensure that the proper
type conversion takes
- // place.
- // For a SELECT, it is empty. In this case, we want to set the
targetColumns to match the
- // projection
- // from the dataPlan to ensure that the metadata for when an index is used
matches the metadata
- // for
- // when the data table is used.
+ // place. For a SELECT, it is empty. In this case, we want to set the
targetColumns to match
+ // the projection from the dataPlan to ensure that the metadata for when
an index is used
+ // matches the metadata for when the data table is used.
if (targetColumns.isEmpty()) {
List<? extends ColumnProjector> projectors =
dataPlan.getProjector().getColumnProjectors();
List<PDatum> targetDatums =
Lists.newArrayListWithExpectedSize(projectors.size());
@@ -314,8 +327,10 @@ public class QueryOptimizer {
stopAtBestPlan && hintedPlan.isApplicable()
&& (index.getIndexWhere() == null ||
isPartialIndexUsable(select, dataPlan, index))
) {
- return Collections
+ List<QueryPlan> hinted = Collections
.singletonList(recordDecision(hintedPlan,
OptimizerReasons.RULE_HINT, state));
+ carryForwardRewrites(dataPlan.getContext(), hinted);
+ return hinted;
}
plans.add(0, hintedPlan);
}
@@ -338,7 +353,9 @@ public class QueryOptimizer {
) {
// Query can't possibly return anything so just return this plan.
if (plan.isDegenerate()) {
- return Collections.singletonList(plan);
+ List<QueryPlan> degenerate = Collections.singletonList(plan);
+ carryForwardRewrites(dataPlan.getContext(), degenerate);
+ return degenerate;
}
plans.add(plan);
} else if (plan == null) {
@@ -364,9 +381,34 @@ public class QueryOptimizer {
}
// OrderPlans
- return hintedPlan == null
+ List<QueryPlan> orderedPlans = hintedPlan == null
? orderPlansBestToWorst(select, applicablePlans, stopAtBestPlan, state,
forCDC)
: applicablePlans;
+ carryForwardRewrites(dataPlan.getContext(), orderedPlans);
+ return orderedPlans;
+ }
+
+ /**
+ * Carry the rewrite breadcrumbs recorded on the data plan's context onto
the candidate plans'
+ * contexts. The partial index applicability decisions are recorded once per
index on the shared
+ * root context while scoring plans, so that all of them stay visible even
though only one wins.
+ */
+ private static void carryForwardRewrites(StatementContext from,
List<QueryPlan> plans) {
+ List<String> rewrites = from.getAppliedRewrites();
+ if (rewrites.isEmpty()) {
+ return;
+ }
+ for (QueryPlan plan : plans) {
+ StatementContext to = plan.getContext();
+ if (to == from) {
+ continue;
+ }
+ for (String rewrite : rewrites) {
+ if (!to.getAppliedRewrites().contains(rewrite)) {
+ to.addAppliedRewrite(rewrite);
+ }
+ }
+ }
}
private QueryPlan getHintedQueryPlan(PhoenixStatement statement,
SelectStatement select,
@@ -631,11 +673,12 @@ public class QueryOptimizer {
new Hint[] { Hint.INDEX,
Hint.NO_CHILD_PARENT_JOIN_OPTIMIZATION }),
FACTORY.hint("NO_INDEX"));
SelectStatement query = FACTORY.select(dataSelect, hint,
outerWhere);
- RewriteResult rewriteResult = ParseNodeUtil.rewrite(query,
statement.getConnection());
+ RewriteResult rewriteResult = ParseNodeUtil.rewrite(query,
dataPlan.getContext());
QueryPlan plan =
new QueryCompiler(statement,
rewriteResult.getRewrittenSelectStatement(),
rewriteResult.getColumnResolver(), targetColumns,
parallelIteratorFactory,
- dataPlan.getContext().getSequenceManager(), isProjected, true,
dataPlans).compile();
+ dataPlan.getContext().getSequenceManager(), isProjected, true,
dataPlans)
+ .withRewriteContext(dataPlan.getContext()).compile();
return AddPlanResult.success(plan);
}
}
@@ -908,9 +951,7 @@ public class QueryOptimizer {
return null;
}
- /**
- * The bound-PK-column count for {@code plan}.
- */
+ /** The bound-PK-column count for {@code plan}. */
private static int adjustedBoundCount(QueryPlan plan, int boundRanges) {
PTable table = plan.getTableRef().getTable();
int boundCount = plan.getContext().getScanRanges().getBoundPkColumnCount();
@@ -1002,7 +1043,8 @@ public class QueryOptimizer {
private static SelectStatement rewriteQueryWithIndexReplacement(
final PhoenixConnection connection, final ColumnResolver resolver, final
SelectStatement select,
- final Map<TableRef, TableRef> replacement) throws SQLException {
+ final Map<TableRef, TableRef> replacement, final StatementContext
breadcrumbContext)
+ throws SQLException {
TableNode from = select.getFrom();
TableNode newFrom = from.accept(new QueryOptimizerTableNode(resolver,
replacement));
if (from == newFrom) {
@@ -1015,7 +1057,8 @@ public class QueryOptimizer {
// replace expressions with corresponding matching columns for
functional indexes
indexSelect = ParseNodeRewriter.rewrite(indexSelect,
new IndexExpressionParseNodeRewriter(indexTableRef.getTable(),
- indexTableRef.getTableAlias(), connection,
indexSelect.getUdfParseNodes()));
+ indexTableRef.getTableAlias(), connection,
indexSelect.getUdfParseNodes(),
+ breadcrumbContext));
}
return indexSelect;
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/IndexExpressionParseNodeRewriter.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/IndexExpressionParseNodeRewriter.java
index 67085741d5..8d93f6415f 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/IndexExpressionParseNodeRewriter.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/IndexExpressionParseNodeRewriter.java
@@ -46,6 +46,12 @@ public class IndexExpressionParseNodeRewriter extends
ParseNodeRewriter {
public IndexExpressionParseNodeRewriter(PTable index, String alias,
PhoenixConnection connection,
Map<String, UDFParseNode> udfParseNodes) throws SQLException {
+ this(index, alias, connection, udfParseNodes, null);
+ }
+
+ public IndexExpressionParseNodeRewriter(PTable index, String alias,
PhoenixConnection connection,
+ Map<String, UDFParseNode> udfParseNodes, StatementContext
breadcrumbContext)
+ throws SQLException {
indexedParseNodeToColumnParseNodeMap =
Maps.newHashMapWithExpectedSize(index.getColumns().size());
NamedTableNode tableNode =
@@ -74,6 +80,10 @@ public class IndexExpressionParseNodeRewriter extends
ParseNodeRewriter {
columnParseNode = NODE_FACTORY.cast(columnParseNode,
expressionDataType, null, null);
}
indexedParseNodeToColumnParseNodeMap.put(indexedParseNode,
columnParseNode);
+ if (breadcrumbContext != null) {
+ breadcrumbContext.addAppliedRewrite("INDEX EXPRESSION " +
expressionStr + " AS " + colName);
+ breadcrumbContext.addIndexExpressionSubstitution(indexedParseNode,
colName);
+ }
}
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/util/ParseNodeUtil.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/util/ParseNodeUtil.java
index 4d96f1bf9c..e3c1b83aa6 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/util/ParseNodeUtil.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/util/ParseNodeUtil.java
@@ -23,6 +23,7 @@ import java.util.Set;
import org.apache.phoenix.compile.ColumnResolver;
import org.apache.phoenix.compile.FromCompiler;
import org.apache.phoenix.compile.QueryCompiler;
+import org.apache.phoenix.compile.StatementContext;
import org.apache.phoenix.compile.StatementNormalizer;
import org.apache.phoenix.compile.SubqueryRewriter;
import org.apache.phoenix.compile.SubselectRewriter;
@@ -159,16 +160,20 @@ public class ParseNodeUtil {
/**
* Optimize rewriting {@link SelectStatement} by {@link SubselectRewriter}
and
* {@link SubqueryRewriter} before {@link QueryCompiler#compile}.
+ * <p>
+ * The supplied {@link StatementContext} is the top level context that
carries top-of-plan rewrite
+ * breadcrumb.
*/
- public static RewriteResult rewrite(SelectStatement selectStatement,
- PhoenixConnection phoenixConnection) throws SQLException {
+ public static RewriteResult rewrite(SelectStatement selectStatement,
StatementContext context)
+ throws SQLException {
+ PhoenixConnection phoenixConnection = context.getConnection();
SelectStatement selectStatementToUse =
- SubselectRewriter.flatten(selectStatement, phoenixConnection);
+ SubselectRewriter.flatten(selectStatement, phoenixConnection, context);
ColumnResolver columnResolver =
FromCompiler.getResolverForQuery(selectStatementToUse,
phoenixConnection);
selectStatementToUse = StatementNormalizer.normalize(selectStatementToUse,
columnResolver);
SelectStatement transformedSubquery =
- SubqueryRewriter.transform(selectStatementToUse, columnResolver,
phoenixConnection);
+ SubqueryRewriter.transform(selectStatementToUse, columnResolver,
phoenixConnection, context);
if (transformedSubquery != selectStatementToUse) {
columnResolver = FromCompiler.getResolverForQuery(transformedSubquery,
phoenixConnection);
transformedSubquery = StatementNormalizer.normalize(transformedSubquery,
columnResolver);
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java
index 08471a9e5b..5162930c3f 100644
---
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java
+++
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/ChildViewsUseParentViewIndexIT.java
@@ -269,7 +269,6 @@ public class ChildViewsUseParentViewIndexIT extends
ParallelStatsDisabledIT {
.keyRanges(" [" + Short.MIN_VALUE +
",'00Dxxxxxxxxxxx1','003xxxxxxxxxxx1',*] - ["
+ Short.MIN_VALUE + ",'00Dxxxxxxxxxxx1','003xxxxxxxxxxx5',~'2016-01-01
06:00:00.000']")
.indexRule(OptimizerReasons.RULE_MORE_BOUND_PK_COLUMNS).indexRejectedNone();
-
ResultSet rs = conn.createStatement().executeQuery(sql);
for (int i = 0; i < expectedRows; ++i) {
assertTrue(rs.next());
diff --git
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/PartialIndexIT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/PartialIndexIT.java
index eeeb02bdf0..f59c9b74e2 100644
---
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/PartialIndexIT.java
+++
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/index/PartialIndexIT.java
@@ -243,18 +243,16 @@ public class PartialIndexIT extends BaseTest {
assertEquals("b", rs.getString(1));
assertFalse(rs.next());
// explain plan verify to check if partial index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(indexTableName));
+ assertTrue(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).toString().contains(indexTableName));
selectSql = "SELECT D from " + dataTableName + " WHERE A = 50";
rs = conn.createStatement().executeQuery(selectSql);
// Verify that the index table is not used
assertCurrentTable((PhoenixResultSet) rs, "", dataTableName);
// explain plan verify to check if partial index is not used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertFalse(rs.getString(1).contains(indexTableName));
+ assertFalse(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).get(0).contains(indexTableName));
// Add more rows to test the index write path
conn.createStatement()
@@ -292,9 +290,8 @@ public class PartialIndexIT extends BaseTest {
assertTrue(rs.next());
assertEquals("id2", rs.getString(1));
// explain plan verify to check if partial index is not used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertFalse(rs.getString(1).contains(indexTableName));
+ assertFalse(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).get(0).contains(indexTableName));
// Test index verification and repair by IndexTool
verifyIndex(dataTableName, indexTableName);
@@ -306,6 +303,36 @@ public class PartialIndexIT extends BaseTest {
}
}
+ @Test
+ public void testPartialIndexBreadcrumbsAreDistinctPerIndex() throws
Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl())) {
+ String dataTableName = generateUniqueName();
+ conn.createStatement()
+ .execute("create table " + dataTableName + " (id varchar not null
primary key, "
+ + "A integer, B integer, C double, D varchar) COLUMN_ENCODED_BYTES=0"
+ + (salted ? ", SALT_BUCKETS=4" : ""));
+ // Two partial indexes on the same table with different WHERE
predicates. The index is
+ // created on an empty table so it is active immediately without an
async rebuild.
+ String indexName1 = generateUniqueName();
+ String indexName2 = generateUniqueName();
+ conn.createStatement()
+ .execute("CREATE " + (uncovered ? "UNCOVERED " : " ") + (local ?
"LOCAL " : " ") + "INDEX "
+ + indexName1 + " on " + dataTableName + " (A) " + (uncovered ? "" :
"INCLUDE (B, C, D)")
+ + " WHERE A > 50");
+ conn.createStatement()
+ .execute("CREATE " + (uncovered ? "UNCOVERED " : " ") + (local ?
"LOCAL " : " ") + "INDEX "
+ + indexName2 + " on " + dataTableName + " (B) " + (uncovered ? "" :
"INCLUDE (A, C, D)")
+ + " WHERE B > 50");
+ // A query whose WHERE implies both index WHERE clauses makes the
optimizer evaluate both
+ // partial indexes, so both applicability decisions are recorded. Each
breadcrumb names its
+ // index.
+ String selectSql = "SELECT D from " + dataTableName + " WHERE A > 60 AND
B > 60";
+ ExplainPlanTestUtil.assertPlan(conn, selectSql)
+ .rewriteContains("PARTIAL INDEX " + indexName1 + " APPLICABLE")
+ .rewriteContains("PARTIAL INDEX " + indexName2 + " APPLICABLE");
+ }
+ }
+
@Test
public void testComparisonOfColumns() throws Exception {
try (Connection conn = DriverManager.getConnection(getUrl())) {
@@ -340,18 +367,16 @@ public class PartialIndexIT extends BaseTest {
assertEquals("a", rs.getString(1));
assertFalse(rs.next());
// explain plan verify to check if partial index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(indexTableName));
+ assertTrue(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).toString().contains(indexTableName));
selectSql = "SELECT D from " + dataTableName + " WHERE A > 100";
rs = conn.createStatement().executeQuery(selectSql);
// Verify that the index table is not used
assertCurrentTable((PhoenixResultSet) rs, "", dataTableName);
// explain plan verify to check if partial index is not used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertFalse(rs.getString(1).contains(indexTableName));
+ assertFalse(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).get(0).contains(indexTableName));
// Add more rows to test the index write path
conn.createStatement()
@@ -429,9 +454,8 @@ public class PartialIndexIT extends BaseTest {
assertEquals("a", rs.getString(2));
assertFalse(rs.next());
// explain plan verify to check if partial index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(indexTableName));
+ assertTrue(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).toString().contains(indexTableName));
// Add more rows to test the index write path
conn.createStatement()
@@ -460,10 +484,8 @@ public class PartialIndexIT extends BaseTest {
assertTrue(rs.next());
assertEquals(5, rs.getInt(1));
// explain plan verify to check if partial index is not used
- rs =
- conn.createStatement().executeQuery("EXPLAIN " + "SELECT Count(*) from
" + dataTableName);
- assertTrue(rs.next());
- assertFalse(rs.getString(1).contains(indexTableName));
+ assertFalse(ExplainPlanTestUtil.getPlanSteps(conn, "SELECT Count(*) from
" + dataTableName)
+ .get(0).contains(indexTableName));
// Overwrite an existing row that satisfies the index WHERE clause such
that
// the new version of the row does not satisfy the index where clause
anymore. This
@@ -519,9 +541,8 @@ public class PartialIndexIT extends BaseTest {
assertEquals("abcdef", rs.getString(1));
assertFalse(rs.next());
// explain plan verify to check if partial index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(indexTableName));
+ assertTrue(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).toString().contains(indexTableName));
// Add more rows to test the index write path
conn.createStatement()
@@ -548,9 +569,8 @@ public class PartialIndexIT extends BaseTest {
assertTrue(rs.next());
assertEquals(5, rs.getInt(1));
// explain plan verify to check if partial index is not used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertFalse(rs.getString(1).contains(indexTableName));
+ assertFalse(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).get(0).contains(indexTableName));
// Overwrite an existing row that satisfies the index WHERE clause such
that
// the new version of the row does not satisfy the index where clause
anymore. This
@@ -611,9 +631,8 @@ public class PartialIndexIT extends BaseTest {
assertEquals(70, rs.getInt(1));
assertFalse(rs.next());
// explain plan verify to check if partial index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(indexTableName));
+ assertTrue(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).toString().contains(indexTableName));
// Add more rows to test the index write path
conn.createStatement().execute("upsert into " + dataTableName + " values
('id2', 20, 3)");
@@ -627,10 +646,8 @@ public class PartialIndexIT extends BaseTest {
assertTrue(rs.next());
assertEquals(4, rs.getInt(1));
// explain plan verify to check if partial index is not used
- rs =
- conn.createStatement().executeQuery("EXPLAIN " + "SELECT Count(*) from
" + dataTableName);
- assertTrue(rs.next());
- assertFalse(rs.getString(1).contains(indexTableName));
+ assertFalse(ExplainPlanTestUtil.getPlanSteps(conn, "SELECT Count(*) from
" + dataTableName)
+ .get(0).contains(indexTableName));
rs = conn.createStatement().executeQuery("SELECT Count(*) from " +
indexTableName);
assertTrue(rs.next());
@@ -649,9 +666,8 @@ public class PartialIndexIT extends BaseTest {
assertEquals(0, rs.getInt(1));
assertFalse(rs.next());
// explain plan verify to check if partial index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(indexTableName));
+ assertTrue(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).toString().contains(indexTableName));
// Test index verification and repair by IndexTool
verifyIndex(dataTableName, indexTableName);
@@ -805,9 +821,8 @@ public class PartialIndexIT extends BaseTest {
assertEquals("b", rs.getString(1));
assertFalse(rs.next());
// explain plan verify to check if partial index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(partialIndexTableName));
+ assertTrue(ExplainPlanTestUtil.getPlanSteps(conn, selectSql).toString()
+ .contains(partialIndexTableName));
selectSql = "SELECT D from " + dataTableName + " WHERE A < 50";
// Verify that the full index table is used
@@ -817,9 +832,8 @@ public class PartialIndexIT extends BaseTest {
assertEquals("a", rs.getString(1));
assertFalse(rs.next());
// explain plan verify to check if full index is used
- rs = conn.createStatement().executeQuery("EXPLAIN " + selectSql);
- assertTrue(rs.next());
- assertTrue(rs.getString(1).contains(fullIndexTableName));
+ assertTrue(
+ ExplainPlanTestUtil.getPlanSteps(conn,
selectSql).toString().contains(fullIndexTableName));
}
}
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java
index 6665d18d88..6760e5b0f7 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/compile/TenantSpecificViewIndexCompileTest.java
@@ -245,6 +245,7 @@ public class TenantSpecificViewIndexCompileTest extends
BaseConnectionlessQueryT
// -----------------------------------------------------------------
// Private Helper Methods
// -----------------------------------------------------------------
+
private Connection createTenantSpecificConnection() throws SQLException {
Connection conn;
Properties props = new Properties();
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
index bb7f8133c4..09ece953c6 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTest.java
@@ -47,6 +47,7 @@ import org.apache.hadoop.hbase.client.RegionInfoBuilder;
import org.apache.phoenix.compile.ExplainPlan;
import org.apache.phoenix.compile.ExplainPlanAttributes;
import
org.apache.phoenix.compile.ExplainPlanAttributes.ExplainPlanAttributesBuilder;
+import org.apache.phoenix.iterate.ExplainTable;
import org.apache.phoenix.optimize.OptimizerReasons;
import org.apache.phoenix.query.BaseConnectionlessQueryTest;
import org.apache.phoenix.query.QueryServices;
@@ -183,7 +184,8 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
text("CLIENT PARALLEL <N>-WAY REVERSE RANGE SCAN OVER PTSDB2 ['na1']", "
INDEX PTSDB2",
" REGIONS PLANNED <N>", " SERVER PROJECTION FILTER BY FIRST KEY
ONLY"),
scanAttrs("RANGE SCAN ", "PTSDB2", "
['na1']").put("serverFirstKeyOnlyProjection", true)
- .put("clientSortedBy", "REVERSE"));
+ .put("clientSortedBy", "REVERSE")
+ .set("rewrites", rewriteList("REVERSE SCAN SUBSTITUTION")));
}
@Test
@@ -474,10 +476,9 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
@Test
public void testSortMergeJoin() throws Exception {
- // After the SMJ reshape the root is a synthetic node carrying just the
join header and the
- // two operand plans as separate lhs / rhs children.
- // Join operand leaves are compiled through the join path, not optimizer
index selection, so
- // they carry no decision rule.
+ // In a SMJ the root is a synthetic node carrying the join header and the
two operand plans as
+ // lhs and rhs. Join operand leaves are compiled through the join path,
not optimizer index
+ // selection, so they carry no decision rule.
ObjectNode lhs =
scanAttrs("RANGE SCAN ", "ATABLE", "
['00D000000000001']").putNull("indexRule");
ObjectNode rhs = scanAttrs("FULL SCAN ", "ATABLE",
"").putNull("indexRule");
@@ -501,8 +502,8 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
public void testHashJoinInner() throws Exception {
// HashJoinPlan root attributes come from the delegate scan. Each
hash/skip-scan child
// is recorded under subPlans with its join header on abstractExplainPlan.
- // Hash join nodes (the delegate root and the build-side child) are
compiled through the join
- // path, not optimizer index selection, so they carry no decision rule.
+ // Hash joins are compiled through the join path, not optimizer index
selection, so they carry
+ // no decision rule.
ObjectNode child = scanAttrs("FULL SCAN ", "ATABLE", "")
.put("abstractExplainPlan", "PARALLEL INNER-JOIN TABLE 0 /* HASH BUILD
RIGHT */")
.putNull("indexRule");
@@ -525,35 +526,46 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
@Test
public void testHashJoinSemiInSubquery() throws Exception {
- // Phoenix temp aliases are renamed by first appearance so this case
asserts on the canonical
- // "$1.$2" form.
- ObjectNode child = scanAttrs("FULL SCAN ", "ATABLE", "")
- .put("abstractExplainPlan", "SKIP-SCAN-JOIN TABLE 0 /* HASH BUILD RIGHT
*/")
- .put("serverWhereFilter", "SERVER FILTER BY A_INTEGER = 1")
- .put("serverAggregate", "SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY
[ORGANIZATION_ID]");
- // The outer hash-join scan is compiled through the subquery/join path and
carries no decision
- // rule; the aggregate subquery build side is optimized separately and
keeps its data-table
- // rule.
- ObjectNode expected = scanAttrs("FULL SCAN ", "ATABLE",
"").putNull("indexRule");
- expected.set("subPlans", mapper.createArrayNode().add(child));
- expected.put("dynamicServerFilter",
- "DYNAMIC SERVER FILTER BY ATABLE.ORGANIZATION_ID IN ($1.$2)");
- verifyQuery("hashJoinSemiInSubquery",
- "SELECT a_string FROM atable"
- + " WHERE organization_id IN (SELECT organization_id FROM atable WHERE
a_integer = 1)",
- text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", " INDEX ATABLE",
- " REGIONS PLANNED <N>", " SKIP-SCAN-JOIN TABLE 0 /* HASH BUILD
RIGHT */",
- " CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "
INDEX ATABLE",
- " REGIONS PLANNED <N>", " SERVER FILTER BY
A_INTEGER = 1",
- " SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY
[ORGANIZATION_ID]",
- " DYNAMIC SERVER FILTER BY ATABLE.ORGANIZATION_ID IN ($1.$2)"),
- expected);
+ // The behavior under test is the IN-subquery -> semi-join rewrite
breadcrumb, asserted via
+ // attributes below. The optimizer's behavior is nondeterministic.
+ // WhereOptimizer.getKeyExpressionCombination probes the row key using
+ // PDataType.getSampleValue, which for CHAR/VARCHAR row keys returns a
value from an
+ // unseeded ThreadLocal Random. The choice flips between SKIP-SCAN-JOIN
and PARALLEL
+ // SEMI-JOIN ... SKIP MERGE from run to run. Every other rewrite
breadcrumb is stable and
+ // asserted exactly. Phoenix temp aliases are renamed by first appearance
so the dynamic
+ // server filter asserts on the canonical "$1.$2" form.
+ String query = "SELECT a_string FROM atable"
+ + " WHERE organization_id IN (SELECT organization_id FROM atable WHERE
a_integer = 1)";
+ String skipScanJoin = " SKIP-SCAN-JOIN TABLE 0 /* HASH BUILD RIGHT */";
+ String parallelSemiJoin = " PARALLEL SEMI-JOIN TABLE 0 /* HASH BUILD
RIGHT, SKIP MERGE */";
+ // Index 3 is the unstable join-operator line. null marks it as
"tolerated".
+ List<String> expectedStable = text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER
ATABLE",
+ " INDEX ATABLE", " REGIONS PLANNED <N>", null,
+ " CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "
INDEX ATABLE",
+ " REGIONS PLANNED <N>", " SERVER FILTER BY
A_INTEGER = 1",
+ " SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY
[ORGANIZATION_ID]",
+ " DYNAMIC SERVER FILTER BY ATABLE.ORGANIZATION_ID IN ($1.$2)");
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlan plan = ExplainPlanTestUtil.getExplainPlan(conn, query);
+ List<String> actual = oracle.normalizeText(plan.getPlanSteps());
+ assertEquals("plan line count, was " + actual, expectedStable.size(),
actual.size());
+ for (int i = 0; i < expectedStable.size(); i++) {
+ if (expectedStable.get(i) == null) {
+ assertTrue("unexpected join-strategy line: " + actual.get(i),
+ actual.get(i).equals(skipScanJoin) ||
actual.get(i).equals(parallelSemiJoin));
+ } else {
+ assertEquals("@" + i, expectedStable.get(i), actual.get(i));
+ }
+ }
+ // The dynamic server filter line is normalized above.
+ ExplainPlanTestUtil.assertPlan(plan.getPlanStepsAsAttributes())
+ .rewriteContains("IN SUBQUERY AS SEMI JOIN");
+ }
}
@Test
public void testUnionAll() throws Exception {
- // The union composes each branch recursively from its own
getExplainPlan() into the subPlans
- // list.
+ // The union composes each branch recursively from its own explain plan
into subPlans.
ObjectNode lhs = scanAttrs("RANGE SCAN ", "ATABLE", "
['00D000000000001']");
ObjectNode rhs = scanAttrs("RANGE SCAN ", "ATABLE", "
['00D000000000002']");
ObjectNode expected = defaultAttrs();
@@ -573,9 +585,8 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
@Test
public void testUnionAllOfHashJoins() throws Exception {
// A UNION ALL whose branches are each hash joins. Exercises recursive
explain composition end
- // to end and
- // confirms each branch carries its own subPlans and dynamicServerFilter.
- // Every node in a union-of-hash-joins is compiled through the join path,
so none carry a
+ // to end and confirms each branch carries its own subPlans and
dynamicServerFilter.
+ // Every node in a union of hash joins is compiled through the join path,
so none carry a
// decision rule.
ObjectNode joinChild1 = scanAttrs("FULL SCAN ", "ATABLE", "")
.put("abstractExplainPlan", "PARALLEL INNER-JOIN TABLE 0 /* HASH BUILD
RIGHT */")
@@ -715,7 +726,8 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
verifyQuery("multiTenantView", "SELECT * FROM " + MT_VIEW + " LIMIT 1",
tenantProps,
text("CLIENT SERIAL <N>-WAY RANGE SCAN OVER EO_MT_BASE ['tenant42']", "
INDEX EO_MT_VIEW",
" REGIONS PLANNED <N>", " SERVER 1 ROW LIMIT", "CLIENT 1 ROW
LIMIT"),
- attrs().put("iteratorTypeAndScanSize", "SERIAL
<N>-WAY").put("consistency", "STRONG")
+ attrs().put("tenantId", TENANT_ID).put("viewName",
MT_VIEW).put("viewBaseName", MT_BASE)
+ .put("iteratorTypeAndScanSize", "SERIAL <N>-WAY").put("consistency",
"STRONG")
.put("explainScanType", "RANGE SCAN ").put("tableName", "EO_MT_BASE")
.put("indexName", "EO_MT_VIEW").put("indexRule", "data table")
.put("keyRanges", " ['tenant42']").put("serverRowLimit",
1).put("clientRowLimit", 1)
@@ -834,6 +846,224 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
}
}
+ @Test
+ public void testRewriteReverseScanSubstitution() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil.assertPlan(conn, "SELECT inst,\"DATE\" FROM ptsdb2
WHERE inst = 'na1'"
+ + " ORDER BY inst DESC, \"DATE\" DESC").rewriteContains("REVERSE SCAN
SUBSTITUTION");
+ }
+ }
+
+ @Test
+ public void testRewriteInSubqueryAsSemiJoin() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil
+ .assertPlan(conn,
+ "SELECT a_string FROM atable"
+ + " WHERE organization_id IN (SELECT organization_id FROM atable
WHERE a_integer = 1)")
+ .rewriteContains("IN SUBQUERY AS SEMI JOIN");
+ }
+ }
+
+ @Test
+ public void testRewriteNotInSubqueryAsAntiJoin() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil.assertPlan(conn, "SELECT a_string FROM atable"
+ + " WHERE organization_id NOT IN (SELECT organization_id FROM atable
WHERE a_integer = 1)")
+ .rewriteContains("NOT IN SUBQUERY AS ANTI JOIN");
+ }
+ }
+
+ @Test
+ public void testRewriteExistsSubqueryAsSemiJoin() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil.assertPlan(conn,
+ "SELECT a_string FROM atable a WHERE EXISTS"
+ + " (SELECT 1 FROM atable b WHERE b.organization_id =
a.organization_id"
+ + " AND b.a_integer = 1)")
+ .rewriteContains("EXISTS SUBQUERY AS SEMI JOIN");
+ }
+ }
+
+ @Test
+ public void testRewriteNotExistsSubqueryAsAntiJoin() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil.assertPlan(conn,
+ "SELECT a_string FROM atable a WHERE NOT EXISTS"
+ + " (SELECT 1 FROM atable b WHERE b.organization_id =
a.organization_id"
+ + " AND b.a_integer = 1)")
+ .rewriteContains("NOT EXISTS SUBQUERY AS ANTI JOIN");
+ }
+ }
+
+ @Test
+ public void testRewriteScalarSubqueryAsInnerJoin() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil
+ .assertPlan(conn,
+ "SELECT a_string FROM atable a WHERE a_integer ="
+ + " (SELECT max(a_integer) FROM atable b WHERE b.organization_id =
a.organization_id)")
+ .rewriteContains("SCALAR SUBQUERY AS INNER JOIN");
+ }
+ }
+
+ @Test
+ public void testRewriteHavingPredicateAsWhere() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil
+ .assertPlan(conn, "SELECT count(1) FROM atable GROUP BY a_string
HAVING a_string = 'a'")
+ .rewriteContains("HAVING PREDICATE AS WHERE");
+ }
+ }
+
+ @Test
+ public void testRewriteDerivedTableFlattened() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil
+ .assertPlan(conn, "SELECT a_string FROM (SELECT a_string FROM atable)
WHERE a_string = 'a'")
+ .rewriteContains("DERIVED TABLE FLATTENED 1");
+ }
+ }
+
+ @Test
+ public void testRewriteUnionOrderByMerge() throws Exception {
+ // The UNION ORDER BY merge optimization only fires when a union is the
inner select of an
+ // outer query whose GROUP BY / ORDER BY is order-preserving over the
union output.
+ Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+ props.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
+ props.setProperty(QueryServices.FORCE_ROW_KEY_ORDER_ATTRIB,
Boolean.toString(false));
+ try (Connection conn = DriverManager.getConnection(getUrl(), props);
+ java.sql.Statement stmt = conn.createStatement()) {
+ String t1 = generateUniqueName();
+ String t2 = generateUniqueName();
+ stmt.execute("CREATE TABLE " + t1 + " (fuid UNSIGNED_LONG NOT NULL,"
+ + " fstatsdate UNSIGNED_LONG NOT NULL, faid_1 UNSIGNED_LONG NOT NULL,"
+ + " clk_pv_1 UNSIGNED_LONG CONSTRAINT pk PRIMARY KEY (fuid,
fstatsdate, faid_1))");
+ stmt.execute("CREATE TABLE " + t2 + " (fuid UNSIGNED_LONG NOT NULL,"
+ + " fstatsdate UNSIGNED_LONG NOT NULL, faid_2 UNSIGNED_LONG NOT NULL,"
+ + " clk_pv_2 UNSIGNED_LONG CONSTRAINT pk PRIMARY KEY (fuid,
fstatsdate, faid_2))");
+ String unionSql = "(SELECT fuid AS advertiser_id, faid_1 AS adgroup_id,"
+ + " fstatsdate AS date, SUM(clk_pv_1) AS clicks FROM " + t1
+ + " GROUP BY fuid, faid_1, fstatsdate UNION ALL"
+ + " SELECT fuid AS advertiser_id, faid_2 AS adgroup_id,"
+ + " fstatsdate AS date, SUM(clk_pv_2) AS clicks FROM " + t2
+ + " GROUP BY fuid, faid_2, fstatsdate)";
+ String sql = "SELECT advertiser_id, adgroup_id, date, SUM(clicks) AS
clicks FROM " + unionSql
+ + " GROUP BY advertiser_id, adgroup_id, date ORDER BY advertiser_id,
adgroup_id, date";
+ ExplainPlanTestUtil.assertPlan(conn, sql).rewriteContains("UNION ORDER
BY MERGE");
+ }
+ }
+
+ @Test
+ public void testRewriteStarJoin() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil
+ .assertPlan(conn,
+ "SELECT a.a_string, b.a_string, c.a_string FROM atable a"
+ + " JOIN atable b ON a.organization_id = b.organization_id"
+ + " JOIN atable c ON a.organization_id = c.organization_id"
+ + " WHERE a.organization_id = '00D000000000001'")
+ .rewriteContains("STAR JOIN ON 2 RIGHT LEGS");
+ }
+ }
+
+ @Test
+ public void testRewriteRightJoinAsLeftJoin() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil
+ .assertPlan(conn,
+ "SELECT a.a_string, b.a_string FROM atable a"
+ + " RIGHT JOIN atable b ON a.organization_id = b.organization_id"
+ + " WHERE b.organization_id = '00D000000000001'")
+ .rewriteContains("RIGHT JOIN AS LEFT JOIN");
+ }
+ }
+
+ @Test
+ public void testDisclosureTenantAndView() throws Exception {
+ Properties tenantProps = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+ tenantProps.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
+ tenantProps.setProperty(TENANT_ID_ATTRIB, TENANT_ID);
+ try (Connection conn = DriverManager.getConnection(getUrl(), tenantProps))
{
+ ExplainPlanTestUtil.assertPlan(conn, "SELECT * FROM " + MT_VIEW + "
LIMIT 1")
+ .tenant(TENANT_ID).view(MT_VIEW, MT_BASE);
+ }
+ }
+
+ @Test
+ public void testDisclosureNoneOnPlainScan() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ ExplainPlanTestUtil.assertPlan(conn, "SELECT * FROM
atable").tenantNone().viewNone()
+ .cdcScopesNone().txnProviderNone().rewritesNone();
+ }
+ }
+
+ /**
+ * End-to-end check that the disclosure block is prepended to the real JDBC
{@code EXPLAIN} result
+ * set (the {@link org.apache.phoenix.jdbc.PhoenixStatement} path that
invokes
+ * {@link ExplainTable#renderTopOfPlanText}), with {@code TENANT} then
{@code VIEW} ahead of the
+ * first operator line.
+ */
+ @Test
+ public void testTopOfPlanTextTenantAndView() throws Exception {
+ Properties tenantProps = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+ tenantProps.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
+ tenantProps.setProperty(TENANT_ID_ATTRIB, TENANT_ID);
+ try (Connection conn = DriverManager.getConnection(getUrl(), tenantProps))
{
+ List<String> rows = explainViaJdbc(conn, "SELECT * FROM " + MT_VIEW + "
LIMIT 1");
+ assertEquals("TENANT '" + TENANT_ID + "'", rows.get(0));
+ assertEquals("VIEW " + MT_VIEW + " OVER " + MT_BASE, rows.get(1));
+ assertTrue("operator should follow the disclosure block: " + rows.get(2),
+ rows.get(2).startsWith("CLIENT"));
+ }
+ }
+
+ /**
+ * End-to-end check that a {@code REWRITE} breadcrumb is prepended ahead of
the first operator.
+ */
+ @Test
+ public void testTopOfPlanTextRewriteAtTop() throws Exception {
+ try (Connection conn = DriverManager.getConnection(getUrl(),
defaultProps())) {
+ List<String> rows =
+ explainViaJdbc(conn, "SELECT a.a_string, b.a_string FROM atable a
RIGHT JOIN atable b"
+ + " ON a.organization_id = b.organization_id WHERE b.organization_id
= '00D000000000001'");
+ assertEquals("REWRITE RIGHT JOIN AS LEFT JOIN", rows.get(0));
+ assertTrue("operator should follow the REWRITE line: " + rows.get(1),
+ rows.get(1).startsWith("CLIENT"));
+ }
+ }
+
+ /** A view with no resolvable base table renders {@code VIEW <name>} without
{@code OVER}. */
+ @Test
+ public void testRenderTopOfPlanTextViewWithoutBase() {
+ ExplainPlanAttributes attrs = new
ExplainPlanAttributesBuilder().setViewName("V").build();
+ List<String> steps = new java.util.ArrayList<>(Arrays.asList("CLIENT
FOO"));
+ ExplainTable.renderTopOfPlanText(steps, attrs);
+ assertEquals(Arrays.asList("VIEW V", "CLIENT FOO"), steps);
+ }
+
+ /** {@link ExplainTable#renderTopOfPlanText} leaves the steps untouched. */
+ @Test
+ public void testRenderTopOfPlanTextNoopWhenNoDisclosure() {
+ ExplainPlanAttributes attrs = new ExplainPlanAttributesBuilder().build();
+ List<String> steps = new java.util.ArrayList<>(
+ Arrays.asList("CLIENT PARALLEL 1-WAY FULL SCAN OVER T", " SERVER
FILTER BY X"));
+ ExplainTable.renderTopOfPlanText(steps, attrs);
+ assertEquals(Arrays.asList("CLIENT PARALLEL 1-WAY FULL SCAN OVER T", "
SERVER FILTER BY X"),
+ steps);
+ }
+
+ private static List<String> explainViaJdbc(Connection conn, String query)
throws SQLException {
+ List<String> rows = new java.util.ArrayList<>();
+ try (java.sql.Statement stmt = conn.createStatement();
+ java.sql.ResultSet rs = stmt.executeQuery("EXPLAIN " + query)) {
+ while (rs.next()) {
+ rows.add(rs.getString(1));
+ }
+ }
+ return rows;
+ }
+
@Test
public void testTextNormalizerCollapsesWayCount() {
assertEquals(Collections.singletonList("CLIENT PARALLEL <N>-WAY FULL SCAN
OVER ATABLE"),
@@ -1135,6 +1365,12 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
*/
private static ObjectNode defaultAttrs() {
ObjectNode n = mapper.createObjectNode();
+ n.putNull("tenantId");
+ n.putNull("viewName");
+ n.putNull("viewBaseName");
+ n.putNull("cdcScopes");
+ n.putNull("txnProvider");
+ n.putNull("rewrites");
n.putNull("abstractExplainPlan");
n.putNull("splitsChunk");
n.putNull("estimatedRows");
@@ -1192,9 +1428,7 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
}
/**
- * Convenience wrapper that builds {@link #defaultAttrs()} and sets the five
fields every
- * connection backed scan emits via {@code ExplainTable.explain}: {@code
iteratorTypeAndScanSize},
- * {@code consistency}, {@code explainScanType}, {@code tableName}, and
{@code keyRanges}.
+ * Convenience wrapper that builds {@link #defaultAttrs()} for scans.
* @param scanType the {@code explainScanType} string (with its trailing
space, e.g.
* {@code "FULL SCAN "})
* @param table the {@code tableName} value
@@ -1209,9 +1443,8 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
// For a data table scan the per scan INDEX line names the same entity as
tableName. View and
// index scans that diverge override indexName on the returned node.
n.put("indexName", table);
- // A data-table scan that participated in optimizer index selection
records its decision rule.
- // A point lookup short-circuits to the point-lookup rule; any other
data-table target lands on
- // the data-table default. Index targets set their own rule on the
returned node.
+ // A data table scan that participated in optimizer index selection
records its decision rule.
+ // A point lookup short-circuits to the point lookup rule. Index targets
set their own rule.
n.put("indexRule", scanType.trim().startsWith("POINT LOOKUP") ? "point
lookup" : "data table");
if (keys != null) {
n.put("keyRanges", keys);
@@ -1233,6 +1466,15 @@ public class ExplainPlanTest extends
BaseConnectionlessQueryTest {
return arr;
}
+ /** Build a {@code rewrites} JSON array for embedding into an expected
attributes object. */
+ private static ArrayNode rewriteList(String... rewrites) {
+ ArrayNode arr = mapper.createArrayNode();
+ for (String s : rewrites) {
+ arr.add(s);
+ }
+ return arr;
+ }
+
private static ExplainPlan samplePlan(String way, String scanType) {
ExplainPlanAttributes a = new
ExplainPlanAttributesBuilder().setIteratorTypeAndScanSize(way)
.setExplainScanType(scanType).setTableName("T").setServerFirstKeyOnlyProjection(true).build();
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 e01fa9d693..cac67da165 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
@@ -19,6 +19,7 @@ package org.apache.phoenix.query.explain;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.sql.Connection;
@@ -184,7 +185,7 @@ public final class ExplainPlanTestUtil {
/**
* Assert the per-scan index kind token: {@code "LOCAL"}, {@code
"GLOBAL"}, or
- * {@code "UNCOVERED GLOBAL"} (null for a data-table target).
+ * {@code "UNCOVERED GLOBAL"}.
*/
public ExplainPlanAssert indexKind(String expected) {
assertEquals(at("indexKind"), expected, attributes.getIndexKind());
@@ -193,8 +194,7 @@ public final class ExplainPlanTestUtil {
/**
* Assert the optimizer's index-selection rule label, e.g. one of the
- * {@code OptimizerReasons.RULE_*} constants (null when the plan did not
participate in
- * optimizer index selection).
+ * {@code OptimizerReasons.RULE_*} constants.
*/
public ExplainPlanAssert indexRule(String expected) {
assertEquals(at("indexRule"), expected, attributes.getIndexRule());
@@ -203,7 +203,7 @@ public final class ExplainPlanTestUtil {
/**
* Assert the index-selection rule label starts with {@code prefix}.
Useful for the functional
- * index {@code "matches <expr>"} rule whose suffix is
expression-dependent.
+ * index {@code "matches <expr>"} rule whose suffix is expression
dependent.
*/
public ExplainPlanAssert indexRuleStartsWith(String prefix) {
String actual = attributes.getIndexRule();
@@ -586,6 +586,108 @@ public final class ExplainPlanTestUtil {
return new ExplainPlanAssert(subPlans.get(i), this, context +
".subPlan[" + i + "]");
}
+ /** Assert the tenant id disclosed at the top of the plan. */
+ public ExplainPlanAssert tenant(String expected) {
+ assertEquals(at("tenantId"), expected, attributes.getTenantId());
+ return this;
+ }
+
+ /** Assert that no tenant id disclosure was emitted. */
+ public ExplainPlanAssert tenantNone() {
+ assertNull(at("tenantId") + " expected none but was " +
attributes.getTenantId(),
+ attributes.getTenantId());
+ return this;
+ }
+
+ /** Assert the view name and its base (physical) table name disclosed at
the top of the plan. */
+ public ExplainPlanAssert view(String expectedName, String
expectedBaseName) {
+ assertEquals(at("viewName"), expectedName, attributes.getViewName());
+ assertEquals(at("viewBaseName"), expectedBaseName,
attributes.getViewBaseName());
+ return this;
+ }
+
+ /** Assert the view name disclosed at the top of the plan. */
+ public ExplainPlanAssert viewName(String expected) {
+ assertEquals(at("viewName"), expected, attributes.getViewName());
+ return this;
+ }
+
+ /** Assert that no view disclosure was emitted. */
+ public ExplainPlanAssert viewNone() {
+ assertNull(at("viewName") + " expected none but was " +
attributes.getViewName(),
+ attributes.getViewName());
+ return this;
+ }
+
+ /** Assert the CDC change scopes disclosed at the top of the plan. */
+ public ExplainPlanAssert cdcScopes(String expected) {
+ assertEquals(at("cdcScopes"), expected, attributes.getCdcScopes());
+ return this;
+ }
+
+ /** Assert that no CDC scope disclosure was emitted. */
+ public ExplainPlanAssert cdcScopesNone() {
+ assertNull(at("cdcScopes") + " expected none but was " +
attributes.getCdcScopes(),
+ attributes.getCdcScopes());
+ return this;
+ }
+
+ /** Assert the transaction provider disclosed at the top of the plan. */
+ public ExplainPlanAssert txnProvider(String expected) {
+ assertEquals(at("txnProvider"), expected, attributes.getTxnProvider());
+ return this;
+ }
+
+ /** Assert that no transaction provider disclosure was emitted. */
+ public ExplainPlanAssert txnProviderNone() {
+ assertNull(at("txnProvider") + " expected none but was " +
attributes.getTxnProvider(),
+ attributes.getTxnProvider());
+ return this;
+ }
+
+ /** Assert the number of distinct rewrite breadcrumbs disclosed at the top
of the plan. */
+ public ExplainPlanAssert rewriteCount(int expected) {
+ List<String> rewrites = attributes.getRewrites();
+ int actual = rewrites == null ? 0 : rewrites.size();
+ assertEquals(at("rewrites.size"), expected, actual);
+ return this;
+ }
+
+ /** Assert the i-th rewrite breadcrumb. */
+ public ExplainPlanAssert rewrite(int i, String expected) {
+ List<String> rewrites = attributes.getRewrites();
+ assertNotNull(at("rewrites") + " must not be null", rewrites);
+ assertTrue(at("rewrites") + " has no index " + i + " (size=" +
rewrites.size() + ")",
+ i >= 0 && i < rewrites.size());
+ assertEquals(at("rewrites[" + i + "]"), expected, rewrites.get(i));
+ return this;
+ }
+
+ /** Assert the entire ordered rewrite breadcrumb list matches {@code
expected}. */
+ public ExplainPlanAssert rewrites(String... expected) {
+ List<String> actual = attributes.getRewrites();
+ List<String> actualOrEmpty = actual == null ? Collections.<String>
emptyList() : actual;
+ assertEquals(at("rewrites"), Arrays.asList(expected), actualOrEmpty);
+ return this;
+ }
+
+ /** Assert the rewrite breadcrumb list contains {@code expected}. */
+ public ExplainPlanAssert rewriteContains(String expected) {
+ List<String> rewrites = attributes.getRewrites();
+ assertNotNull(at("rewrites") + " must not be null", rewrites);
+ assertTrue(at("rewrites") + " expected to contain '" + expected + "' but
was " + rewrites,
+ rewrites.contains(expected));
+ return this;
+ }
+
+ /** Assert that no rewrite breadcrumbs were disclosed (null or empty). */
+ public ExplainPlanAssert rewritesNone() {
+ List<String> rewrites = attributes.getRewrites();
+ assertTrue(at("rewrites") + " expected none but was " + rewrites,
+ rewrites == null || rewrites.isEmpty());
+ return this;
+ }
+
/**
* Return to the parent assertion after navigating into {@link #rhs()} or
{@link #subPlan(int)}.
*/