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,


Reply via email to