This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch 5.3
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/5.3 by this push:
     new 2eb4ba4df0 Revert "PHOENIX-7879 Tests for EXPLAIN text and 
ExplainPlanAttributes serialization compatibility (#2495)"
2eb4ba4df0 is described below

commit 2eb4ba4df02579753d871b9c7c8f5e46d23748e6
Author: Andrew Purtell <[email protected]>
AuthorDate: Mon Jun 8 08:48:21 2026 -0700

    Revert "PHOENIX-7879 Tests for EXPLAIN text and ExplainPlanAttributes 
serialization compatibility (#2495)"
    
    This reverts commit cec6a64228a3d794834ecdd8ec93a0dcd3a2227e.
---
 .../phoenix/compile/ExplainPlanAttributes.java     |  13 -
 .../compile/RegionLocationsListSerializer.java     |  61 --
 .../compile/ServerMergeColumnsSerializer.java      |  59 --
 .../org/apache/phoenix/schema/MetaDataClient.java  |   6 +-
 .../phoenix/query/explain/ExplainChangeRule.java   |  54 --
 .../query/explain/ExplainCompatibilityTest.java    | 769 ---------------------
 .../query/explain/ExplainJsonNormalizer.java       |  71 --
 .../phoenix/query/explain/ExplainOracle.java       | 207 ------
 .../query/explain/ExplainTextNormalizer.java       |  68 --
 9 files changed, 3 insertions(+), 1305 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 f4ed63f245..07f00f2b67 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
@@ -17,8 +17,6 @@
  */
 package org.apache.phoenix.compile;
 
-import com.fasterxml.jackson.annotation.JsonPropertyOrder;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import java.util.List;
 import java.util.Set;
 import org.apache.hadoop.hbase.HRegionLocation;
@@ -32,15 +30,6 @@ import org.apache.phoenix.schema.PColumn;
  * against. This also makes attribute retrieval easier as an API rather than 
retrieving list of
  * Strings containing entire plan.
  */
-@JsonPropertyOrder({ "abstractExplainPlan", "splitsChunk", "estimatedRows", 
"estimatedSizeInBytes",
-  "iteratorTypeAndScanSize", "samplingRate", "useRoundRobinIterator", 
"hexStringRVCOffset",
-  "consistency", "hint", "serverSortedBy", "explainScanType", "tableName", 
"keyRanges",
-  "scanTimeRangeMin", "scanTimeRangeMax", "serverWhereFilter", 
"serverDistinctFilter",
-  "serverOffset", "serverRowLimit", "serverArrayElementProjection", 
"serverAggregate",
-  "clientFilterBy", "clientAggregate", "clientSortedBy", 
"clientAfterAggregate",
-  "clientDistinctFilter", "clientOffset", "clientRowLimit", 
"clientSequenceCount",
-  "clientCursorName", "clientSortAlgo", "rhsJoinQueryExplainPlan", 
"serverMergeColumns",
-  "regionLocations", "numRegionLocationLookups" })
 public class ExplainPlanAttributes {
 
   private final String abstractExplainPlan;
@@ -310,12 +299,10 @@ public class ExplainPlanAttributes {
     return rhsJoinQueryExplainPlan;
   }
 
-  @JsonSerialize(using = ServerMergeColumnsSerializer.class)
   public Set<PColumn> getServerMergeColumns() {
     return serverMergeColumns;
   }
 
-  @JsonSerialize(using = RegionLocationsListSerializer.class)
   public List<HRegionLocation> getRegionLocations() {
     return regionLocations;
   }
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RegionLocationsListSerializer.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RegionLocationsListSerializer.java
deleted file mode 100644
index 1ae1b77a53..0000000000
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RegionLocationsListSerializer.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.phoenix.compile;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.ser.std.StdSerializer;
-import java.io.IOException;
-import java.util.List;
-import org.apache.hadoop.hbase.HRegionLocation;
-import org.apache.hadoop.hbase.ServerName;
-import org.apache.hadoop.hbase.client.RegionInfo;
-import org.apache.hadoop.hbase.util.Bytes;
-
-/**
- * Jackson serializer for {@code List<HRegionLocation>} as it appears on
- * {@link ExplainPlanAttributes#getRegionLocations()}. The HBase {@code 
HRegionLocation} bean is not
- * cleanly serializable by Jackson's default introspection.
- */
-public class RegionLocationsListSerializer extends 
StdSerializer<List<HRegionLocation>> {
-
-  private static final long serialVersionUID = 1L;
-
-  @SuppressWarnings("unchecked")
-  public RegionLocationsListSerializer() {
-    super((Class<List<HRegionLocation>>) (Class<?>) List.class);
-  }
-
-  @Override
-  public void serialize(List<HRegionLocation> value, JsonGenerator gen, 
SerializerProvider provider)
-    throws IOException {
-    gen.writeStartArray();
-    for (HRegionLocation loc : value) {
-      gen.writeStartObject();
-      RegionInfo region = loc == null ? null : loc.getRegion();
-      gen.writeStringField("startKey",
-        region == null ? null : Bytes.toStringBinary(region.getStartKey()));
-      gen.writeStringField("endKey",
-        region == null ? null : Bytes.toStringBinary(region.getEndKey()));
-      ServerName sn = loc == null ? null : loc.getServerName();
-      gen.writeStringField("server", sn == null ? null : sn.toString());
-      gen.writeEndObject();
-    }
-    gen.writeEndArray();
-  }
-}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ServerMergeColumnsSerializer.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ServerMergeColumnsSerializer.java
deleted file mode 100644
index 988c319bb8..0000000000
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/ServerMergeColumnsSerializer.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.phoenix.compile;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.ser.std.StdSerializer;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Set;
-import org.apache.phoenix.schema.PColumn;
-
-/**
- * Jackson serializer for {@code Set<PColumn>} as it appears on
- * {@link ExplainPlanAttributes#getServerMergeColumns()}. {@code PColumn} is 
an interface backed by
- * implementations that are not Jackson-friendly.
- */
-public class ServerMergeColumnsSerializer extends StdSerializer<Set<PColumn>> {
-
-  private static final long serialVersionUID = 1L;
-
-  @SuppressWarnings("unchecked")
-  public ServerMergeColumnsSerializer() {
-    super((Class<Set<PColumn>>) (Class<?>) Set.class);
-  }
-
-  @Override
-  public void serialize(Set<PColumn> value, JsonGenerator gen, 
SerializerProvider provider)
-    throws IOException {
-    List<String> names = new ArrayList<>(value.size());
-    for (PColumn column : value) {
-      names.add(column == null ? null : column.toString());
-    }
-    Collections.sort(names, Comparator.nullsFirst(Comparator.naturalOrder()));
-    gen.writeStartArray();
-    for (String name : names) {
-      gen.writeString(name);
-    }
-    gen.writeEndArray();
-  }
-}
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/MetaDataClient.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/MetaDataClient.java
index a2e719c3f0..4fff6826c6 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/MetaDataClient.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/MetaDataClient.java
@@ -4786,9 +4786,9 @@ public class MetaDataClient {
           /**
            * To check if TTL is defined at any of the child below we are 
checking it at
            * {@link 
org.apache.phoenix.coprocessor.MetaDataEndpointImpl#mutateColumn(List, 
ColumnMutator, int, PTable, PTable, boolean)}
-           * level where in function {@link 
org.apache.phoenix.coprocessor.MetaDataEndpointImpl#
-           * validateIfMutationAllowedOnParent(PTable, List, PTableType, long, 
byte[], byte[],
-           * byte[], List, int)} we are already traversing through 
allDescendantViews.
+           * level where in function
+           * {@link org.apache.phoenix.coprocessor.MetaDataEndpointImpl# 
validateIfMutationAllowedOnParent(PTable, List, PTableType, long, byte[], 
byte[], byte[], List, int)}
+           * we are already traversing through allDescendantViews.
            */
         }
 
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainChangeRule.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainChangeRule.java
deleted file mode 100644
index a661bdca15..0000000000
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainChangeRule.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.phoenix.query.explain;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import java.util.List;
-
-/**
- * The {@link ExplainOracle} freezes today's EXPLAIN output as a set of golden 
fixtures. Future
- * EXPLAIN-design PRs that intentionally change the output register an {@code 
ExplainChangeRule}
- * that rewrites the (frozen) form into the new expected form, so every change 
to the grammar is
- * explicit and reviewable.
- */
-public interface ExplainChangeRule {
-
-  /**
-   * Transform the golden plan steps text for the named case. The default 
implementation returns
-   * {@code goldenText} unchanged.
-   * @param caseId     the corpus case identifier (e.g. {@code "pointLookup"})
-   * @param goldenText the line-oriented golden as currently expected (already 
transformed by any
-   *                   earlier rule in the chain)
-   * @return the next-expected golden text (may be a new list or the same 
instance)
-   */
-  default List<String> applyText(String caseId, List<String> goldenText) {
-    return goldenText;
-  }
-
-  /**
-   * Transform the golden JSON attributes tree for the named case. The default 
implementation
-   * returns {@code goldenJson} unchanged.
-   * @param caseId     the corpus case identifier
-   * @param goldenJson the JSON attributes tree as currently expected (already 
transformed by any
-   *                   earlier rule in the chain)
-   * @return the next-expected JSON tree (may be a mutated input or a new node)
-   */
-  default JsonNode applyJson(String caseId, JsonNode goldenJson) {
-    return goldenJson;
-  }
-}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainCompatibilityTest.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainCompatibilityTest.java
deleted file mode 100644
index cd17176286..0000000000
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainCompatibilityTest.java
+++ /dev/null
@@ -1,769 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.phoenix.query.explain;
-
-import static org.apache.phoenix.query.QueryServices.AUTO_COMMIT_ATTRIB;
-import static org.apache.phoenix.query.QueryServices.DATE_FORMAT_ATTRIB;
-import static org.apache.phoenix.util.PhoenixRuntime.TENANT_ID_ATTRIB;
-import static org.apache.phoenix.util.TestUtil.TEST_PROPERTIES;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Properties;
-import java.util.Set;
-import org.apache.hadoop.hbase.HRegionLocation;
-import org.apache.hadoop.hbase.ServerName;
-import org.apache.hadoop.hbase.TableName;
-import org.apache.hadoop.hbase.client.RegionInfoBuilder;
-import org.apache.phoenix.compile.ExplainPlan;
-import org.apache.phoenix.compile.ExplainPlanAttributes;
-import 
org.apache.phoenix.compile.ExplainPlanAttributes.ExplainPlanAttributesBuilder;
-import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
-import org.apache.phoenix.query.BaseConnectionlessQueryTest;
-import org.apache.phoenix.query.QueryServices;
-import org.apache.phoenix.schema.PColumn;
-import org.apache.phoenix.schema.PColumnImpl;
-import org.apache.phoenix.schema.PName;
-import org.apache.phoenix.schema.PNameFactory;
-import org.apache.phoenix.schema.SortOrder;
-import org.apache.phoenix.schema.types.PInteger;
-import org.apache.phoenix.util.PropertiesUtil;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
-/**
- * Backward compatibility tests for Phoenix EXPLAIN output.
- * <p>
- * Each corpus {@code @Test} method compiles a representative query against a 
connectionless Phoenix
- * driver, builds the expected normalized plan-steps text and JSON attributes 
inline, and hands both
- * to the {@link ExplainOracle} for a tolerant comparison. The corpus covers 
every EXPLAIN grammar
- * branch reachable without a connection.
- */
-public class ExplainCompatibilityTest extends BaseConnectionlessQueryTest {
-
-  private static final String SALTED = "EO_SALTED";
-  private static final String SEQ = "EO_SEQ";
-  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 ExplainOracle oracle;
-  private static ObjectMapper mapper;
-
-  @BeforeClass
-  public static synchronized void setUp() throws Exception {
-    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
-    props.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
-    props.setProperty(QueryServices.FORCE_ROW_KEY_ORDER_ATTRIB, 
Boolean.toString(false));
-    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
-      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 " + MT_BASE + " (" + "  tenant_id 
VARCHAR(8) NOT NULL,"
-          + "  userid INTEGER NOT NULL," + "  username VARCHAR NOT NULL," + "  
col VARCHAR"
-          + "  CONSTRAINT pk PRIMARY KEY (tenant_id, userid, username)) 
MULTI_TENANT=true");
-    }
-    Properties tenantProps = PropertiesUtil.deepCopy(TEST_PROPERTIES);
-    tenantProps.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
-    tenantProps.setProperty(TENANT_ID_ATTRIB, TENANT_ID);
-    try (Connection conn = DriverManager.getConnection(getUrl(), tenantProps)) 
{
-      conn.createStatement()
-        .execute("CREATE VIEW IF NOT EXISTS " + MT_VIEW + " AS SELECT * FROM " 
+ MT_BASE);
-    }
-
-    oracle = new ExplainOracle();
-    mapper = oracle.mapper();
-  }
-
-  @Test
-  public void testPointLookup() throws Exception {
-    verifyQuery("pointLookup",
-      "SELECT a_string, b_string FROM atable"
-        + " WHERE organization_id = '00D000000000001' AND entity_id = 
'00E00000000001'"
-        + " AND x_integer = 2 AND a_integer < 5",
-      text("CLIENT PARALLEL <N>-WAY POINT LOOKUP ON 1 KEY OVER ATABLE",
-        "    SERVER FILTER BY (X_INTEGER = 2 AND A_INTEGER < 5)"),
-      scanAttrs("POINT LOOKUP ON 1 KEY ", "ATABLE", 
null).put("serverWhereFilter",
-        "SERVER FILTER BY (X_INTEGER = 2 AND A_INTEGER < 5)"));
-  }
-
-  @Test
-  public void testPointLookupMultiKey() throws Exception {
-    verifyQuery("pointLookupMultiKey",
-      "SELECT a_string, b_string FROM atable"
-        + " WHERE organization_id IN ('00D000000000001', '00D000000000005')"
-        + " AND entity_id IN ('00E00000000000X','00E00000000000Z')",
-      text("CLIENT PARALLEL <N>-WAY POINT LOOKUP ON 4 KEYS OVER ATABLE"),
-      scanAttrs("POINT LOOKUP ON 4 KEYS ", "ATABLE", null));
-  }
-
-  @Test
-  public void testRangeScan() throws Exception {
-    verifyQuery("rangeScan",
-      "SELECT a_string FROM atable WHERE organization_id = '00D000000000001'"
-        + " AND entity_id > '00E00000000002' AND entity_id < '00E00000000008'",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE"
-        + " ['00D000000000001','00E00000000002!'] - 
['00D000000000001','00E00000000008 ']"),
-      scanAttrs("RANGE SCAN ", "ATABLE",
-        " ['00D000000000001','00E00000000002!'] - 
['00D000000000001','00E00000000008 ']"));
-  }
-
-  @Test
-  public void testSkipScanKeys() throws Exception {
-    verifyQuery("skipScanKeys", "SELECT host FROM ptsdb3 WHERE host IN 
('na1','na2','na3')",
-      text("CLIENT PARALLEL <N>-WAY SKIP SCAN ON 3 KEYS OVER PTSDB3 [~'na3'] - 
[~'na1']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
-      scanAttrs("SKIP SCAN ON 3 KEYS ", "PTSDB3", " [~'na3'] - 
[~'na1']").put("serverWhereFilter",
-        "SERVER FILTER BY FIRST KEY ONLY"));
-  }
-
-  @Test
-  public void testSkipScanRanges() throws Exception {
-    verifyQuery("skipScanRanges",
-      "SELECT inst,host FROM ptsdb WHERE inst IN ('na1','na2','na3')"
-        + " AND host IN ('a','b') AND \"DATE\" >= to_date('2013-01-01')"
-        + " AND \"DATE\" < to_date('2013-01-02')",
-      text(
-        "CLIENT PARALLEL <N>-WAY SKIP SCAN ON 6 RANGES OVER PTSDB"
-          + " ['na1','a','2013-01-01'] - ['na3','b','2013-01-02']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
-      scanAttrs("SKIP SCAN ON 6 RANGES ", "PTSDB",
-        " ['na1','a','2013-01-01'] - 
['na3','b','2013-01-02']").put("serverWhereFilter",
-          "SERVER FILTER BY FIRST KEY ONLY"));
-  }
-
-  @Test
-  public void testFullScan() throws Exception {
-    verifyQuery("fullScan", "SELECT * FROM atable",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE"), scanAttrs("FULL 
SCAN ", "ATABLE", ""));
-  }
-
-  @Test
-  public void testReverseScan() throws Exception {
-    verifyQuery("reverseScan",
-      "SELECT inst,\"DATE\" FROM ptsdb2 WHERE inst = 'na1' ORDER BY inst DESC, 
\"DATE\" DESC",
-      text("CLIENT PARALLEL <N>-WAY REVERSE RANGE SCAN OVER PTSDB2 ['na1']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
-      scanAttrs("RANGE SCAN ", "PTSDB2", " ['na1']")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY")
-        .put("clientSortedBy", "REVERSE"));
-  }
-
-  @Test
-  public void testSmallHint() throws Exception {
-    verifyQuery("smallHint",
-      "SELECT /*+ SMALL */ host FROM ptsdb3 WHERE host IN ('na1','na2','na3')",
-      text("CLIENT PARALLEL <N>-WAY SMALL SKIP SCAN ON 3 KEYS OVER PTSDB3 
[~'na3'] - [~'na1']",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
-      scanAttrs("SKIP SCAN ON 3 KEYS ", "PTSDB3", " [~'na3'] - 
[~'na1']").put("hint", "SMALL")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY"));
-  }
-
-  @Test
-  public void testAggregateSingleRow() throws Exception {
-    verifyQuery("aggregateSingleRow", "SELECT count(*) FROM atable",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER 
BY FIRST KEY ONLY",
-        "    SERVER AGGREGATE INTO SINGLE ROW"),
-      scanAttrs("FULL SCAN ", "ATABLE", "")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY")
-        .put("serverAggregate", "SERVER AGGREGATE INTO SINGLE ROW"));
-  }
-
-  @Test
-  public void testAggregateOrderedDistinct() throws Exception {
-    verifyQuery("aggregateOrderedDistinct", "SELECT count(1) FROM atable GROUP 
BY a_string",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "    SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING]", "CLIENT MERGE 
SORT"),
-      scanAttrs("FULL SCAN ", "ATABLE", "")
-        .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY 
[A_STRING]")
-        .put("clientSortAlgo", "CLIENT MERGE SORT"));
-  }
-
-  @Test
-  public void testAggregateHashDistinct() throws Exception {
-    verifyQuery("aggregateHashDistinct",
-      "SELECT count(1) FROM atable WHERE a_integer = 1"
-        + " GROUP BY ROUND(a_time,'HOUR',2), entity_id",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER 
BY A_INTEGER = 1",
-        "    SERVER AGGREGATE INTO DISTINCT ROWS BY [ENTITY_ID, 
ROUND(A_TIME)]",
-        "CLIENT MERGE SORT"),
-      scanAttrs("FULL SCAN ", "ATABLE", "")
-        .put("serverWhereFilter", "SERVER FILTER BY A_INTEGER = 1")
-        .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY 
[ENTITY_ID, ROUND(A_TIME)]")
-        .put("clientSortAlgo", "CLIENT MERGE SORT"));
-  }
-
-  @Test
-  public void testTopNSortedBy() throws Exception {
-    verifyQuery("topNSortedBy", "SELECT a_string FROM atable ORDER BY a_string 
DESC LIMIT 3",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "    SERVER TOP 3 ROWS SORTED BY [A_STRING DESC]", "CLIENT MERGE 
SORT", "CLIENT LIMIT 3"),
-      scanAttrs("FULL SCAN ", "ATABLE", "").put("serverSortedBy", "[A_STRING 
DESC]")
-        .put("serverRowLimit", 3).put("clientRowLimit", 3)
-        .put("clientSortAlgo", "CLIENT MERGE SORT"));
-  }
-
-  @Test
-  public void testClientFilterByMax() throws Exception {
-    verifyQuery("clientFilterByMax",
-      "SELECT count(1) FROM atable GROUP BY a_string, b_string HAVING 
max(a_string) = 'a'",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "    SERVER AGGREGATE INTO DISTINCT ROWS BY [A_STRING, B_STRING]", 
"CLIENT MERGE SORT",
-        "CLIENT FILTER BY MAX(A_STRING) = 'a'"),
-      scanAttrs("FULL SCAN ", "ATABLE", "")
-        .put("serverAggregate", "SERVER AGGREGATE INTO DISTINCT ROWS BY 
[A_STRING, B_STRING]")
-        .put("clientFilterBy", "MAX(A_STRING) = 'a'").put("clientSortAlgo", 
"CLIENT MERGE SORT"));
-  }
-
-  @Test
-  public void testClientLimit() throws Exception {
-    verifyQuery("clientLimit",
-      "SELECT a_string, b_string FROM atable"
-        + " WHERE organization_id = '00D000000000001' AND entity_id != 
'00E00000000002'"
-        + " AND x_integer = 2 AND a_integer < 5 LIMIT 10",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
-        "    SERVER FILTER BY (ENTITY_ID != '00E00000000002' AND X_INTEGER = 2 
AND A_INTEGER < 5)",
-        "    SERVER 10 ROW LIMIT", "CLIENT 10 ROW LIMIT"),
-      scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']")
-        .put("serverWhereFilter",
-          "SERVER FILTER BY (ENTITY_ID != '00E00000000002' AND X_INTEGER = 2 
AND A_INTEGER < 5)")
-        .put("serverRowLimit", 10).put("clientRowLimit", 10));
-  }
-
-  @Test
-  public void testArrayElementProjection() throws Exception {
-    verifyQuery("arrayElementProjection", "SELECT a_string_array[1] FROM 
table_with_array",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER TABLE_WITH_ARRAY",
-        "    SERVER ARRAY ELEMENT PROJECTION"),
-      scanAttrs("FULL SCAN ", "TABLE_WITH_ARRAY", 
"").put("serverArrayElementProjection", true));
-  }
-
-  @Test
-  public void testSortMergeJoin() throws Exception {
-    ObjectNode rhs = scanAttrs("FULL SCAN ", "ATABLE", "");
-    verifyQuery("sortMergeJoin",
-      "SELECT /*+ USE_SORT_MERGE_JOIN */ a.a_string, b.a_string FROM atable a"
-        + " JOIN atable b ON a.organization_id = b.organization_id"
-        + " WHERE a.organization_id = '00D000000000001'",
-      text("SORT-MERGE-JOIN (INNER) TABLES",
-        "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']", "AND",
-        "    CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE"),
-      scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']")
-        .put("abstractExplainPlan", "SORT-MERGE-JOIN 
(INNER)").set("rhsJoinQueryExplainPlan", rhs));
-  }
-
-  @Test
-  public void testHashJoinInner() throws Exception {
-    verifyQuery("hashJoinInner",
-      "SELECT a.a_string, b.a_string FROM atable a"
-        + " JOIN atable b ON a.organization_id = b.organization_id"
-        + " WHERE a.organization_id = '00D000000000001'",
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
-        "    PARALLEL INNER-JOIN TABLE 0", "        CLIENT PARALLEL <N>-WAY 
FULL SCAN OVER ATABLE",
-        "    DYNAMIC SERVER FILTER BY A.ORGANIZATION_ID IN 
(B.ORGANIZATION_ID)"),
-      // HashJoinPlan uses the List<String>-only ExplainPlan constructor, 
which installs the
-      // default attributes (all-null/empty). Freeze that baseline.
-      defaultAttrs());
-  }
-
-  @Test
-  public void testHashJoinSemiInSubquery() throws Exception {
-    verifyQuery("hashJoinSemiInSubquery",
-      "SELECT a_string FROM atable"
-        + " WHERE organization_id IN (SELECT organization_id FROM atable WHERE 
a_integer = 1)",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    
SKIP-SCAN-JOIN TABLE 0",
-        "        CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "            SERVER FILTER BY A_INTEGER = 1",
-        "            SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY 
[ORGANIZATION_ID]",
-        "    DYNAMIC SERVER FILTER BY ATABLE.ORGANIZATION_ID IN ($1.$3)"),
-      defaultAttrs());
-  }
-
-  @Test
-  public void testUnionAll() throws Exception {
-    ObjectNode rhs = scanAttrs("RANGE SCAN ", "ATABLE", " 
['00D000000000002']");
-    verifyQuery("unionAll",
-      "SELECT a_string FROM atable WHERE organization_id = '00D000000000001'" 
+ " UNION ALL"
-        + " SELECT a_string FROM atable WHERE organization_id = 
'00D000000000002'",
-      text("UNION ALL OVER 2 QUERIES",
-        "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']",
-        "    CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000002']"),
-      scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']")
-        .put("abstractExplainPlan", "UNION ALL OVER 2 QUERIES")
-        .set("rhsJoinQueryExplainPlan", rhs));
-  }
-
-  @Test
-  public void testPutSingleRow() throws Exception {
-    verifyMutation("putSingleRow",
-      "UPSERT INTO atable (organization_id, entity_id, a_string)"
-        + " VALUES ('00D000000000001','00E00000000001','x')",
-      false, text("PUT SINGLE ROW"), defaultAttrs());
-  }
-
-  @Test
-  public void testUpsertSelectClient() throws Exception {
-    verifyMutation("upsertSelectClient",
-      "UPSERT INTO atable (organization_id, entity_id, a_string)"
-        + " SELECT organization_id, entity_id, a_string FROM atable"
-        + " WHERE organization_id = '00D000000000001'",
-      false,
-      text("UPSERT SELECT", "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']"),
-      scanAttrs("RANGE SCAN ", "ATABLE", " 
['00D000000000001']").put("abstractExplainPlan",
-        "UPSERT SELECT"));
-  }
-
-  @Test
-  public void testUpsertSelectServer() throws Exception {
-    verifyMutation("upsertSelectServer",
-      "UPSERT INTO atable (organization_id, entity_id, a_string)"
-        + " SELECT organization_id, entity_id, a_string FROM atable"
-        + " WHERE organization_id = '00D000000000001'",
-      true,
-      text("UPSERT ROWS", "CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['00D000000000001']"),
-      scanAttrs("RANGE SCAN ", "ATABLE", " 
['00D000000000001']").put("abstractExplainPlan",
-        "UPSERT ROWS"));
-  }
-
-  @Test
-  public void testDeleteSingleRow() throws Exception {
-    verifyMutation("deleteSingleRow", "DELETE FROM atable WHERE 
organization_id = '00D000000000001'"
-      + " AND entity_id = '00E00000000001'", true, text("DELETE SINGLE ROW"), 
defaultAttrs());
-  }
-
-  @Test
-  public void testDeleteServer() throws Exception {
-    verifyMutation("deleteServer", "DELETE FROM atable WHERE entity_id = 
'abc'", true,
-      text("DELETE ROWS SERVER SELECT", "CLIENT PARALLEL <N>-WAY FULL SCAN 
OVER ATABLE",
-        "    SERVER FILTER BY FIRST KEY ONLY AND ENTITY_ID = 'abc'"),
-      scanAttrs("FULL SCAN ", "ATABLE", "").put("abstractExplainPlan", "DELETE 
ROWS SERVER SELECT")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY AND 
ENTITY_ID = 'abc'"));
-  }
-
-  @Test
-  public void testDeleteClient() throws Exception {
-    verifyMutation("deleteClient", "DELETE FROM atable WHERE entity_id = 
'abc'", false,
-      text("DELETE ROWS CLIENT SELECT", "CLIENT PARALLEL <N>-WAY FULL SCAN 
OVER ATABLE",
-        "    SERVER FILTER BY FIRST KEY ONLY AND ENTITY_ID = 'abc'"),
-      scanAttrs("FULL SCAN ", "ATABLE", "").put("abstractExplainPlan", "DELETE 
ROWS CLIENT SELECT")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY AND 
ENTITY_ID = 'abc'"));
-  }
-
-  @Test
-  public void testSequenceNextValue() throws Exception {
-    verifyQuery("sequenceNextValue", "SELECT NEXT VALUE FOR " + SEQ + " FROM 
atable",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER 
BY FIRST KEY ONLY",
-        "CLIENT RESERVE VALUES FROM 1 SEQUENCE"),
-      scanAttrs("FULL SCAN ", "ATABLE", "")
-        .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY 
ONLY").put("clientSequenceCount", 1));
-  }
-
-  @Test
-  public void testSaltedTableScan() throws Exception {
-    verifyQuery("saltedTableScan", "SELECT * FROM " + SALTED + " WHERE v = 7",
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER EO_SALTED", "    SERVER 
FILTER BY V = 7",
-        "CLIENT MERGE SORT"),
-      scanAttrs("FULL SCAN ", "EO_SALTED", "").put("serverWhereFilter", 
"SERVER FILTER BY V = 7")
-        .put("clientSortAlgo", "CLIENT MERGE SORT"));
-  }
-
-  @Test
-  public void testMultiTenantView() throws Exception {
-    Properties tenantProps = PropertiesUtil.deepCopy(TEST_PROPERTIES);
-    tenantProps.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
-    tenantProps.setProperty(TENANT_ID_ATTRIB, TENANT_ID);
-    verifyQuery("multiTenantView", "SELECT * FROM " + MT_VIEW + " LIMIT 1", 
tenantProps,
-      text("CLIENT SERIAL <N>-WAY RANGE SCAN OVER EO_MT_BASE ['tenant42']",
-        "    SERVER 1 ROW LIMIT", "CLIENT 1 ROW LIMIT"),
-      attrs().put("iteratorTypeAndScanSize", "SERIAL 
<N>-WAY").put("consistency", "STRONG")
-        .put("explainScanType", "RANGE SCAN ").put("tableName", "EO_MT_BASE")
-        .put("keyRanges", " ['tenant42']").put("serverRowLimit", 
1).put("clientRowLimit", 1));
-  }
-
-  @Test
-  public void testTextNormalizerCollapsesWayCount() {
-    assertEquals(Collections.singletonList("CLIENT PARALLEL <N>-WAY FULL SCAN 
OVER ATABLE"),
-      new ExplainTextNormalizer()
-        .normalize(Arrays.asList("CLIENT PARALLEL 400-WAY FULL SCAN OVER 
ATABLE")));
-  }
-
-  @Test
-  public void testTextNormalizerCollapsesChunkCount() {
-    assertEquals(
-      Collections.singletonList("CLIENT <N>-CHUNK PARALLEL <N>-WAY FULL SCAN 
OVER ATABLE"),
-      new ExplainTextNormalizer()
-        .normalize(Arrays.asList("CLIENT 5-CHUNK PARALLEL 16-WAY FULL SCAN 
OVER ATABLE")));
-  }
-
-  @Test
-  public void testTextNormalizerStripsRowsBytes() {
-    assertEquals(
-      Collections.singletonList("CLIENT <N>-CHUNK PARALLEL <N>-WAY FULL SCAN 
OVER ATABLE"),
-      new ExplainTextNormalizer().normalize(
-        Arrays.asList("CLIENT 1-CHUNK 100 ROWS 2048 BYTES PARALLEL 1-WAY FULL 
SCAN OVER ATABLE")));
-  }
-
-  @Test
-  public void testTextNormalizerDropsRegionLocationsLine() {
-    assertEquals(
-      Arrays.asList("CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE",
-        "    SERVER FILTER BY FIRST KEY ONLY"),
-      new ExplainTextNormalizer().normalize(Arrays.asList(
-        "CLIENT PARALLEL 1-WAY FULL SCAN OVER ATABLE", "    SERVER FILTER BY 
FIRST KEY ONLY",
-        " (region locations = [{startKey=\\x00, endKey=, 
server=foo,1234}])")));
-  }
-
-  @Test
-  public void testTextNormalizerPreservesAllGrammar() {
-    List<String> in = Arrays.asList("CLIENT PARALLEL 1-WAY RANGE SCAN OVER 
ATABLE ['a','b']",
-      "    SERVER FILTER BY (X = 1 AND Y = 'z')",
-      "    SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [Y]", "CLIENT MERGE 
SORT",
-      "CLIENT 3 ROW LIMIT");
-    assertEquals(Arrays.asList("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE 
['a','b']",
-      "    SERVER FILTER BY (X = 1 AND Y = 'z')",
-      "    SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [Y]", "CLIENT MERGE 
SORT",
-      "CLIENT 3 ROW LIMIT"), new ExplainTextNormalizer().normalize(in));
-  }
-
-  @Test
-  public void testJsonNormalizerErasesClusterFields() {
-    ObjectNode root = mapper.createObjectNode();
-    root.put("iteratorTypeAndScanSize", "PARALLEL 16-WAY");
-    root.put("splitsChunk", 4);
-    root.put("estimatedRows", 1234L);
-    root.put("estimatedSizeInBytes", 9876L);
-    root.put("numRegionLocationLookups", 7);
-    root.set("regionLocations", mapper.createArrayNode().add("anything"));
-
-    new ExplainJsonNormalizer().normalize(root);
-
-    assertEquals("PARALLEL <N>-WAY", 
root.get("iteratorTypeAndScanSize").asText());
-    assertTrue(root.get("splitsChunk").isNull());
-    assertTrue(root.get("estimatedRows").isNull());
-    assertTrue(root.get("estimatedSizeInBytes").isNull());
-    assertTrue(root.get("regionLocations").isNull());
-    assertEquals(0, root.get("numRegionLocationLookups").asInt());
-  }
-
-  @Test
-  public void testJsonNormalizerRecursesIntoRhsJoinQueryExplainPlan() {
-    ObjectNode root = mapper.createObjectNode();
-    root.put("iteratorTypeAndScanSize", "PARALLEL 5-WAY");
-    root.put("numRegionLocationLookups", 1);
-    ObjectNode rhs = mapper.createObjectNode();
-    rhs.put("iteratorTypeAndScanSize", "PARALLEL 12-WAY");
-    rhs.put("numRegionLocationLookups", 99);
-    rhs.set("regionLocations", mapper.createArrayNode().add(1));
-    root.set("rhsJoinQueryExplainPlan", rhs);
-
-    new ExplainJsonNormalizer().normalize(root);
-
-    assertEquals("PARALLEL <N>-WAY", 
root.get("iteratorTypeAndScanSize").asText());
-    assertEquals(0, root.get("numRegionLocationLookups").asInt());
-    JsonNode nestedRhs = root.get("rhsJoinQueryExplainPlan");
-    assertEquals("PARALLEL <N>-WAY", 
nestedRhs.get("iteratorTypeAndScanSize").asText());
-    assertEquals(0, nestedRhs.get("numRegionLocationLookups").asInt());
-    assertTrue(nestedRhs.get("regionLocations").isNull());
-  }
-
-  @Test
-  public void testJacksonFieldOrderMatchesPropertyOrderAnnotation() throws 
Exception {
-    ExplainPlanAttributes a = new ExplainPlanAttributesBuilder()
-      .setIteratorTypeAndScanSize("PARALLEL 1-WAY").setTableName("T").build();
-    String json = mapper.writeValueAsString(a);
-    int iAbstract = json.indexOf("\"abstractExplainPlan\"");
-    int iIter = json.indexOf("\"iteratorTypeAndScanSize\"");
-    int iTable = json.indexOf("\"tableName\"");
-    int iRhs = json.indexOf("\"rhsJoinQueryExplainPlan\"");
-    int iMerge = json.indexOf("\"serverMergeColumns\"");
-    int iRegions = json.indexOf("\"regionLocations\"");
-    int iLookups = json.indexOf("\"numRegionLocationLookups\"");
-    assertTrue("abstractExplainPlan first", iAbstract >= 0 && iAbstract < 
iIter);
-    assertTrue("iteratorTypeAndScanSize before tableName", iIter < iTable);
-    assertTrue("rhsJoinQueryExplainPlan before serverMergeColumns", iRhs < 
iMerge);
-    assertTrue("serverMergeColumns before regionLocations", iMerge < iRegions);
-    assertTrue("regionLocations before numRegionLocationLookups", iRegions < 
iLookups);
-  }
-
-  @Test
-  public void testRegionLocationsSerializerRendersTriple() throws Exception {
-    HRegionLocation loc = new HRegionLocation(
-      RegionInfoBuilder.newBuilder(TableName.valueOf("FOO")).setStartKey(new 
byte[] { 0x01, 0x02 })
-        .setEndKey(new byte[] { 0x03, 0x04 }).build(),
-      ServerName.valueOf("rs.example.com", 16020, 1234567890L));
-    ExplainPlanAttributes a = new ExplainPlanAttributesBuilder()
-      
.setRegionLocations(Collections.singletonList(loc)).setNumRegionLocationLookups(1).build();
-    JsonNode tree = mapper.readTree(mapper.writeValueAsString(a));
-    JsonNode entry = tree.get("regionLocations").get(0);
-    assertEquals("\\x01\\x02", entry.get("startKey").asText());
-    assertEquals("\\x03\\x04", entry.get("endKey").asText());
-    assertTrue(entry.get("server").asText().contains("rs.example.com"));
-  }
-
-  @Test
-  public void testServerMergeColumnsSerializerEmitsSortedNames() throws 
Exception {
-    Set<PColumn> cols = new HashSet<>(
-      Arrays.asList(column("CF", "B_COL"), column("CF", "A_COL"), column("CF", 
"C_COL")));
-    ExplainPlanAttributes a =
-      new ExplainPlanAttributesBuilder().setServerMergeColumns(cols).build();
-    JsonNode tree = mapper.readTree(mapper.writeValueAsString(a));
-    JsonNode array = tree.get("serverMergeColumns");
-    assertEquals(3, array.size());
-    // PColumn.toString() uses QueryConstants.NAME_SEPARATOR (".") between 
family and name.
-    assertEquals("CF.A_COL", array.get(0).asText());
-    assertEquals("CF.B_COL", array.get(1).asText());
-    assertEquals("CF.C_COL", array.get(2).asText());
-  }
-
-  @Test
-  public void testIdentityRuleChainPasses() throws Exception {
-    ExplainPlan plan = samplePlan("PARALLEL 4-WAY", "RANGE SCAN ");
-    List<String> expectedText =
-      text("CLIENT PARALLEL <N>-WAY RANGE SCAN OVER T", "    SERVER FILTER BY 
FIRST KEY ONLY");
-    // samplePlan() sets only four attributes on the builder; consistency 
stays null.
-    ObjectNode expectedJson = defaultAttrs().put("iteratorTypeAndScanSize", 
"PARALLEL <N>-WAY")
-      .put("explainScanType", "RANGE SCAN ").put("tableName", "T")
-      .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY");
-    // Identity rule: returns inputs unchanged.
-    new ExplainOracle(Collections.singletonList(new ExplainChangeRule() {
-    })).verify("identity", plan, expectedText, expectedJson);
-  }
-
-  @Test
-  public void testChangeRuleRewritesText() throws Exception {
-    // Today's plan emits "SERVER FILTER BY FIRST KEY ONLY".
-    ExplainPlanAttributes todayAttrs = new ExplainPlanAttributesBuilder()
-      .setIteratorTypeAndScanSize("PARALLEL 1-WAY").setExplainScanType("FULL 
SCAN ")
-      .setTableName("T").setServerWhereFilter("SERVER FILTER BY FIRST KEY 
ONLY").build();
-    ExplainPlan todayPlan = new ExplainPlan(Arrays.asList("CLIENT PARALLEL 
1-WAY FULL SCAN OVER T",
-      "    SERVER FILTER BY FIRST KEY ONLY"), todayAttrs);
-    List<String> todayExpectedText =
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER T", "    SERVER FILTER BY 
FIRST KEY ONLY");
-    // samplePlan-style builder leaves consistency null and key-ranges unset.
-    ObjectNode todayExpectedJson = 
defaultAttrs().put("iteratorTypeAndScanSize", "PARALLEL <N>-WAY")
-      .put("explainScanType", "FULL SCAN ").put("tableName", "T")
-      .put("serverWhereFilter", "SERVER FILTER BY FIRST KEY ONLY");
-
-    // Sanity: with no rules, today's plan compares against today's expected.
-    new ExplainOracle().verify("today", todayPlan, todayExpectedText, 
todayExpectedJson);
-
-    // Tomorrow: design renames the marker. Future plan emits the new form; 
the embedded baseline
-    // stays unchanged ("today's" expected); a rule transforms the baseline 
into the new shape and
-    // the comparison passes.
-    ExplainPlanAttributes futureAttrs = new ExplainPlanAttributesBuilder()
-      .setIteratorTypeAndScanSize("PARALLEL 1-WAY").setExplainScanType("FULL 
SCAN ")
-      .setTableName("T").setServerWhereFilter("SERVER PROJECTION FILTER BY 
FIRST KEY ONLY").build();
-    ExplainPlan futurePlan = new ExplainPlan(Arrays.asList("CLIENT PARALLEL 
1-WAY FULL SCAN OVER T",
-      "    SERVER PROJECTION FILTER BY FIRST KEY ONLY"), futureAttrs);
-
-    ExplainChangeRule rename = new ExplainChangeRule() {
-      @Override
-      public List<String> applyText(String caseId, List<String> goldenText) {
-        List<String> out = new ArrayList<>(goldenText.size());
-        for (String s : goldenText) {
-          out.add(s.replace("SERVER FILTER BY FIRST KEY ONLY",
-            "SERVER PROJECTION FILTER BY FIRST KEY ONLY"));
-        }
-        return out;
-      }
-
-      @Override
-      public JsonNode applyJson(String caseId, JsonNode goldenJson) {
-        if (goldenJson.isObject()) {
-          ObjectNode obj = (ObjectNode) goldenJson;
-          JsonNode swf = obj.get("serverWhereFilter");
-          if (
-            swf != null && swf.isTextual() && swf.asText().equals("SERVER 
FILTER BY FIRST KEY ONLY")
-          ) {
-            obj.put("serverWhereFilter", "SERVER PROJECTION FILTER BY FIRST 
KEY ONLY");
-          }
-        }
-        return goldenJson;
-      }
-    };
-
-    new ExplainOracle(Collections.singletonList(rename)).verify("today", 
futurePlan,
-      todayExpectedText, todayExpectedJson);
-  }
-
-  @Test
-  public void testDiffMessageShowsExpectedAndActualForTextMismatch() {
-    ExplainPlan plan = samplePlan("PARALLEL 1-WAY", "FULL SCAN ");
-    // Caller's "expected" disagrees with what the plan actually emits.
-    List<String> divergentExpectedText =
-      text("CLIENT PARALLEL <N>-WAY FULL SCAN OVER T", "    SERVER FILTER BY 
(X = 9)");
-    ObjectNode divergentExpectedJson = defaultAttrs()
-      .put("iteratorTypeAndScanSize", "PARALLEL 
<N>-WAY").put("explainScanType", "FULL SCAN ")
-      .put("tableName", "T").put("serverWhereFilter", "SERVER FILTER BY (X = 
9)");
-    try {
-      new ExplainOracle().verify("x", plan, divergentExpectedText, 
divergentExpectedJson);
-      fail("Expected AssertionError for diverged plan");
-    } catch (AssertionError expected) {
-      String msg = expected.getMessage();
-      assertTrue(msg.contains("Text mismatch for case 'x'"));
-      assertTrue(msg.contains("SERVER FILTER BY FIRST KEY ONLY"));
-      assertTrue(msg.contains("SERVER FILTER BY (X = 9)"));
-    } catch (Exception e) {
-      fail("Unexpected exception type: " + e);
-    }
-  }
-
-  private void verifyQuery(String caseId, String query, List<String> 
expectedText,
-    JsonNode expectedJson) throws Exception {
-    verifyQuery(caseId, query, defaultProps(), expectedText, expectedJson);
-  }
-
-  private void verifyQuery(String caseId, String query, Properties props, 
List<String> expectedText,
-    JsonNode expectedJson) throws Exception {
-    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
-      ExplainPlan plan = 
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)
-        .optimizeQuery().getExplainPlan();
-      oracle.verify(caseId, plan, expectedText, expectedJson);
-    }
-  }
-
-  private void verifyMutation(String caseId, String query, boolean autoCommit,
-    List<String> expectedText, JsonNode expectedJson) throws Exception {
-    Properties props = defaultProps();
-    if (autoCommit) {
-      props.setProperty(AUTO_COMMIT_ATTRIB, "true");
-    }
-    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
-      ExplainPlan plan = compileMutation(conn, query);
-      oracle.verify(caseId, plan, expectedText, expectedJson);
-    }
-  }
-
-  private ExplainPlan compileMutation(Connection conn, String query) throws 
SQLException {
-    PhoenixPreparedStatement ps =
-      conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class);
-    return ps.compileMutation().getExplainPlan();
-  }
-
-  private static Properties defaultProps() {
-    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
-    props.setProperty(DATE_FORMAT_ATTRIB, "yyyy-MM-dd");
-    return props;
-  }
-
-  private static List<String> text(String... lines) {
-    return Arrays.asList(lines);
-  }
-
-  /**
-   * Returns a fresh {@link ObjectNode} populated with the JSON shape that
-   * {@link ExplainPlanAttributes#getDefaultExplainPlan()} serializes to 
(after normalization): all
-   * nullable fields are null, both booleans are false, and {@code 
numRegionLocationLookups} is 0.
-   * Each test case starts from this baseline and overrides only the fields it 
asserts on. Field
-   * order is irrelevant — {@link JsonNode#equals(Object)} compares maps, not 
order.
-   */
-  private static ObjectNode defaultAttrs() {
-    ObjectNode n = mapper.createObjectNode();
-    n.putNull("abstractExplainPlan");
-    n.putNull("splitsChunk");
-    n.putNull("estimatedRows");
-    n.putNull("estimatedSizeInBytes");
-    n.putNull("iteratorTypeAndScanSize");
-    n.putNull("samplingRate");
-    n.put("useRoundRobinIterator", false);
-    n.putNull("hexStringRVCOffset");
-    n.putNull("consistency");
-    n.putNull("hint");
-    n.putNull("serverSortedBy");
-    n.putNull("explainScanType");
-    n.putNull("tableName");
-    n.putNull("keyRanges");
-    n.putNull("scanTimeRangeMin");
-    n.putNull("scanTimeRangeMax");
-    n.putNull("serverWhereFilter");
-    n.putNull("serverDistinctFilter");
-    n.putNull("serverOffset");
-    n.putNull("serverRowLimit");
-    n.put("serverArrayElementProjection", false);
-    n.putNull("serverAggregate");
-    n.putNull("clientFilterBy");
-    n.putNull("clientAggregate");
-    n.putNull("clientSortedBy");
-    n.putNull("clientAfterAggregate");
-    n.putNull("clientDistinctFilter");
-    n.putNull("clientOffset");
-    n.putNull("clientRowLimit");
-    n.putNull("clientSequenceCount");
-    n.putNull("clientCursorName");
-    n.putNull("clientSortAlgo");
-    n.putNull("rhsJoinQueryExplainPlan");
-    n.putNull("serverMergeColumns");
-    n.putNull("regionLocations");
-    n.put("numRegionLocationLookups", 0);
-    return n;
-  }
-
-  /**
-   * Convenience wrapper that builds {@link #defaultAttrs()} and sets the five 
fields every
-   * connection-backed scan emits via {@code ExplainTable.explain}: {@code 
iteratorTypeAndScanSize},
-   * {@code consistency}, {@code explainScanType}, {@code tableName}, and 
{@code keyRanges}.
-   * @param scanType the {@code explainScanType} string (with its trailing 
space, e.g.
-   *                 {@code "FULL SCAN "})
-   * @param table    the {@code tableName} value
-   * @param keys     the {@code keyRanges} string (may be {@code null} or 
empty)
-   */
-  private static ObjectNode scanAttrs(String scanType, String table, String 
keys) {
-    ObjectNode n = defaultAttrs();
-    n.put("iteratorTypeAndScanSize", "PARALLEL <N>-WAY");
-    n.put("consistency", "STRONG");
-    n.put("explainScanType", scanType);
-    n.put("tableName", table);
-    if (keys != null) {
-      n.put("keyRanges", keys);
-    }
-    return n;
-  }
-
-  /** A plain {@link ObjectNode} alias for clarity in tests that don't use 
{@link #scanAttrs}. */
-  private static ObjectNode attrs() {
-    return defaultAttrs();
-  }
-
-  private static ExplainPlan samplePlan(String way, String scanType) {
-    ExplainPlanAttributes a = new 
ExplainPlanAttributesBuilder().setIteratorTypeAndScanSize(way)
-      .setExplainScanType(scanType).setTableName("T")
-      .setServerWhereFilter("SERVER FILTER BY FIRST KEY ONLY").build();
-    return new ExplainPlan(Arrays.asList("CLIENT " + way + " " + 
scanType.trim() + " OVER T",
-      "    SERVER FILTER BY FIRST KEY ONLY"), a);
-  }
-
-  private static PColumn column(String family, String name) {
-    PName fName = PNameFactory.newName(family);
-    PName cName = PNameFactory.newName(name);
-    return new PColumnImpl(cName, fName, PInteger.INSTANCE, null, null, false, 
0, SortOrder.ASC, 0,
-      null, false, "expression", false, false, name.getBytes(), 0L);
-  }
-}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainJsonNormalizer.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainJsonNormalizer.java
deleted file mode 100644
index 51144afd20..0000000000
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainJsonNormalizer.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.phoenix.query.explain;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.NullNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import java.util.regex.Pattern;
-
-/**
- * Elides cluster- and connection-specific fields from the JSON view of
- * {@code ExplainPlanAttributes} so the comparison is invariant under 
environment differences.
- */
-public final class ExplainJsonNormalizer {
-
-  private static final Pattern WAY_COUNT = Pattern.compile("\\b\\d+-WAY\\b");
-
-  /**
-   * Recursively normalize the given attributes-shaped JSON node.
-   * @return the same node, for fluent chaining.
-   */
-  public JsonNode normalize(JsonNode node) {
-    if (node == null || node.isNull() || !node.isObject()) {
-      return node;
-    }
-    ObjectNode obj = (ObjectNode) node;
-
-    if (obj.has("regionLocations")) {
-      obj.set("regionLocations", NullNode.getInstance());
-    }
-    if (obj.has("numRegionLocationLookups")) {
-      obj.put("numRegionLocationLookups", 0);
-    }
-    if (obj.has("splitsChunk")) {
-      obj.set("splitsChunk", NullNode.getInstance());
-    }
-    if (obj.has("estimatedRows")) {
-      obj.set("estimatedRows", NullNode.getInstance());
-    }
-    if (obj.has("estimatedSizeInBytes")) {
-      obj.set("estimatedSizeInBytes", NullNode.getInstance());
-    }
-
-    JsonNode iter = obj.get("iteratorTypeAndScanSize");
-    if (iter != null && iter.isTextual()) {
-      obj.put("iteratorTypeAndScanSize", 
WAY_COUNT.matcher(iter.asText()).replaceAll("<N>-WAY"));
-    }
-
-    JsonNode rhs = obj.get("rhsJoinQueryExplainPlan");
-    if (rhs != null && rhs.isObject()) {
-      normalize(rhs);
-    }
-
-    return obj;
-  }
-}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainOracle.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainOracle.java
deleted file mode 100644
index 42ab8a2876..0000000000
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainOracle.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.phoenix.query.explain;
-
-import static org.junit.Assert.fail;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.ObjectWriter;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import org.apache.phoenix.compile.ExplainPlan;
-import org.apache.phoenix.compile.ExplainPlanAttributes;
-
-/**
- * Backward compatibility test for Phoenix EXPLAIN output.
- * <p>
- * For each case, the test compares the current EXPLAIN output against an 
expected baseline supplied
- * by the caller (embedded directly in the test).
- * <p>
- * The {@link ExplainChangeRule} chain is applied to the expected side before 
comparison, so a
- * future EXPLAIN change appends one rule transforming the embedded baseline 
into its new expected
- * form.
- */
-public final class ExplainOracle {
-
-  private final List<ExplainChangeRule> rules;
-  private final ExplainTextNormalizer textNormalizer;
-  private final ExplainJsonNormalizer jsonNormalizer;
-  private final ObjectMapper mapper;
-  private final ObjectWriter prettyWriter;
-
-  public ExplainOracle() {
-    this(Collections.<ExplainChangeRule> emptyList());
-  }
-
-  public ExplainOracle(List<ExplainChangeRule> rules) {
-    this.rules = new ArrayList<>(rules);
-    this.textNormalizer = new ExplainTextNormalizer();
-    this.jsonNormalizer = new ExplainJsonNormalizer();
-    this.mapper = new ObjectMapper();
-    this.mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
-    this.mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, 
false);
-    this.mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
-    this.prettyWriter = mapper.writerWithDefaultPrettyPrinter();
-  }
-
-  /** Test-side {@link ObjectMapper} for building expected JSON */
-  public ObjectMapper mapper() {
-    return mapper;
-  }
-
-  /**
-   * Verify the given plan against the embedded expected baseline for {@code 
caseId}.
-   * @param caseId       the corpus case identifier (used in diff messages)
-   * @param plan         the plan under test
-   * @param expectedText the embedded expected, post-normalization plan-steps 
text
-   * @param expectedJson the embedded expected, post-normalization JSON 
attributes tree
-   */
-  public void verify(String caseId, ExplainPlan plan, List<String> 
expectedText,
-    JsonNode expectedJson) throws IOException {
-    List<String> textCurrent = textNormalizer.normalize(plan.getPlanSteps());
-    JsonNode jsonCurrent = 
serializeNormalized(plan.getPlanStepsAsAttributes());
-
-    List<String> textExpected = new ArrayList<>(expectedText);
-    JsonNode jsonExpected = expectedJson == null ? null : 
expectedJson.deepCopy();
-
-    for (ExplainChangeRule rule : rules) {
-      textExpected = rule.applyText(caseId, textExpected);
-      if (jsonExpected != null) {
-        jsonExpected = rule.applyJson(caseId, jsonExpected);
-      }
-    }
-
-    if (!textExpected.equals(textCurrent)) {
-      fail(textDiffMessage(caseId, textExpected, textCurrent));
-    }
-    if (jsonExpected != null && !jsonExpected.equals(jsonCurrent)) {
-      fail(jsonDiffMessage(caseId, jsonExpected, jsonCurrent));
-    }
-  }
-
-  /**
-   * Serialize the given attributes to JSON and apply the cluster/connection 
normalizer in one step.
-   * Exposed so the test can render and inspect the normalized JSON.
-   */
-  public JsonNode serializeNormalized(ExplainPlanAttributes attributes) throws 
IOException {
-    String raw = mapper.writeValueAsString(attributes);
-    JsonNode node = mapper.readTree(raw);
-    jsonNormalizer.normalize(node);
-    return node;
-  }
-
-  /** Normalize plan-steps text. Exposed so the test can render and inspect 
normalized output. */
-  public List<String> normalizeText(List<String> raw) {
-    return textNormalizer.normalize(raw);
-  }
-
-  /** Pretty-print a JSON node using the oracle's mapper config. */
-  public String prettyJson(JsonNode node) throws JsonProcessingException {
-    return prettyWriter.writeValueAsString(node);
-  }
-
-  private static String textDiffMessage(String caseId, List<String> expected, 
List<String> actual) {
-    StringBuilder sb = new StringBuilder();
-    sb.append("Text mismatch for case '").append(caseId).append("'.\n");
-    sb.append("--- expected (").append(expected.size()).append(" lines)\n");
-    for (String l : expected) {
-      sb.append("  ").append(l).append('\n');
-    }
-    sb.append("--- actual (").append(actual.size()).append(" lines)\n");
-    for (String l : actual) {
-      sb.append("  ").append(l).append('\n');
-    }
-    sb.append("--- line-by-line diff\n");
-    int n = Math.max(expected.size(), actual.size());
-    for (int i = 0; i < n; i++) {
-      String e = i < expected.size() ? expected.get(i) : "<missing>";
-      String a = i < actual.size() ? actual.get(i) : "<missing>";
-      if (!e.equals(a)) {
-        sb.append(" @").append(i).append(":\n");
-        sb.append("  - expected: ").append(e).append('\n');
-        sb.append("  - actual:   ").append(a).append('\n');
-      }
-    }
-    return sb.toString();
-  }
-
-  private String jsonDiffMessage(String caseId, JsonNode expected, JsonNode 
actual) {
-    StringBuilder sb = new StringBuilder();
-    sb.append("JSON mismatch for case '").append(caseId).append("'.\n");
-    try {
-      sb.append("--- expected\n").append(prettyJson(expected)).append('\n');
-      sb.append("--- actual\n").append(prettyJson(actual)).append('\n');
-    } catch (JsonProcessingException e) {
-      sb.append("(failed to pretty-print: 
").append(e.getMessage()).append(")\n");
-    }
-    sb.append("--- pointer diff\n");
-    appendPointerDiff(sb, "", expected, actual);
-    return sb.toString();
-  }
-
-  private static void appendPointerDiff(StringBuilder sb, String pointer, 
JsonNode expected,
-    JsonNode actual) {
-    if (expected == null && actual == null) {
-      return;
-    }
-    if (expected == null || actual == null) {
-      sb.append("  ").append(pointer.isEmpty() ? "/" : pointer).append(" 
expected=")
-        .append(expected).append(" actual=").append(actual).append('\n');
-      return;
-    }
-    if (expected.equals(actual)) {
-      return;
-    }
-    if (expected.isObject() && actual.isObject()) {
-      List<String> fields = new ArrayList<>();
-      expected.fieldNames().forEachRemaining(fields::add);
-      actual.fieldNames().forEachRemaining(f -> {
-        if (!fields.contains(f)) {
-          fields.add(f);
-        }
-      });
-      Collections.sort(fields);
-      for (String f : fields) {
-        appendPointerDiff(sb, pointer + "/" + f, expected.get(f), 
actual.get(f));
-      }
-      return;
-    }
-    if (expected.isArray() && actual.isArray()) {
-      int n = Math.max(expected.size(), actual.size());
-      for (int i = 0; i < n; i++) {
-        appendPointerDiff(sb, pointer + "/" + i, i < expected.size() ? 
expected.get(i) : null,
-          i < actual.size() ? actual.get(i) : null);
-      }
-      return;
-    }
-    sb.append("  ").append(pointer.isEmpty() ? "/" : pointer).append(" 
expected=").append(expected)
-      .append(" actual=").append(actual).append('\n');
-  }
-
-  /** Assemble an ExplainOracle with the given rule chain. */
-  public static ExplainOracle forRules(ExplainChangeRule... rules) {
-    return new ExplainOracle(Arrays.asList(rules));
-  }
-}
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainTextNormalizer.java
 
b/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainTextNormalizer.java
deleted file mode 100644
index 62b5c6854d..0000000000
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/query/explain/ExplainTextNormalizer.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.phoenix.query.explain;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Pattern;
-
-/**
- * Elides cluster- and connection-specific details from the {@code 
List<String>} returned by
- * {@code ExplainPlan.getPlanSteps()} so the EXPLAIN text can be compared 
across environments.
- */
-public final class ExplainTextNormalizer {
-
-  // CLIENT 5-CHUNK -> CLIENT <N>-CHUNK ; matches any non-negative integer 
immediately before
-  // -CHUNK.
-  private static final Pattern CHUNK_COUNT = 
Pattern.compile("\\b\\d+-CHUNK\\b");
-
-  // PARALLEL 400-WAY -> PARALLEL <N>-WAY ; matches the iterator parallelism 
count.
-  private static final Pattern WAY_COUNT = Pattern.compile("\\b\\d+-WAY\\b");
-
-  // 1234 ROWS 5678 BYTES (stats-row-count gated; we strip when present).
-  private static final Pattern ROWS_BYTES = Pattern.compile("\\d+ ROWS \\d+ 
BYTES\\s*");
-
-  // " (region locations = [...]) " emitted via 
planSteps.add(regionLocationPlan); the line always
-  // begins with the leading-space form of ExplainTable.REGION_LOCATIONS.
-  private static final String REGION_LOCATIONS_PREFIX = " (region locations = 
";
-
-  /**
-   * @param raw the result of {@code ExplainPlan.getPlanSteps()}
-   * @return a new list with cluster/connection-specific detail elided. The 
original list is not
-   *         mutated.
-   */
-  public List<String> normalize(List<String> raw) {
-    List<String> out = new ArrayList<>(raw.size());
-    for (String line : raw) {
-      if (line == null) {
-        out.add(null);
-        continue;
-      }
-      // Drop region-location lines outright
-      if (line.contains(REGION_LOCATIONS_PREFIX)) {
-        continue;
-      }
-      String normalized = line;
-      normalized = CHUNK_COUNT.matcher(normalized).replaceAll("<N>-CHUNK");
-      normalized = WAY_COUNT.matcher(normalized).replaceAll("<N>-WAY");
-      normalized = ROWS_BYTES.matcher(normalized).replaceAll("");
-      out.add(normalized);
-    }
-    return out;
-  }
-}

Reply via email to