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 c06c96fe54 PHOENIX-7902 SERVER ARRAY|JSON|BSON PROJECTION counted 
forms in EXPLAIN (#2522)
c06c96fe54 is described below

commit c06c96fe548c1ab4fb2b012643666a6ba891c4a8
Author: Andrew Purtell <[email protected]>
AuthorDate: Thu Jun 11 18:42:28 2026 -0700

    PHOENIX-7902 SERVER ARRAY|JSON|BSON PROJECTION counted forms in EXPLAIN 
(#2522)
    
    Co-authored-by: Claude Opus 4.8[1m] <[email protected]>
---
 .../phoenix/compile/ExplainPlanAttributes.java     | 52 ++++++++++++++++-----
 .../apache/phoenix/compile/ProjectionCompiler.java |  3 ++
 .../apache/phoenix/compile/StatementContext.java   | 21 +++++++++
 .../org/apache/phoenix/iterate/ExplainTable.java   | 53 ++++++++++++++++++----
 .../end2end/ProjectArrayElemAfterHashJoinIT.java   |  3 +-
 .../phoenix/end2end/json/JsonFunctionsIT.java      | 20 ++++----
 .../apache/phoenix/compile/QueryCompilerTest.java  | 14 +++---
 .../phoenix/query/explain/ExplainPlanTest.java     | 39 ++++++++++++++--
 .../phoenix/query/explain/ExplainPlanTestUtil.java | 38 ++++++++++++++--
 9 files changed, 198 insertions(+), 45 deletions(-)

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 9b9dd8dd8c..45768f6f73 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
@@ -21,7 +21,9 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.apache.hadoop.hbase.HRegionLocation;
 import org.apache.hadoop.hbase.client.Consistency;
@@ -41,7 +43,7 @@ import org.apache.phoenix.schema.PColumn;
   "indexRejected", "saltBuckets", "regionsPlanned", "scanTimeRangeMin", 
"scanTimeRangeMax",
   "splitsChunk", "useRoundRobinIterator", "samplingRate", "hexStringRVCOffset",
   "iteratorTypeAndScanSize", "scanEstimatedRows", "scanEstimatedSizeInBytes", 
"serverWhereFilter",
-  "serverDistinctFilter", "serverMergeColumns", "serverArrayElementProjection",
+  "serverDistinctFilter", "serverMergeColumns", "serverParsedProjections",
   "serverFirstKeyOnlyProjection", "serverEmptyColumnOnlyProjection", 
"serverAggregate",
   "serverGroupByLimit", "serverSortedBy", "serverOffset", "serverRowLimit", 
"clientFilterBy",
   "clientAggregate", "clientDistinctFilter", "clientAfterAggregate", 
"clientSortAlgo",
@@ -91,7 +93,7 @@ public class ExplainPlanAttributes {
   private final String serverWhereFilter;
   private final String serverDistinctFilter;
   private final Set<PColumn> serverMergeColumns;
-  private final boolean serverArrayElementProjection;
+  private final Map<String, List<String>> serverParsedProjections;
   private final boolean serverFirstKeyOnlyProjection;
   private final boolean serverEmptyColumnOnlyProjection;
   private final String serverAggregate;
@@ -165,7 +167,7 @@ public class ExplainPlanAttributes {
     this.serverWhereFilter = null;
     this.serverDistinctFilter = null;
     this.serverMergeColumns = null;
-    this.serverArrayElementProjection = false;
+    this.serverParsedProjections = null;
     this.serverFirstKeyOnlyProjection = false;
     this.serverEmptyColumnOnlyProjection = false;
     this.serverAggregate = null;
@@ -205,7 +207,7 @@ public class ExplainPlanAttributes {
     Integer splitsChunk, boolean useRoundRobinIterator, Double samplingRate,
     String hexStringRVCOffset, String iteratorTypeAndScanSize, Long 
scanEstimatedRows,
     Long scanEstimatedSizeInBytes, String serverWhereFilter, String 
serverDistinctFilter,
-    Set<PColumn> serverMergeColumns, boolean serverArrayElementProjection,
+    Set<PColumn> serverMergeColumns, Map<String, List<String>> 
serverParsedProjections,
     boolean serverFirstKeyOnlyProjection, boolean 
serverEmptyColumnOnlyProjection,
     String serverAggregate, Integer serverGroupByLimit, String serverSortedBy, 
Integer serverOffset,
     Long serverRowLimit, String clientFilterBy, String clientAggregate, String 
clientDistinctFilter,
@@ -253,7 +255,7 @@ public class ExplainPlanAttributes {
     this.serverWhereFilter = serverWhereFilter;
     this.serverDistinctFilter = serverDistinctFilter;
     this.serverMergeColumns = serverMergeColumns;
-    this.serverArrayElementProjection = serverArrayElementProjection;
+    this.serverParsedProjections = 
copyServerParsedProjections(serverParsedProjections);
     this.serverFirstKeyOnlyProjection = serverFirstKeyOnlyProjection;
     this.serverEmptyColumnOnlyProjection = serverEmptyColumnOnlyProjection;
     this.serverAggregate = serverAggregate;
@@ -419,8 +421,20 @@ public class ExplainPlanAttributes {
     return serverMergeColumns;
   }
 
-  public boolean isServerArrayElementProjection() {
-    return serverArrayElementProjection;
+  public Map<String, List<String>> getServerParsedProjections() {
+    return serverParsedProjections;
+  }
+
+  private static Map<String, List<String>>
+    copyServerParsedProjections(Map<String, List<String>> source) {
+    if (source == null || source.isEmpty()) {
+      return null;
+    }
+    Map<String, List<String>> copy = new LinkedHashMap<>();
+    for (Map.Entry<String, List<String>> entry : source.entrySet()) {
+      copy.put(entry.getKey(), Collections.unmodifiableList(new 
ArrayList<>(entry.getValue())));
+    }
+    return Collections.unmodifiableMap(copy);
   }
 
   public boolean isServerFirstKeyOnlyProjection() {
@@ -574,7 +588,7 @@ public class ExplainPlanAttributes {
     private String serverWhereFilter;
     private String serverDistinctFilter;
     private Set<PColumn> serverMergeColumns;
-    private boolean serverArrayElementProjection;
+    private Map<String, List<String>> serverParsedProjections;
     private boolean serverFirstKeyOnlyProjection;
     private boolean serverEmptyColumnOnlyProjection;
     private String serverAggregate;
@@ -644,7 +658,10 @@ public class ExplainPlanAttributes {
       this.serverWhereFilter = explainPlanAttributes.getServerWhereFilter();
       this.serverDistinctFilter = 
explainPlanAttributes.getServerDistinctFilter();
       this.serverMergeColumns = explainPlanAttributes.getServerMergeColumns();
-      this.serverArrayElementProjection = 
explainPlanAttributes.isServerArrayElementProjection();
+      Map<String, List<String>> srcServerParsedProjections =
+        explainPlanAttributes.getServerParsedProjections();
+      this.serverParsedProjections =
+        srcServerParsedProjections == null ? null : new 
LinkedHashMap<>(srcServerParsedProjections);
       this.serverFirstKeyOnlyProjection = 
explainPlanAttributes.isServerFirstKeyOnlyProjection();
       this.serverEmptyColumnOnlyProjection =
         explainPlanAttributes.isServerEmptyColumnOnlyProjection();
@@ -851,8 +868,19 @@ public class ExplainPlanAttributes {
     }
 
     public ExplainPlanAttributesBuilder
-      setServerArrayElementProjection(boolean serverArrayElementProjection) {
-      this.serverArrayElementProjection = serverArrayElementProjection;
+      setServerParsedProjections(Map<String, List<String>> 
serverParsedProjections) {
+      this.serverParsedProjections =
+        serverParsedProjections == null ? null : new 
LinkedHashMap<>(serverParsedProjections);
+      return this;
+    }
+
+    public ExplainPlanAttributesBuilder addServerParsedProjection(String label,
+      List<String> details) {
+      if (this.serverParsedProjections == null) {
+        this.serverParsedProjections = new LinkedHashMap<>();
+      }
+      this.serverParsedProjections.put(label,
+        Collections.unmodifiableList(new ArrayList<>(details)));
       return this;
     }
 
@@ -1016,7 +1044,7 @@ public class ExplainPlanAttributes {
         indexRejected, saltBuckets, regionsPlanned, scanTimeRangeMin, 
scanTimeRangeMax, splitsChunk,
         useRoundRobinIterator, samplingRate, hexStringRVCOffset, 
iteratorTypeAndScanSize,
         scanEstimatedRows, scanEstimatedSizeInBytes, serverWhereFilter, 
serverDistinctFilter,
-        serverMergeColumns, serverArrayElementProjection, 
serverFirstKeyOnlyProjection,
+        serverMergeColumns, serverParsedProjections, 
serverFirstKeyOnlyProjection,
         serverEmptyColumnOnlyProjection, serverAggregate, serverGroupByLimit, 
serverSortedBy,
         serverOffset, serverRowLimit, clientFilterBy, clientAggregate, 
clientDistinctFilter,
         clientAfterAggregate, clientSortAlgo, clientSortedBy, clientOffset, 
clientRowLimit,
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
index 4ee8c1f469..138f000e50 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ProjectionCompiler.java
@@ -600,6 +600,9 @@ public class ProjectionCompiler {
             serverAttributeToKVExpressionMap.get(entry.getKey()));
         }
       }
+      // Stash the per-type expression buckets on the context so EXPLAIN can 
render the per-type
+      // SERVER ARRAY|JSON|BSON PROJECTION clauses (and their per-expression 
detail lines).
+      context.setServerParsedProjections(serverAttributeToFuncExpressionMap);
       KeyValueSchemaBuilder builder = new KeyValueSchemaBuilder(0);
       for (Expression expression : serverParsedKVRefs) {
         builder.addField(expression);
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
index e329b4ee86..d5eac400e1 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/StatementContext.java
@@ -31,6 +31,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.hadoop.hbase.client.Scan;
 import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
 import org.apache.hadoop.hbase.util.Pair;
+import org.apache.phoenix.expression.Expression;
 import org.apache.phoenix.jdbc.PhoenixConnection;
 import org.apache.phoenix.jdbc.PhoenixStatement;
 import org.apache.phoenix.log.QueryLogger;
@@ -106,6 +107,7 @@ public class StatementContext {
   private int derivedTableFlattenCount;
   private List<Pair<ParseNode, String>> indexExpressionSubstitutions;
   private Set<Pair<String, String>> partialIndexCheckedSet;
+  private Map<String, List<Expression>> serverParsedProjections;
   private StatementContext parentContext;
 
   public StatementContext(PhoenixStatement statement) {
@@ -147,6 +149,7 @@ public class StatementContext {
     this.derivedTableFlattenCount = context.derivedTableFlattenCount;
     this.indexExpressionSubstitutions = context.indexExpressionSubstitutions;
     this.partialIndexCheckedSet = context.partialIndexCheckedSet;
+    this.serverParsedProjections = context.serverParsedProjections;
     this.parentContext = context.parentContext;
   }
 
@@ -214,6 +217,7 @@ public class StatementContext {
     this.derivedTableFlattenCount = 0;
     this.indexExpressionSubstitutions = new ArrayList<>();
     this.partialIndexCheckedSet = Sets.newHashSet();
+    this.serverParsedProjections = null;
     this.parentContext = null;
   }
 
@@ -506,6 +510,7 @@ public class StatementContext {
     this.derivedTableFlattenCount = source.derivedTableFlattenCount;
     this.indexExpressionSubstitutions = source.indexExpressionSubstitutions;
     this.partialIndexCheckedSet = source.partialIndexCheckedSet;
+    this.serverParsedProjections = source.serverParsedProjections;
   }
 
   public void incrementDerivedTableFlattenCount() {
@@ -534,6 +539,22 @@ public class StatementContext {
     return partialIndexCheckedSet.add(new Pair<>(tableName, indexName));
   }
 
+  /**
+   * Server-evaluated parsed projection expressions, keyed by the scan 
attribute they were
+   * serialized into ({@code _SpecificArrayIndex}, {@code _JsonValueFunction},
+   * {@code _JsonQueryFunction}, {@code _BsonValueFunction}). Populated by
+   * {@link ProjectionCompiler} when at least one JSON/BSON/array path 
expression is pushed to the
+   * server, and consumed by {@code ExplainTable} to render the per-type 
{@code SERVER * PROJECTION}
+   * clauses. {@code null} when no server-side parsed projection compile 
occurred.
+   */
+  public Map<String, List<Expression>> getServerParsedProjections() {
+    return serverParsedProjections;
+  }
+
+  public void setServerParsedProjections(Map<String, List<Expression>> 
serverParsedProjections) {
+    this.serverParsedProjections = serverParsedProjections;
+  }
+
   public StatementContext getParentContext() {
     return parentContext;
   }
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 28318ea2c6..62b8ed556f 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
@@ -26,6 +26,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import org.apache.hadoop.hbase.HRegionLocation;
 import org.apache.hadoop.hbase.client.Consistency;
@@ -44,6 +45,7 @@ import org.apache.phoenix.compile.ScanRanges;
 import org.apache.phoenix.compile.StatementContext;
 import org.apache.phoenix.compile.StatementPlan;
 import org.apache.phoenix.coprocessorclient.BaseScannerRegionObserverConstants;
+import org.apache.phoenix.expression.Expression;
 import org.apache.phoenix.filter.BooleanExpressionFilter;
 import org.apache.phoenix.filter.DistinctPrefixFilter;
 import org.apache.phoenix.filter.EmptyColumnOnlyFilter;
@@ -513,17 +515,50 @@ public abstract class ExplainTable {
     }
     getRegionLocations(planSteps, explainPlanAttributesBuilder, 
regionLocations);
     groupBy.explain(planSteps, groupByLimit, explainPlanAttributesBuilder);
-    if 
(scan.getAttribute(BaseScannerRegionObserverConstants.SPECIFIC_ARRAY_INDEX) != 
null) {
-      planSteps.add("    SERVER ARRAY ELEMENT PROJECTION");
-      if (explainPlanAttributesBuilder != null) {
-        explainPlanAttributesBuilder.setServerArrayElementProjection(true);
+    emitServerProjection(planSteps, explainPlanAttributesBuilder, "ARRAY",
+      
Collections.singletonList(BaseScannerRegionObserverConstants.SPECIFIC_ARRAY_INDEX));
+    emitServerProjection(planSteps, explainPlanAttributesBuilder, "JSON",
+      Arrays.asList(BaseScannerRegionObserverConstants.JSON_VALUE_FUNCTION,
+        BaseScannerRegionObserverConstants.JSON_QUERY_FUNCTION));
+    emitServerProjection(planSteps, explainPlanAttributesBuilder, "BSON",
+      
Collections.singletonList(BaseScannerRegionObserverConstants.BSON_VALUE_FUNCTION));
+  }
+
+  /**
+   * Emit a {@code SERVER <label> PROJECTION <count>} clause plus one indented 
detail line per
+   * server evaluated path expression of the given type. The expressions are 
read from
+   * {@link StatementContext#getServerParsedProjections()}, gathering the 
buckets named by
+   * {@code attributeKeys} in order.
+   * @param planSteps                    plan step lines to append to
+   * @param explainPlanAttributesBuilder attributes builder to populate, or 
{@code null}
+   * @param label                        the type label ({@code ARRAY}, {@code 
JSON}, {@code BSON})
+   * @param attributeKeys                the scan attribute bucket keys 
contributing to this type
+   */
+  private void emitServerProjection(List<String> planSteps,
+    ExplainPlanAttributesBuilder explainPlanAttributesBuilder, String label,
+    List<String> attributeKeys) {
+    Map<String, List<Expression>> serverParsedProjections = 
context.getServerParsedProjections();
+    if (serverParsedProjections == null) {
+      return;
+    }
+    List<String> details = new ArrayList<>();
+    for (String attributeKey : attributeKeys) {
+      List<Expression> expressions = serverParsedProjections.get(attributeKey);
+      if (expressions != null) {
+        for (Expression expression : expressions) {
+          details.add(expression.toString());
+        }
       }
     }
-    if (
-      
scan.getAttribute(BaseScannerRegionObserverConstants.JSON_VALUE_FUNCTION) != 
null
-        || 
scan.getAttribute(BaseScannerRegionObserverConstants.JSON_QUERY_FUNCTION) != 
null
-    ) {
-      planSteps.add("    SERVER JSON FUNCTION PROJECTION");
+    if (details.isEmpty()) {
+      return;
+    }
+    planSteps.add("    SERVER " + label + " PROJECTION " + details.size());
+    for (String detail : details) {
+      planSteps.add("        " + detail);
+    }
+    if (explainPlanAttributesBuilder != null) {
+      explainPlanAttributesBuilder.addServerParsedProjection(label, details);
     }
   }
 
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ProjectArrayElemAfterHashJoinIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ProjectArrayElemAfterHashJoinIT.java
index 0b882f3a16..18899bd340 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/ProjectArrayElemAfterHashJoinIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/ProjectArrayElemAfterHashJoinIT.java
@@ -110,11 +110,10 @@ public class ProjectArrayElemAfterHashJoinIT extends 
ParallelStatsDisabledIT {
 
   private void verifyExplain(Connection conn, String table, boolean fullArray, 
boolean hashJoin)
     throws Exception {
-
     String query = getQuery(table, fullArray, hashJoin);
     ExplainPlanAttributes attributes = getExplainAttributes(conn, query);
     if (!fullArray) {
-      assertPlan(attributes).serverArrayElementProjection(true);
+      assertPlan(attributes).serverParsedProjectionCount("ARRAY", 4);
     }
     assertPlan(attributes).subPlanCount(hashJoin ? 1 : 0);
   }
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/json/JsonFunctionsIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/json/JsonFunctionsIT.java
index f452aacace..d33debd7b7 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/json/JsonFunctionsIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/json/JsonFunctionsIT.java
@@ -113,7 +113,7 @@ public class JsonFunctionsIT extends 
ParallelStatsDisabledIT {
 
       // Check here for the JSON server side projection
       rs = conn.createStatement().executeQuery("EXPLAIN " + query);
-      assertTrue(QueryUtil.getExplainPlan(rs).contains("    SERVER JSON 
FUNCTION PROJECTION"));
+      assertTrue(QueryUtil.getExplainPlan(rs).contains("    SERVER JSON 
PROJECTION "));
     }
   }
 
@@ -542,7 +542,7 @@ public class JsonFunctionsIT extends 
ParallelStatsDisabledIT {
       String query = String.format(queryTemplate, "AndersenFamily");
       // check if the explain plan indicates server side execution
       ResultSet rs = conn.createStatement().executeQuery("EXPLAIN " + query);
-      assertFalse(QueryUtil.getExplainPlan(rs).contains("    SERVER JSON 
FUNCTION PROJECTION"));
+      assertFalse(QueryUtil.getExplainPlan(rs).contains("    SERVER JSON 
PROJECTION "));
     }
   }
 
@@ -568,8 +568,8 @@ public class JsonFunctionsIT extends 
ParallelStatsDisabledIT {
       // Since we are using complete array and json col, no server side 
execution
       ResultSet rs = conn.createStatement().executeQuery("EXPLAIN " + query);
       String explainPlan = QueryUtil.getExplainPlan(rs);
-      assertFalse(explainPlan.contains("    SERVER JSON FUNCTION PROJECTION"));
-      assertPlan(conn, query).serverArrayElementProjection(false)
+      assertFalse(explainPlan.contains("    SERVER JSON PROJECTION "));
+      assertPlan(conn, query).serverParsedProjectionsNone()
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
       rs = conn.createStatement().executeQuery(query);
       assertTrue(rs.next());
@@ -583,8 +583,8 @@ public class JsonFunctionsIT extends 
ParallelStatsDisabledIT {
         + " WHERE JSON_VALUE(jsoncol, '$.name') = 'AndersenFamily'";
       rs = conn.createStatement().executeQuery("EXPLAIN " + query);
       explainPlan = QueryUtil.getExplainPlan(rs);
-      assertTrue(explainPlan.contains("    SERVER JSON FUNCTION PROJECTION"));
-      assertPlan(conn, query).serverArrayElementProjection(true)
+      assertTrue(explainPlan.contains("    SERVER JSON PROJECTION "));
+      assertPlan(conn, query).serverParsedProjectionCount("ARRAY", 1)
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       // only Array optimization and not Json
@@ -592,8 +592,8 @@ public class JsonFunctionsIT extends 
ParallelStatsDisabledIT {
         + " WHERE JSON_VALUE(jsoncol, '$.name') = 'AndersenFamily'";
       rs = conn.createStatement().executeQuery("EXPLAIN " + query);
       explainPlan = QueryUtil.getExplainPlan(rs);
-      assertFalse(explainPlan.contains("    SERVER JSON FUNCTION PROJECTION"));
-      assertPlan(conn, query).serverArrayElementProjection(true)
+      assertFalse(explainPlan.contains("    SERVER JSON PROJECTION "));
+      assertPlan(conn, query).serverParsedProjectionCount("ARRAY", 1)
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       // only Json optimization and not Array Index
@@ -601,8 +601,8 @@ public class JsonFunctionsIT extends 
ParallelStatsDisabledIT {
         + " WHERE JSON_VALUE(jsoncol, '$.name') = 'AndersenFamily'";
       rs = conn.createStatement().executeQuery("EXPLAIN " + query);
       explainPlan = QueryUtil.getExplainPlan(rs);
-      assertTrue(explainPlan.contains("    SERVER JSON FUNCTION PROJECTION"));
-      assertPlan(conn, query).serverArrayElementProjection(false)
+      assertTrue(explainPlan.contains("    SERVER JSON PROJECTION "));
+      assertPlan(conn, query).serverParsedProjectionCount("ARRAY", 0)
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
     }
   }
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
index d806e615f1..ec714f23e2 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
@@ -2202,7 +2202,8 @@ public class QueryCompilerTest extends 
BaseConnectionlessQueryTest {
     Connection conn = DriverManager.getConnection(getUrl());
     try {
       conn.createStatement().execute("CREATE TABLE t(a INTEGER PRIMARY KEY, 
arr INTEGER ARRAY)");
-      assertPlan(conn, "SELECT arr[1] from 
t").serverArrayElementProjection(true);
+      assertPlan(conn, "SELECT arr[1] from t").serverParsedProjections("ARRAY",
+        "ARRAY_ELEM(ARR, 1)");
     } finally {
       conn.createStatement().execute("DROP TABLE IF EXISTS t");
       conn.close();
@@ -2214,7 +2215,7 @@ public class QueryCompilerTest extends 
BaseConnectionlessQueryTest {
     Connection conn = DriverManager.getConnection(getUrl());
     try {
       conn.createStatement().execute("CREATE TABLE t(a INTEGER PRIMARY KEY, 
arr INTEGER ARRAY)");
-      assertPlan(conn, "SELECT arr, arr[1] from 
t").serverArrayElementProjection(false);
+      assertPlan(conn, "SELECT arr, arr[1] from 
t").serverParsedProjectionsNone();
     } finally {
       conn.createStatement().execute("DROP TABLE IF EXISTS t");
       conn.close();
@@ -2227,7 +2228,8 @@ public class QueryCompilerTest extends 
BaseConnectionlessQueryTest {
     try {
       conn.createStatement()
         .execute("CREATE TABLE t(a INTEGER PRIMARY KEY, arr INTEGER ARRAY, 
arr2 VARCHAR ARRAY)");
-      assertPlan(conn, "SELECT arr, arr[1], arr2[1] from 
t").serverArrayElementProjection(true);
+      assertPlan(conn, "SELECT arr, arr[1], arr2[1] from 
t").serverParsedProjections("ARRAY",
+        "ARRAY_ELEM(ARR2, 1)");
     } finally {
       conn.createStatement().execute("DROP TABLE IF EXISTS t");
       conn.close();
@@ -2242,7 +2244,7 @@ public class QueryCompilerTest extends 
BaseConnectionlessQueryTest {
         .execute("CREATE TABLE t (p INTEGER PRIMARY KEY, arr1 INTEGER ARRAY, 
arr2 INTEGER ARRAY)");
       assertPlan(conn,
         "SELECT arr1, arr1[1], ARRAY_APPEND(ARRAY_APPEND(arr1, arr2[2]), 
arr2[1]), p from t")
-          .serverArrayElementProjection(true);
+          .serverParsedProjectionCount("ARRAY", 2);
     } finally {
       conn.createStatement().execute("DROP TABLE IF EXISTS t");
       conn.close();
@@ -2302,7 +2304,7 @@ public class QueryCompilerTest extends 
BaseConnectionlessQueryTest {
         .execute("CREATE TABLE t (p INTEGER PRIMARY KEY, arr1 INTEGER ARRAY, 
arr2 INTEGER ARRAY)");
       assertPlan(conn,
         "SELECT arr1, arr1[1], ARRAY_ELEM(ARRAY_APPEND(arr1, arr2[1]), 1), p, 
arr2[2] from t")
-          .serverArrayElementProjection(true);
+          .serverParsedProjectionCount("ARRAY", 2);
     } finally {
       conn.createStatement().execute("DROP TABLE IF EXISTS t");
       conn.close();
@@ -2314,7 +2316,7 @@ public class QueryCompilerTest extends 
BaseConnectionlessQueryTest {
     Connection conn = DriverManager.getConnection(getUrl());
     try {
       conn.createStatement().execute("CREATE TABLE t(arr INTEGER ARRAY PRIMARY 
KEY)");
-      assertPlan(conn, "SELECT arr[1] from 
t").serverArrayElementProjection(false);
+      assertPlan(conn, "SELECT arr[1] from t").serverParsedProjectionsNone();
     } finally {
       conn.createStatement().execute("DROP TABLE IF EXISTS t");
       conn.close();
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 cd4503bf3f..99ad4f17e7 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
@@ -76,6 +76,8 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
   private static final String MT_BASE = "EO_MT_BASE";
   private static final String MT_VIEW = "EO_MT_VIEW";
   private static final String TENANT_ID = "tenant42";
+  private static final String JSON_TBL = "EO_JSON";
+  private static final String BSON_TBL = "EO_BSON";
 
   private static ExplainOracle oracle;
   private static ObjectMapper mapper;
@@ -89,6 +91,10 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
       conn.createStatement().execute("CREATE TABLE IF NOT EXISTS " + SALTED
         + " (k VARCHAR NOT NULL PRIMARY KEY, v INTEGER) SALT_BUCKETS=4");
       conn.createStatement().execute("CREATE SEQUENCE IF NOT EXISTS " + SEQ);
+      conn.createStatement().execute("CREATE TABLE IF NOT EXISTS " + JSON_TBL
+        + " (pk VARCHAR NOT NULL PRIMARY KEY, jsoncol JSON)");
+      conn.createStatement().execute("CREATE TABLE IF NOT EXISTS " + BSON_TBL
+        + " (pk VARCHAR NOT NULL PRIMARY KEY, payload BSON)");
       conn.createStatement()
         .execute("CREATE TABLE IF NOT EXISTS " + MT_BASE + " (" + "  tenant_id 
VARCHAR(8) NOT NULL,"
           + "  userid INTEGER NOT NULL," + "  username VARCHAR NOT NULL," + "  
col VARCHAR"
@@ -283,10 +289,37 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
 
   @Test
   public void testArrayElementProjection() throws Exception {
+    ObjectNode arrayBucket = mapper.createObjectNode();
+    arrayBucket.set("ARRAY", 
mapper.createArrayNode().add("ARRAY_ELEM(A_STRING_ARRAY, 1)"));
     verifyQuery("arrayElementProjection", "SELECT a_string_array[1] FROM 
table_with_array",
       text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER TABLE_WITH_ARRAY", "    
INDEX TABLE_WITH_ARRAY",
-        "    REGIONS PLANNED <N>", "    SERVER ARRAY ELEMENT PROJECTION"),
-      scanAttrs("FULL SCAN ", "TABLE_WITH_ARRAY", 
"").put("serverArrayElementProjection", true));
+        "    REGIONS PLANNED <N>", "    SERVER ARRAY PROJECTION 1",
+        "        ARRAY_ELEM(A_STRING_ARRAY, 1)"),
+      scanAttrs("FULL SCAN ", "TABLE_WITH_ARRAY", 
"").set("serverParsedProjections", arrayBucket));
+  }
+
+  @Test
+  public void testJsonFunctionProjection() throws Exception {
+    ObjectNode jsonBucket = mapper.createObjectNode();
+    jsonBucket.set("JSON", mapper.createArrayNode().add("JSON_VALUE(JSONCOL, 
'$.type')"));
+    verifyQuery("jsonFunctionProjection", "SELECT JSON_VALUE(jsoncol, 
'$.type') FROM " + JSON_TBL,
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER " + JSON_TBL, "    INDEX " 
+ JSON_TBL,
+        "    REGIONS PLANNED <N>", "    SERVER JSON PROJECTION 1",
+        "        JSON_VALUE(JSONCOL, '$.type')"),
+      scanAttrs("FULL SCAN ", JSON_TBL, "").set("serverParsedProjections", 
jsonBucket));
+  }
+
+  @Test
+  public void testBsonValueProjection() throws Exception {
+    ObjectNode bsonBucket = mapper.createObjectNode();
+    bsonBucket.set("BSON",
+      mapper.createArrayNode().add("BSON_VALUE(PAYLOAD, 'user.id', 'VARCHAR', 
)"));
+    verifyQuery("bsonValueProjection",
+      "SELECT BSON_VALUE(payload, 'user.id', 'VARCHAR') FROM " + BSON_TBL,
+      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER " + BSON_TBL, "    INDEX " 
+ BSON_TBL,
+        "    REGIONS PLANNED <N>", "    SERVER BSON PROJECTION 1",
+        "        BSON_VALUE(PAYLOAD, 'user.id', 'VARCHAR', )"),
+      scanAttrs("FULL SCAN ", BSON_TBL, "").set("serverParsedProjections", 
bsonBucket));
   }
 
   @Test
@@ -1406,7 +1439,7 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     n.putNull("serverDistinctFilter");
     n.putNull("serverOffset");
     n.putNull("serverRowLimit");
-    n.put("serverArrayElementProjection", false);
+    n.putNull("serverParsedProjections");
     n.put("serverFirstKeyOnlyProjection", false);
     n.put("serverEmptyColumnOnlyProjection", false);
     n.putNull("serverAggregate");
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 62e32025f0..fffc655160 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
@@ -27,6 +27,7 @@ import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import org.apache.phoenix.compile.ExplainPlan;
 import org.apache.phoenix.compile.ExplainPlanAttributes;
 import org.apache.phoenix.compile.QueryPlan;
@@ -404,9 +405,40 @@ public final class ExplainPlanTestUtil {
       return this;
     }
 
-    public ExplainPlanAssert serverArrayElementProjection(boolean expected) {
-      assertEquals(at("serverArrayElementProjection"), expected,
-        attributes.isServerArrayElementProjection());
+    /** Assert the entire server-parsed-projection map matches {@code 
expected}. */
+    public ExplainPlanAssert serverParsedProjections(Map<String, List<String>> 
expected) {
+      assertEquals(at("serverParsedProjections"), expected,
+        attributes.getServerParsedProjections());
+      return this;
+    }
+
+    /**
+     * Assert that the named bucket ({@code ARRAY}, {@code JSON}, {@code 
BSON}) holds exactly the
+     * listed per-expression renderings, in order.
+     */
+    public ExplainPlanAssert serverParsedProjections(String label, String... 
expected) {
+      Map<String, List<String>> actual = 
attributes.getServerParsedProjections();
+      assertNotNull(at("serverParsedProjections") + " must not be null", 
actual);
+      List<String> bucket = actual.get(label);
+      assertNotNull(at("serverParsedProjections[" + label + "]") + " must not 
be null", bucket);
+      assertEquals(at("serverParsedProjections[" + label + "]"), 
Arrays.asList(expected), bucket);
+      return this;
+    }
+
+    /** Assert that no server-parsed projections were disclosed (null or 
empty). */
+    public ExplainPlanAssert serverParsedProjectionsNone() {
+      Map<String, List<String>> actual = 
attributes.getServerParsedProjections();
+      assertTrue(at("serverParsedProjections") + " expected none but was " + 
actual,
+        actual == null || actual.isEmpty());
+      return this;
+    }
+
+    /** Assert the number of expressions in the named bucket. */
+    public ExplainPlanAssert serverParsedProjectionCount(String label, int 
expected) {
+      Map<String, List<String>> actual = 
attributes.getServerParsedProjections();
+      List<String> bucket = actual == null ? null : actual.get(label);
+      int actualCount = bucket == null ? 0 : bucket.size();
+      assertEquals(at("serverParsedProjections[" + label + "].size"), 
expected, actualCount);
       return this;
     }
 

Reply via email to