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 c1bd129ac04bcad528976cd86c8dec62ff4c9770 Author: Andrew Purtell <[email protected]> AuthorDate: Fri Jun 12 14:13:09 2026 -0700 PHOENIX-7916 Disclose atomic upsert flavors and RETURNING in EXPLAIN (#2524) Co-authored-by: Claude Opus 4.8[1m] <[email protected]> --- .../org/apache/phoenix/compile/DeleteCompiler.java | 41 +++++-- .../phoenix/compile/ExplainPlanAttributes.java | 106 +++++++++++++----- .../org/apache/phoenix/compile/UpsertCompiler.java | 82 +++++++++++--- .../phoenix/query/explain/ExplainPlanTest.java | 121 +++++++++++++++++++++ .../phoenix/query/explain/ExplainPlanTestUtil.java | 38 +++++++ 5 files changed, 338 insertions(+), 50 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 5acc76a12e..0ca150ef21 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 @@ -673,7 +673,8 @@ public class DeleteCompiler { // from the data table, while the others will be for deleting rows from immutable indexes. List<MutationPlan> mutationPlans = Lists.newArrayListWithExpectedSize(queryPlans.size()); for (final QueryPlan plan : queryPlans) { - mutationPlans.add(new SingleRowDeleteMutationPlan(plan, connection, maxSize, maxSizeBytes)); + mutationPlans.add(new SingleRowDeleteMutationPlan(plan, connection, maxSize, maxSizeBytes, + delete.isReturningRow())); } return new MultiRowDeleteMutationPlan(dataPlan, mutationPlans); } else if (runOnServer) { @@ -706,7 +707,7 @@ public class DeleteCompiler { new AggregatePlan(context, select, dataPlan.getTableRef(), projector, null, null, OrderBy.EMPTY_ORDER_BY, null, GroupBy.EMPTY_GROUP_BY, null, dataPlan); return new ServerSelectDeleteMutationPlan(dataPlan, connection, aggPlan, projector, maxSize, - maxSizeBytes); + maxSizeBytes, delete.isReturningRow()); } else { final DeletingParallelIteratorFactory parallelIteratorFactory = parallelIteratorFactoryToBe; List<PColumn> adjustedProjectedColumns = @@ -751,7 +752,7 @@ public class DeleteCompiler { } return new ClientSelectDeleteMutationPlan(targetTableRef, dataPlan, bestPlan, hasPreOrPostProcessing, parallelIteratorFactory, otherTableRefs, projectedTableRef, maxSize, - maxSizeBytes, connection); + maxSizeBytes, connection, delete.isReturningRow()); } } @@ -765,14 +766,16 @@ public class DeleteCompiler { private final int maxSize; private final StatementContext context; private final long maxSizeBytes; + private final boolean returningRow; public SingleRowDeleteMutationPlan(QueryPlan dataPlan, PhoenixConnection connection, - int maxSize, long maxSizeBytes) { + int maxSize, long maxSizeBytes, boolean returningRow) { this.dataPlan = dataPlan; this.connection = connection; this.maxSize = maxSize; this.context = dataPlan.getContext(); this.maxSizeBytes = maxSizeBytes; + this.returningRow = returningRow; } @Override @@ -801,11 +804,17 @@ public class DeleteCompiler { public ExplainPlan getExplainPlan() throws SQLException { ExplainPlanAttributesBuilder builder = new ExplainPlanAttributesBuilder().setAbstractExplainPlan("DELETE SINGLE ROW"); + builder.setReturningRow(returningRow); if (getContext().isRoot()) { ExplainTable.populateTopOfPlanAttributes(builder, getContext(), getTargetRef()); ExplainTable.populateTopOfPlanEstimates(builder, this); } - return new ExplainPlan(Collections.singletonList("DELETE SINGLE ROW"), builder.build()); + List<String> planSteps = Lists.newArrayListWithExpectedSize(2); + planSteps.add("DELETE SINGLE ROW"); + if (returningRow) { + planSteps.add(" RETURNING *"); + } + return new ExplainPlan(planSteps, builder.build()); } @Override @@ -864,9 +873,11 @@ public class DeleteCompiler { private final RowProjector projector; private final int maxSize; private final long maxSizeBytes; + private final boolean returningRow; public ServerSelectDeleteMutationPlan(QueryPlan dataPlan, PhoenixConnection connection, - QueryPlan aggPlan, RowProjector projector, int maxSize, long maxSizeBytes) { + QueryPlan aggPlan, RowProjector projector, int maxSize, long maxSizeBytes, + boolean returningRow) { this.context = dataPlan.getContext(); this.dataPlan = dataPlan; this.connection = connection; @@ -874,6 +885,7 @@ public class DeleteCompiler { this.projector = projector; this.maxSize = maxSize; this.maxSizeBytes = maxSizeBytes; + this.returningRow = returningRow; } @Override @@ -982,11 +994,15 @@ public class DeleteCompiler { ExplainPlan explainPlan = aggPlan.getExplainPlan(); List<String> queryPlanSteps = explainPlan.getPlanSteps(); ExplainPlanAttributes explainPlanAttributes = explainPlan.getPlanStepsAsAttributes(); - List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 1); + List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 2); ExplainPlanAttributesBuilder newBuilder = new ExplainPlanAttributesBuilder(explainPlanAttributes); newBuilder.setAbstractExplainPlan("DELETE ROWS SERVER SELECT"); + newBuilder.setReturningRow(returningRow); planSteps.add("DELETE ROWS SERVER SELECT"); + if (returningRow) { + planSteps.add(" RETURNING *"); + } planSteps.addAll(queryPlanSteps); if (getContext().isRoot()) { ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(), getTargetRef()); @@ -1032,11 +1048,13 @@ public class DeleteCompiler { private final int maxSize; private final long maxSizeBytes; private final PhoenixConnection connection; + private final boolean returningRow; public ClientSelectDeleteMutationPlan(TableRef targetTableRef, QueryPlan dataPlan, QueryPlan bestPlan, boolean hasPreOrPostProcessing, DeletingParallelIteratorFactory parallelIteratorFactory, List<TableRef> otherTableRefs, - TableRef projectedTableRef, int maxSize, long maxSizeBytes, PhoenixConnection connection) { + TableRef projectedTableRef, int maxSize, long maxSizeBytes, PhoenixConnection connection, + boolean returningRow) { this.context = bestPlan.getContext(); this.targetTableRef = targetTableRef; this.dataPlan = dataPlan; @@ -1048,6 +1066,7 @@ public class DeleteCompiler { this.maxSize = maxSize; this.maxSizeBytes = maxSizeBytes; this.connection = connection; + this.returningRow = returningRow; } @Override @@ -1121,11 +1140,15 @@ public class DeleteCompiler { ExplainPlan explainPlan = bestPlan.getExplainPlan(); List<String> queryPlanSteps = explainPlan.getPlanSteps(); ExplainPlanAttributes explainPlanAttributes = explainPlan.getPlanStepsAsAttributes(); - List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 1); + List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 2); ExplainPlanAttributesBuilder newBuilder = new ExplainPlanAttributesBuilder(explainPlanAttributes); newBuilder.setAbstractExplainPlan("DELETE ROWS CLIENT SELECT"); + newBuilder.setReturningRow(returningRow); planSteps.add("DELETE ROWS CLIENT SELECT"); + if (returningRow) { + planSteps.add(" RETURNING *"); + } planSteps.addAll(queryPlanSteps); if (getContext().isRoot()) { ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(), getTargetRef()); 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 45768f6f73..6a6f7c0ea8 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 @@ -30,6 +30,7 @@ import org.apache.hadoop.hbase.client.Consistency; import org.apache.phoenix.optimize.RejectedIndexEntry; import org.apache.phoenix.parse.HintNode; import org.apache.phoenix.parse.HintNode.Hint; +import org.apache.phoenix.parse.UpsertStatement.OnDuplicateKeyType; import org.apache.phoenix.schema.PColumn; /** @@ -38,19 +39,20 @@ import org.apache.phoenix.schema.PColumn; * Strings containing entire plan. */ @JsonPropertyOrder({ "tenantId", "viewName", "viewBaseName", "cdcScopes", "txnProvider", "rewrites", - "estimatedRows", "estimatedSizeInBytes", "estimateInfoTs", "abstractExplainPlan", "hint", - "explainScanType", "consistency", "tableName", "keyRanges", "indexName", "indexKind", "indexRule", - "indexRejected", "saltBuckets", "regionsPlanned", "scanTimeRangeMin", "scanTimeRangeMax", - "splitsChunk", "useRoundRobinIterator", "samplingRate", "hexStringRVCOffset", - "iteratorTypeAndScanSize", "scanEstimatedRows", "scanEstimatedSizeInBytes", "serverWhereFilter", - "serverDistinctFilter", "serverMergeColumns", "serverParsedProjections", - "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" }) + "estimatedRows", "estimatedSizeInBytes", "estimateInfoTs", "abstractExplainPlan", + "onDuplicateKeyAction", "serverUpdateSet", "returningRow", "hint", "explainScanType", + "consistency", "tableName", "keyRanges", "indexName", "indexKind", "indexRule", "indexRejected", + "saltBuckets", "regionsPlanned", "scanTimeRangeMin", "scanTimeRangeMax", "splitsChunk", + "useRoundRobinIterator", "samplingRate", "hexStringRVCOffset", "iteratorTypeAndScanSize", + "scanEstimatedRows", "scanEstimatedSizeInBytes", "serverWhereFilter", "serverDistinctFilter", + "serverMergeColumns", "serverParsedProjections", "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) @@ -67,6 +69,10 @@ public class ExplainPlanAttributes { // Plan identity and scan-level metadata private final String abstractExplainPlan; + // Mutation-operator detail (populated only on mutation plans). + private final OnDuplicateKeyType onDuplicateKeyAction; + private final List<String> serverUpdateSet; + private final boolean returningRow; private final Hint hint; private final String explainScanType; private final Consistency consistency; @@ -144,6 +150,9 @@ public class ExplainPlanAttributes { this.estimatedSizeInBytes = null; this.estimateInfoTs = null; this.abstractExplainPlan = null; + this.onDuplicateKeyAction = null; + this.serverUpdateSet = null; + this.returningRow = false; this.hint = null; this.explainScanType = null; this.consistency = null; @@ -200,8 +209,9 @@ public class ExplainPlanAttributes { public ExplainPlanAttributes(String tenantId, String viewName, String viewBaseName, String cdcScopes, String txnProvider, List<String> rewrites, Long estimatedRows, - Long estimatedSizeInBytes, Long estimateInfoTs, String abstractExplainPlan, Hint hint, - String explainScanType, Consistency consistency, String tableName, String keyRanges, + Long estimatedSizeInBytes, Long estimateInfoTs, String abstractExplainPlan, + OnDuplicateKeyType onDuplicateKeyAction, List<String> serverUpdateSet, boolean returningRow, + 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, @@ -230,6 +240,11 @@ public class ExplainPlanAttributes { this.estimatedSizeInBytes = estimatedSizeInBytes; this.estimateInfoTs = estimateInfoTs; this.abstractExplainPlan = abstractExplainPlan; + this.onDuplicateKeyAction = onDuplicateKeyAction; + this.serverUpdateSet = (serverUpdateSet == null || serverUpdateSet.isEmpty()) + ? null + : Collections.unmodifiableList(new ArrayList<>(serverUpdateSet)); + this.returningRow = returningRow; this.hint = hint; this.explainScanType = explainScanType; this.consistency = consistency; @@ -316,6 +331,18 @@ public class ExplainPlanAttributes { return abstractExplainPlan; } + public OnDuplicateKeyType getOnDuplicateKeyAction() { + return onDuplicateKeyAction; + } + + public List<String> getServerUpdateSet() { + return serverUpdateSet; + } + + public boolean isReturningRow() { + return returningRow; + } + public Hint getHint() { return hint; } @@ -565,6 +592,9 @@ public class ExplainPlanAttributes { private Long estimatedSizeInBytes; private Long estimateInfoTs; private String abstractExplainPlan; + private OnDuplicateKeyType onDuplicateKeyAction; + private List<String> serverUpdateSet; + private boolean returningRow; private HintNode.Hint hint; private String explainScanType; private Consistency consistency; @@ -634,6 +664,11 @@ public class ExplainPlanAttributes { this.estimatedSizeInBytes = explainPlanAttributes.getEstimatedSizeInBytes(); this.estimateInfoTs = explainPlanAttributes.getEstimateInfoTs(); this.abstractExplainPlan = explainPlanAttributes.getAbstractExplainPlan(); + this.onDuplicateKeyAction = explainPlanAttributes.getOnDuplicateKeyAction(); + List<String> srcServerUpdateSet = explainPlanAttributes.getServerUpdateSet(); + this.serverUpdateSet = + srcServerUpdateSet == null ? null : new ArrayList<>(srcServerUpdateSet); + this.returningRow = explainPlanAttributes.isReturningRow(); this.hint = explainPlanAttributes.getHint(); this.explainScanType = explainPlanAttributes.getExplainScanType(); this.consistency = explainPlanAttributes.getConsistency(); @@ -752,6 +787,22 @@ public class ExplainPlanAttributes { return this; } + public ExplainPlanAttributesBuilder + setOnDuplicateKeyAction(OnDuplicateKeyType onDuplicateKeyAction) { + this.onDuplicateKeyAction = onDuplicateKeyAction; + return this; + } + + public ExplainPlanAttributesBuilder setServerUpdateSet(List<String> serverUpdateSet) { + this.serverUpdateSet = serverUpdateSet == null ? null : new ArrayList<>(serverUpdateSet); + return this; + } + + public ExplainPlanAttributesBuilder setReturningRow(boolean returningRow) { + this.returningRow = returningRow; + return this; + } + public ExplainPlanAttributesBuilder setHint(HintNode.Hint hint) { this.hint = hint; return this; @@ -1039,18 +1090,19 @@ public class ExplainPlanAttributes { public ExplainPlanAttributes build() { return new ExplainPlanAttributes(tenantId, viewName, viewBaseName, cdcScopes, txnProvider, - rewrites, estimatedRows, estimatedSizeInBytes, estimateInfoTs, abstractExplainPlan, hint, - explainScanType, consistency, tableName, keyRanges, indexName, indexKind, indexRule, - indexRejected, saltBuckets, regionsPlanned, scanTimeRangeMin, scanTimeRangeMax, splitsChunk, - useRoundRobinIterator, samplingRate, hexStringRVCOffset, iteratorTypeAndScanSize, - scanEstimatedRows, scanEstimatedSizeInBytes, serverWhereFilter, serverDistinctFilter, - serverMergeColumns, serverParsedProjections, 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); + rewrites, estimatedRows, estimatedSizeInBytes, estimateInfoTs, abstractExplainPlan, + onDuplicateKeyAction, serverUpdateSet, returningRow, hint, explainScanType, consistency, + tableName, keyRanges, indexName, indexKind, indexRule, indexRejected, saltBuckets, + regionsPlanned, scanTimeRangeMin, scanTimeRangeMax, splitsChunk, useRoundRobinIterator, + samplingRate, hexStringRVCOffset, iteratorTypeAndScanSize, scanEstimatedRows, + scanEstimatedSizeInBytes, serverWhereFilter, serverDistinctFilter, serverMergeColumns, + serverParsedProjections, 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/UpsertCompiler.java b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/UpsertCompiler.java index f5838a936e..25b182b546 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 @@ -850,7 +850,8 @@ public class UpsertCompiler { statementContext.getCurrentTable(), aggProjector, null, null, OrderBy.EMPTY_ORDER_BY, null, GroupBy.EMPTY_GROUP_BY, null, originalQueryPlan); return new ServerUpsertSelectMutationPlan(queryPlan, tableRef, originalQueryPlan, context, - connection, scan, aggPlan, aggProjector, maxSize, maxSizeBytes); + connection, scan, aggPlan, aggProjector, upsert.isReturningRow(), maxSize, + maxSizeBytes); } } //////////////////////////////////////////////////////////////////// @@ -858,7 +859,7 @@ public class UpsertCompiler { ///////////////////////////////////////////////////////////////////// return new ClientUpsertSelectMutationPlan(queryPlan, tableRef, originalQueryPlan, parallelIteratorFactory, projector, columnIndexes, pkSlotIndexes, useServerTimestamp, - maxSize, maxSizeBytes); + upsert.isReturningRow(), maxSize, maxSizeBytes); } //////////////////////////////////////////////////////////////////// @@ -971,7 +972,8 @@ public class UpsertCompiler { return new UpsertValuesMutationPlan(context, tableRef, nodeIndexOffset, constantExpressionsList, allColumns, columnIndexes, overlapViewColumns, valuesList, addViewColumns, connection, - pkSlotIndexes, useServerTimestamp, onDupKeyBytes, onDupKeyType, maxSize, maxSizeBytes); + pkSlotIndexes, useServerTimestamp, onDupKeyBytes, onDupKeyType, upsert.getOnDupKeyPairs(), + upsert.isReturningRow(), maxSize, maxSizeBytes); } private static byte[] getOnDuplicateKeyBytes(PTable table, StatementContext context, @@ -1156,12 +1158,14 @@ public class UpsertCompiler { private final Scan scan; private final QueryPlan aggPlan; private final RowProjector aggProjector; + private final boolean returningRow; private final int maxSize; private final long maxSizeBytes; public ServerUpsertSelectMutationPlan(QueryPlan queryPlan, TableRef tableRef, QueryPlan originalQueryPlan, StatementContext context, PhoenixConnection connection, - Scan scan, QueryPlan aggPlan, RowProjector aggProjector, int maxSize, long maxSizeBytes) { + Scan scan, QueryPlan aggPlan, RowProjector aggProjector, boolean returningRow, int maxSize, + long maxSizeBytes) { this.queryPlan = queryPlan; this.tableRef = tableRef; this.originalQueryPlan = originalQueryPlan; @@ -1170,6 +1174,7 @@ public class UpsertCompiler { this.scan = scan; this.aggPlan = aggPlan; this.aggProjector = aggProjector; + this.returningRow = returningRow; this.maxSize = maxSize; this.maxSizeBytes = maxSizeBytes; } @@ -1248,11 +1253,15 @@ public class UpsertCompiler { ExplainPlan explainPlan = aggPlan.getExplainPlan(); List<String> queryPlanSteps = explainPlan.getPlanSteps(); ExplainPlanAttributes explainPlanAttributes = explainPlan.getPlanStepsAsAttributes(); - List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 1); + List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 2); ExplainPlanAttributesBuilder newBuilder = new ExplainPlanAttributesBuilder(explainPlanAttributes); newBuilder.setAbstractExplainPlan("UPSERT ROWS"); + newBuilder.setReturningRow(returningRow); planSteps.add("UPSERT ROWS"); + if (returningRow) { + planSteps.add(" RETURNING *"); + } planSteps.addAll(queryPlanSteps); if (getContext().isRoot()) { ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(), getTargetRef()); @@ -1292,6 +1301,8 @@ public class UpsertCompiler { private final boolean useServerTimestamp; private final byte[] onDupKeyBytes; private final OnDuplicateKeyType onDupKeyType; + private final List<Pair<ColumnName, ParseNode>> onDupKeyPairs; + private final boolean returningRow; private final int maxSize; private final long maxSizeBytes; @@ -1300,7 +1311,8 @@ public class UpsertCompiler { int[] columnIndexes, Set<PColumn> overlapViewColumns, List<byte[][]> valuesList, Set<PColumn> addViewColumns, PhoenixConnection connection, int[] pkSlotIndexes, boolean useServerTimestamp, byte[] onDupKeyBytes, OnDuplicateKeyType onDupKeyType, - int maxSize, long maxSizeBytes) { + List<Pair<ColumnName, ParseNode>> onDupKeyPairs, boolean returningRow, int maxSize, + long maxSizeBytes) { this.context = context; this.tableRef = tableRef; this.nodeIndexOffset = nodeIndexOffset; @@ -1315,6 +1327,8 @@ public class UpsertCompiler { this.useServerTimestamp = useServerTimestamp; this.onDupKeyBytes = onDupKeyBytes; this.onDupKeyType = onDupKeyType; + this.onDupKeyPairs = onDupKeyPairs; + this.returningRow = returningRow; this.maxSize = maxSize; this.maxSizeBytes = maxSizeBytes; } @@ -1436,20 +1450,54 @@ public class UpsertCompiler { @Override public ExplainPlan getExplainPlan() throws SQLException { - List<String> planSteps = Lists.newArrayListWithExpectedSize(2); + List<String> planSteps = Lists.newArrayListWithExpectedSize(4); if (context.getSequenceManager().getSequenceCount() > 0) { planSteps .add("CLIENT RESERVE " + context.getSequenceManager().getSequenceCount() + " SEQUENCES"); } - planSteps.add("PUT SINGLE ROW"); + String header; + switch (onDupKeyType) { + case NONE: + header = "PUT SINGLE ROW"; + break; + case UPDATE: + header = "PUT SINGLE ROW ON DUPLICATE KEY UPDATE"; + break; + case UPDATE_ONLY: + header = "PUT SINGLE ROW ON DUPLICATE KEY UPDATE_ONLY"; + break; + case IGNORE: + header = "PUT SINGLE ROW ON DUPLICATE KEY IGNORE"; + break; + default: + throw new IllegalStateException("Unhandled OnDuplicateKeyType: " + onDupKeyType); + } + planSteps.add(header); + List<String> serverUpdateSet = null; + if ( + onDupKeyType == OnDuplicateKeyType.UPDATE && onDupKeyPairs != null + && !onDupKeyPairs.isEmpty() + ) { + serverUpdateSet = Lists.newArrayListWithExpectedSize(onDupKeyPairs.size()); + for (Pair<ColumnName, ParseNode> pair : onDupKeyPairs) { + serverUpdateSet.add(pair.getFirst().toString() + " = " + pair.getSecond().toString()); + } + planSteps.add(" SERVER UPDATE SET " + String.join(", ", serverUpdateSet)); + } + if (returningRow) { + planSteps.add(" RETURNING *"); + } + ExplainPlanAttributesBuilder builder = + new ExplainPlanAttributesBuilder(ExplainPlanAttributes.getDefaultExplainPlan()); + builder + .setOnDuplicateKeyAction(onDupKeyType == OnDuplicateKeyType.NONE ? null : onDupKeyType); + builder.setServerUpdateSet(serverUpdateSet); + builder.setReturningRow(returningRow); if (getContext().isRoot()) { - ExplainPlanAttributesBuilder builder = - new ExplainPlanAttributesBuilder(ExplainPlanAttributes.getDefaultExplainPlan()); ExplainTable.populateTopOfPlanAttributes(builder, getContext(), getTargetRef()); ExplainTable.populateTopOfPlanEstimates(builder, this); - return new ExplainPlan(planSteps, builder.build()); } - return new ExplainPlan(planSteps); + return new ExplainPlan(planSteps, builder.build()); } @Override @@ -1477,13 +1525,14 @@ public class UpsertCompiler { private final int[] columnIndexes; private final int[] pkSlotIndexes; private final boolean useServerTimestamp; + private final boolean returningRow; private final int maxSize; private final long maxSizeBytes; public ClientUpsertSelectMutationPlan(QueryPlan queryPlan, TableRef tableRef, QueryPlan originalQueryPlan, UpsertingParallelIteratorFactory parallelIteratorFactory, RowProjector projector, int[] columnIndexes, int[] pkSlotIndexes, boolean useServerTimestamp, - int maxSize, long maxSizeBytes) { + boolean returningRow, int maxSize, long maxSizeBytes) { this.queryPlan = queryPlan; this.tableRef = tableRef; this.originalQueryPlan = originalQueryPlan; @@ -1492,6 +1541,7 @@ public class UpsertCompiler { this.columnIndexes = columnIndexes; this.pkSlotIndexes = pkSlotIndexes; this.useServerTimestamp = useServerTimestamp; + this.returningRow = returningRow; this.maxSize = maxSize; this.maxSizeBytes = maxSizeBytes; queryPlan.getContext().setClientSideUpsertSelect(true); @@ -1567,11 +1617,15 @@ public class UpsertCompiler { ExplainPlan explainPlan = queryPlan.getExplainPlan(); List<String> queryPlanSteps = explainPlan.getPlanSteps(); ExplainPlanAttributes explainPlanAttributes = explainPlan.getPlanStepsAsAttributes(); - List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 1); + List<String> planSteps = Lists.newArrayListWithExpectedSize(queryPlanSteps.size() + 2); ExplainPlanAttributesBuilder newBuilder = new ExplainPlanAttributesBuilder(explainPlanAttributes); newBuilder.setAbstractExplainPlan("UPSERT SELECT"); + newBuilder.setReturningRow(returningRow); planSteps.add("UPSERT SELECT"); + if (returningRow) { + planSteps.add(" RETURNING *"); + } planSteps.addAll(queryPlanSteps); if (getContext().isRoot()) { ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(), getTargetRef()); 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 99ad4f17e7..9e510fd9e9 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 @@ -728,6 +728,108 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { .put("serverWhereFilter", "SERVER FILTER BY ENTITY_ID = 'abc'")); } + @Test + public void testPutSingleRowOnDuplicateKeyUpdate() throws Exception { + verifyMutation("putSingleRowOnDuplicateKeyUpdate", + "UPSERT INTO atable (organization_id, entity_id, a_integer)" + + " VALUES ('00D000000000001','00E00000000001',1)" + + " ON DUPLICATE KEY UPDATE a_integer = a_integer + 1", + false, + text("PUT SINGLE ROW ON DUPLICATE KEY UPDATE", + " SERVER UPDATE SET A_INTEGER = (A_INTEGER + 1)"), + onDupKeyAttrs("UPDATE", "A_INTEGER = (A_INTEGER + 1)")); + } + + @Test + public void testPutSingleRowOnDuplicateKeyUpdateOnly() throws Exception { + verifyMutation("putSingleRowOnDuplicateKeyUpdateOnly", + "UPSERT INTO atable (organization_id, entity_id, a_integer)" + + " VALUES ('00D000000000001','00E00000000001',1)" + + " ON DUPLICATE KEY UPDATE_ONLY a_integer = a_integer + 1", + false, text("PUT SINGLE ROW ON DUPLICATE KEY UPDATE_ONLY"), + defaultAttrs().put("onDuplicateKeyAction", "UPDATE_ONLY")); + } + + @Test + public void testPutSingleRowOnDuplicateKeyIgnore() throws Exception { + verifyMutation("putSingleRowOnDuplicateKeyIgnore", + "UPSERT INTO atable (organization_id, entity_id, a_integer)" + + " VALUES ('00D000000000001','00E00000000001',1) ON DUPLICATE KEY IGNORE", + false, text("PUT SINGLE ROW ON DUPLICATE KEY IGNORE"), + defaultAttrs().put("onDuplicateKeyAction", "IGNORE")); + } + + @Test + public void testPutSingleRowReturning() throws Exception { + verifyMutation("putSingleRowReturning", + "UPSERT INTO atable (organization_id, entity_id, a_string)" + + " VALUES ('00D000000000001','00E00000000001','x') RETURNING *", + false, text("PUT SINGLE ROW", " RETURNING *"), defaultAttrs().put("returningRow", true)); + } + + @Test + public void testPutSingleRowOnDuplicateKeyUpdateReturning() throws Exception { + verifyMutation("putSingleRowOnDuplicateKeyUpdateReturning", + "UPSERT INTO atable (organization_id, entity_id, a_integer)" + + " VALUES ('00D000000000001','00E00000000001',1)" + + " ON DUPLICATE KEY UPDATE a_integer = a_integer + 1 RETURNING *", + false, + text("PUT SINGLE ROW ON DUPLICATE KEY UPDATE", + " SERVER UPDATE SET A_INTEGER = (A_INTEGER + 1)", " RETURNING *"), + onDupKeyAttrs("UPDATE", "A_INTEGER = (A_INTEGER + 1)").put("returningRow", true)); + } + + @Test + public void testUpsertSelectClientReturning() throws Exception { + verifyMutation("upsertSelectClientReturning", + "UPSERT INTO atable (organization_id, entity_id, a_string)" + + " SELECT organization_id, entity_id, a_string FROM atable" + + " WHERE organization_id = '00D000000000001' RETURNING *", + false, + text("UPSERT SELECT", " RETURNING *", + "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE ['00D000000000001']", " INDEX ATABLE", + " REGIONS PLANNED <N>"), + scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']") + .put("abstractExplainPlan", "UPSERT SELECT").put("returningRow", true)); + } + + @Test + public void testUpsertSelectServerReturning() throws Exception { + verifyMutation("upsertSelectServerReturning", + "UPSERT INTO atable (organization_id, entity_id, a_string)" + + " SELECT organization_id, entity_id, a_string FROM atable" + + " WHERE organization_id = '00D000000000001' RETURNING *", + true, + text("UPSERT ROWS", " RETURNING *", + "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE ['00D000000000001']", " INDEX ATABLE", + " REGIONS PLANNED <N>"), + scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']") + .put("abstractExplainPlan", "UPSERT ROWS").putNull("indexRule").put("returningRow", true)); + } + + @Test + public void testDeleteSingleRowReturning() throws Exception { + verifyMutation("deleteSingleRowReturning", + "DELETE FROM atable WHERE organization_id = '00D000000000001'" + + " AND entity_id = '00E00000000001' RETURNING *", + true, text("DELETE SINGLE ROW", " RETURNING *"), + defaultAttrs().put("abstractExplainPlan", "DELETE SINGLE ROW").put("returningRow", true)); + } + + @Test + public void testDeleteServerReturning() throws Exception { + verifyMutation("deleteServerReturning", + "DELETE FROM atable WHERE entity_id = 'abc' RETURNING *", true, + text("DELETE ROWS SERVER SELECT", " RETURNING *", + "CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", " INDEX ATABLE", + " REGIONS PLANNED <N>", " SERVER PROJECTION FILTER BY FIRST KEY ONLY", + " SERVER FILTER BY ENTITY_ID = 'abc'"), + scanAttrs("FULL SCAN ", "ATABLE", "").put("abstractExplainPlan", "DELETE ROWS SERVER SELECT") + .put("serverFirstKeyOnlyProjection", true) + .put("serverWhereFilter", "SERVER FILTER BY ENTITY_ID = 'abc'").putNull("indexRule") + .put("returningRow", true)); + } + @Test public void testSequenceNextValue() throws Exception { verifyQuery("sequenceNextValue", "SELECT NEXT VALUE FOR " + SEQ + " FROM atable", @@ -1414,6 +1516,9 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { n.putNull("estimatedSizeInBytes"); n.putNull("estimateInfoTs"); n.putNull("abstractExplainPlan"); + n.putNull("onDuplicateKeyAction"); + n.putNull("serverUpdateSet"); + n.put("returningRow", false); n.putNull("splitsChunk"); n.putNull("scanEstimatedRows"); n.putNull("scanEstimatedSizeInBytes"); @@ -1499,6 +1604,22 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { return defaultAttrs(); } + /** + * Convenience wrapper that builds {@link #defaultAttrs()} for an atomic UPSERT operator, + * populating {@code onDuplicateKeyAction} and the {@code serverUpdateSet} assignment array. + * @param action the {@code OnDuplicateKeyType} name (e.g. {@code "UPDATE"}) + * @param assignments the ordered {@code <col> = <expr>} renderings + */ + private static ObjectNode onDupKeyAttrs(String action, String... assignments) { + ObjectNode n = defaultAttrs(); + n.put("onDuplicateKeyAction", action); + ArrayNode arr = n.putArray("serverUpdateSet"); + for (String a : assignments) { + arr.add(a); + } + return n; + } + /** Build a {@code clientSteps} JSON array for embedding into an expected attributes object. */ private static ArrayNode clientSteps(String... steps) { ArrayNode arr = mapper.createArrayNode(); 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 fffc655160..f85b616dad 100644 --- a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java +++ b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainPlanTestUtil.java @@ -33,6 +33,7 @@ import org.apache.phoenix.compile.ExplainPlanAttributes; import org.apache.phoenix.compile.QueryPlan; import org.apache.phoenix.jdbc.PhoenixPreparedStatement; import org.apache.phoenix.optimize.RejectedIndexEntry; +import org.apache.phoenix.parse.UpsertStatement.OnDuplicateKeyType; /** * Test helpers for retrieving the {@link ExplainPlan} and its structured @@ -287,6 +288,43 @@ public final class ExplainPlanTestUtil { return this; } + /** + * Assert the {@code ON DUPLICATE KEY} flavor disclosed on an atomic UPSERT mutation operator. + */ + public ExplainPlanAssert onDuplicateKeyAction(OnDuplicateKeyType expected) { + assertEquals(at("onDuplicateKeyAction"), expected, attributes.getOnDuplicateKeyAction()); + return this; + } + + /** + * Assert the ordered {@code <col> = <expr>} assignments disclosed under + * {@code ON DUPLICATE KEY UPDATE}. + */ + public ExplainPlanAssert serverUpdateSet(String... expected) { + assertEquals(at("serverUpdateSet"), Arrays.asList(expected), attributes.getServerUpdateSet()); + return this; + } + + /** Assert the number of {@code SERVER UPDATE SET} assignments. */ + public ExplainPlanAssert serverUpdateSetCount(int expected) { + List<String> actual = attributes.getServerUpdateSet(); + int actualCount = actual == null ? 0 : actual.size(); + assertEquals(at("serverUpdateSet.size"), expected, actualCount); + return this; + } + + /** Assert the mutation operator discloses {@code RETURNING *}. */ + public ExplainPlanAssert returningRow() { + assertTrue(at("returningRow") + " expected true", attributes.isReturningRow()); + return this; + } + + /** Assert the mutation operator does not disclose {@code RETURNING *}. */ + public ExplainPlanAssert noReturningRow() { + assertTrue(at("returningRow") + " expected false", !attributes.isReturningRow()); + return this; + } + /** Assert the read consistency level. */ public ExplainPlanAssert consistency(String expected) { assertEquals(at("consistency"), expected,
