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 <[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,