You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@phoenix.apache.org by ap...@apache.org on 2026/06/12 21:13:15 UTC
(phoenix) branch PHOENIX-7876-feature updated: PHOENIX-7916 Disclose atomic upsert flavors and RETURNING in EXPLAIN (#2524)
This is an automated email from the ASF dual-hosted git repository.
apurtell pushed a commit to branch PHOENIX-7876-feature
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/PHOENIX-7876-feature by this push:
new c21e89616e PHOENIX-7916 Disclose atomic upsert flavors and RETURNING in EXPLAIN (#2524)
c21e89616e is described below
commit c21e89616e0f21eec6140415bf204d8111988db8
Author: Andrew Purtell <ap...@apache.org>
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] <no...@anthropic.com>
---
.../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,