This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch PHOENIX-7876-feature in repository https://gitbox.apache.org/repos/asf/phoenix.git
commit 8a510e9275388859d692816c10aff8c89ff53745 Author: Andrew Purtell <[email protected]> AuthorDate: Wed Jun 24 18:56:55 2026 -0700 PHOENIX-7936 EXPLAIN the user's projection on server-driven mutation inner plans (#2545) Co-authored-by: Claude Opus 4.8[1m] <[email protected]> --- .../org/apache/phoenix/compile/DeleteCompiler.java | 4 ++ .../org/apache/phoenix/compile/UpsertCompiler.java | 3 + .../org/apache/phoenix/iterate/ExplainTable.java | 64 ++++++++++++++++++++-- 3 files changed, 65 insertions(+), 6 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 a727a0280d..e73ad41dbe 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 @@ -1002,6 +1002,10 @@ public class DeleteCompiler { planSteps.add(" RETURNING *"); } planSteps.addAll(queryPlanSteps); + // Surface the row-identity projection the scan actually reads so VERBOSE explain describes + // the delete rather than the count. + ExplainTable.overrideMutationProject(planSteps, explainPlanAttributes, newBuilder, + dataPlan.getProjector()); if (getContext().isRoot()) { ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(), getTargetRef()); ExplainTable.populateTopOfPlanEstimates(newBuilder, this); 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 fa06c95a82..ad750d0e07 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 @@ -1261,6 +1261,9 @@ public class UpsertCompiler { planSteps.add(" RETURNING *"); } planSteps.addAll(queryPlanSteps); + // Surface the user's SELECT projection instead so VERBOSE explain describes the upsert. + ExplainTable.overrideMutationProject(planSteps, explainPlanAttributes, newBuilder, + queryPlan.getProjector()); if (getContext().isRoot()) { ExplainTable.populateTopOfPlanAttributes(newBuilder, getContext(), getTargetRef()); ExplainTable.populateTopOfPlanEstimates(newBuilder, this); diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java index 00e2564ab0..88ccead7f3 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/ExplainTable.java @@ -604,13 +604,30 @@ public abstract class ExplainTable { if (!verbose) { return; } - RowProjector projector = getProjector(); - if (projector == null) { + List<String> columns = projectedColumnNames(getProjector()); + if (columns == null) { return; } + planSteps.add(" PROJECT " + String.join(", ", columns)); + if (explainPlanAttributesBuilder != null) { + explainPlanAttributesBuilder.setServerProject(columns); + } + } + + /** + * Render a {@link RowProjector}'s column list as the display names used by the VERBOSE + * {@code PROJECT} line, falling back to the projector's expression string when a column projector + * has no name. + * @return the list of display names, or {@code null} when the projector is {@code null} or + * projects nothing + */ + public static List<String> projectedColumnNames(RowProjector projector) { + if (projector == null) { + return null; + } List<? extends ColumnProjector> columnProjectors = projector.getColumnProjectors(); if (columnProjectors == null || columnProjectors.isEmpty()) { - return; + return null; } List<String> columns = new ArrayList<>(columnProjectors.size()); for (ColumnProjector columnProjector : columnProjectors) { @@ -620,10 +637,45 @@ public abstract class ExplainTable { } columns.add(name); } - planSteps.add(" PROJECT " + String.join(", ", columns)); - if (explainPlanAttributesBuilder != null) { - explainPlanAttributesBuilder.setServerProject(columns); + return columns; + } + + /** + * Server-side {@code UPSERT SELECT} and {@code DELETE} inner plans are compiled as an aggregating + * {@code SELECT COUNT(1)} whose count is used solely to report how many rows were touched. That + * count is an internal compiler choice. Surfacing {@code PROJECT COUNT(1)} on the explain output + * is misleading because the user asked to upsert/delete a set of columns/rows, not to count them. + * @param planSteps the composed mutation plan steps (mutated in place) + * @param innerAttributes the inner aggregate plan's rendered attributes (source of the count-form + * {@code serverProject}) + * @param builder the mutation plan's attribute builder (its {@code serverProject} is + * overwritten) + * @param userProjector the user-facing projection to surface instead of the count form + */ + public static void overrideMutationProject(List<String> planSteps, + ExplainPlanAttributes innerAttributes, ExplainPlanAttributesBuilder builder, + RowProjector userProjector) { + if (planSteps == null || innerAttributes == null || builder == null) { + return; + } + List<String> countProject = innerAttributes.getServerProject(); + if (countProject == null || countProject.isEmpty()) { + return; + } + // Only override the internal COUNT(*)/COUNT(1) aggregate projection used for mutation row counts. + if (countProject.size() != 1 || !countProject.get(0).startsWith("COUNT(")) { + return; + } + List<String> userColumns = projectedColumnNames(userProjector); + if (userColumns == null || userColumns.isEmpty()) { + return; + } + String oldLine = " PROJECT " + String.join(", ", countProject); + int idx = planSteps.indexOf(oldLine); + if (idx >= 0) { + planSteps.set(idx, " PROJECT " + String.join(", ", userColumns)); } + builder.setServerProject(userColumns); } /**
