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 16f9c9327f0ca276b9ede4ec5ceb20bb4fbc76ec 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; }
