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