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

apurtell pushed a commit to branch PHOENIX-7876-feature
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/PHOENIX-7876-feature by this 
push:
     new 6aa792b5b5 PHOENIX-7967 Contain explain diagnostic recording (#2582)
6aa792b5b5 is described below

commit 6aa792b5b57a7f677876fa7fc4e26046b9a51478
Author: Andrew Purtell <[email protected]>
AuthorDate: Mon Jul 27 14:29:31 2026 -0700

    PHOENIX-7967 Contain explain diagnostic recording (#2582)
---
 .../org/apache/phoenix/compile/HavingCompiler.java |  12 +-
 .../org/apache/phoenix/compile/JoinCompiler.java   |  12 +-
 .../apache/phoenix/compile/RVCOffsetCompiler.java  |   4 +-
 .../apache/phoenix/compile/StatementContext.java   | 153 ++++++++++++++++-----
 .../org/apache/phoenix/compile/WhereCompiler.java  |   5 +-
 .../org/apache/phoenix/jdbc/PhoenixStatement.java  |  68 ++++++---
 .../apache/phoenix/optimize/QueryOptimizer.java    | 100 +++++++++-----
 .../java/org/apache/phoenix/schema/PTableImpl.java |   4 +-
 .../java/org/apache/phoenix/end2end/InListIT.java  |  51 +++----
 .../apache/phoenix/compile/QueryOptimizerTest.java |   2 +
 .../phoenix/query/explain/ExplainPlanTest.java     |  74 +++++++++-
 .../phoenix/query/explain/ExplainPlanTestUtil.java | 114 ++++++++++++---
 12 files changed, 443 insertions(+), 156 deletions(-)

diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
index 01b3905820..164817311b 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/HavingCompiler.java
@@ -65,12 +65,14 @@ public class HavingCompiler {
         .buildException();
     }
     // Tag the residual HAVING predicate(s) with their origin for VERBOSE 
attribution.
-    if (expression instanceof org.apache.phoenix.expression.AndExpression) {
-      for (Expression child : expression.getChildren()) {
-        context.tagPredicate(child, "HAVING");
+    if (context.isCollectDiagnostics()) {
+      if (expression instanceof org.apache.phoenix.expression.AndExpression) {
+        for (Expression child : expression.getChildren()) {
+          context.tagPredicate(child, "HAVING");
+        }
+      } else {
+        context.tagPredicate(expression, "HAVING");
       }
-    } else {
-      context.tagPredicate(expression, "HAVING");
     }
     return expression;
   }
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
index 5beab671c8..9c6f71f0fd 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/JoinCompiler.java
@@ -676,11 +676,13 @@ public class JoinCompiler {
           right = CoerceExpression.create(right, toType, toSortOrder, 
right.getMaxLength());
         }
         // Tag the compiled ON predicates with their origin for VERBOSE 
attribution.
-        String decorrelatedAlias = 
lhsCtx.getDecorrelatedSubqueryAlias(condition);
-        String onOrigin =
-          decorrelatedAlias == null ? "JOIN ON" : "decorrelated from " + 
decorrelatedAlias;
-        lhsCtx.tagPredicate(left, onOrigin);
-        rhsCtx.tagPredicate(right, onOrigin);
+        if (lhsCtx.isCollectDiagnostics()) {
+          String decorrelatedAlias = 
lhsCtx.getDecorrelatedSubqueryAlias(condition);
+          String onOrigin =
+            decorrelatedAlias == null ? "JOIN ON" : "decorrelated from " + 
decorrelatedAlias;
+          lhsCtx.tagPredicate(left, onOrigin);
+          rhsCtx.tagPredicate(right, onOrigin);
+        }
         compiled.add(new Pair<Expression, Expression>(left, right));
       }
       // TODO PHOENIX-4618:
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
index abb3270017..4771dceb9f 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/RVCOffsetCompiler.java
@@ -181,7 +181,9 @@ public class RVCOffsetCompiler {
     }
 
     // Tag the RVC offset predicate with its origin for VERBOSE attribution.
-    context.tagPredicate(whereExpression, "RVC OFFSET");
+    if (context.isCollectDiagnostics()) {
+      context.tagPredicate(whereExpression, "RVC OFFSET");
+    }
 
     Expression expression;
     try {
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 6767035b3c..ba19803a51 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
@@ -119,6 +119,9 @@ public class StatementContext {
   private Map<String, List<Expression>> serverParsedProjections;
   private StatementContext parentContext;
   private ExplainOptions explainOptions = ExplainOptions.DEFAULT;
+  // True when compile-time diagnostic recording is enabled for this context
+  private final boolean collectDiagnostics;
+  // Diagnostic-only maps, lazily allocated
   private Map<Expression, Set<String>> predicateOrigins;
   private Map<ParseNode, String> decorrelatedSubqueryAlias;
   private Map<Hint, String> ignoredHints;
@@ -177,6 +180,7 @@ public class StatementContext {
     this.hasRawRowSizeFunction = context.hasRawRowSizeFunction;
     this.parentContext = context.parentContext;
     this.explainOptions = context.explainOptions;
+    this.collectDiagnostics = context.collectDiagnostics;
     copyRewriteStateFrom(context);
   }
 
@@ -242,15 +246,18 @@ public class StatementContext {
     this.subStatementContexts = Sets.newLinkedHashSet();
     this.appliedRewrites = new ArrayList<>();
     this.derivedTableFlattenCount = 0;
-    this.appliedIndexExpressionMatches = Maps.newLinkedHashMap();
-    this.appliedIndexExpressionPairs = Maps.newLinkedHashMap();
-    this.functionalIndexNames = Sets.newLinkedHashSet();
     this.partialIndexCheckedSet = Sets.newLinkedHashSet();
     this.serverParsedProjections = null;
     this.parentContext = null;
-    this.predicateOrigins = new IdentityHashMap<>();
-    this.decorrelatedSubqueryAlias = new IdentityHashMap<>();
-    this.ignoredHints = new EnumMap<>(Hint.class);
+    this.collectDiagnostics = statement.isCollectDiagnostics();
+    // Diagnostic-only collections are lazily allocated on first record under
+    // collectDiagnostics. Normal queries skip these allocations entirely.
+    this.predicateOrigins = null;
+    this.decorrelatedSubqueryAlias = null;
+    this.ignoredHints = null;
+    this.appliedIndexExpressionMatches = null;
+    this.appliedIndexExpressionPairs = null;
+    this.functionalIndexNames = null;
   }
 
   /**
@@ -521,9 +528,13 @@ public class StatementContext {
 
   /**
    * Records a top-of-plan rewrite breadcrumb (e.g. "STAR JOIN ON 2 RIGHT 
LEGS"). Breadcrumbs are
-   * diagnostic only and never affect the compiled plan.
+   * diagnostic only and never affect the compiled plan, so this is a no-op 
when diagnostic
+   * recording is disabled on this context. Normal queries pay no cost for 
breadcrumb bookkeeping.
    */
   public void addAppliedRewrite(String rewrite) {
+    if (!collectDiagnostics) {
+      return;
+    }
     appliedRewrites.add(rewrite);
   }
 
@@ -553,26 +564,56 @@ public class StatementContext {
     this.appliedRewrites =
       source.appliedRewrites == null ? new ArrayList<>() : new 
ArrayList<>(source.appliedRewrites);
     this.derivedTableFlattenCount = source.derivedTableFlattenCount;
-    this.appliedIndexExpressionMatches = Maps.newLinkedHashMap();
-    for (Map.Entry<String, List<String>> entry : 
source.appliedIndexExpressionMatches.entrySet()) {
-      this.appliedIndexExpressionMatches.put(entry.getKey(), new 
ArrayList<>(entry.getValue()));
-    }
-    this.appliedIndexExpressionPairs = Maps.newLinkedHashMap();
-    for (Map.Entry<String, Map<String, String>> entry : 
source.appliedIndexExpressionPairs
-      .entrySet()) {
-      this.appliedIndexExpressionPairs.put(entry.getKey(), new 
LinkedHashMap<>(entry.getValue()));
-    }
-    this.functionalIndexNames = 
Sets.newLinkedHashSet(source.functionalIndexNames);
     this.partialIndexCheckedSet = 
Sets.newLinkedHashSet(source.partialIndexCheckedSet);
     // serverParsedProjections is nullable and only ever replaced wholesale 
via its setter (never
     // mutated in place), so the reference can be carried over directly.
     this.serverParsedProjections = source.serverParsedProjections;
-    this.predicateOrigins = new IdentityHashMap<>();
-    for (Map.Entry<Expression, Set<String>> entry : 
source.predicateOrigins.entrySet()) {
-      this.predicateOrigins.put(entry.getKey(), new 
LinkedHashSet<>(entry.getValue()));
+    // Only copy if the source actually recorded anything. Keep null otherwise.
+    if (source.predicateOrigins == null || source.predicateOrigins.isEmpty()) {
+      this.predicateOrigins = null;
+    } else {
+      this.predicateOrigins = new IdentityHashMap<>();
+      for (Map.Entry<Expression, Set<String>> entry : 
source.predicateOrigins.entrySet()) {
+        this.predicateOrigins.put(entry.getKey(), new 
LinkedHashSet<>(entry.getValue()));
+      }
+    }
+    if (source.decorrelatedSubqueryAlias == null || 
source.decorrelatedSubqueryAlias.isEmpty()) {
+      this.decorrelatedSubqueryAlias = null;
+    } else {
+      this.decorrelatedSubqueryAlias = new 
IdentityHashMap<>(source.decorrelatedSubqueryAlias);
+    }
+    if (source.ignoredHints == null || source.ignoredHints.isEmpty()) {
+      this.ignoredHints = null;
+    } else {
+      this.ignoredHints = new EnumMap<>(source.ignoredHints);
+    }
+    if (
+      source.appliedIndexExpressionMatches == null || 
source.appliedIndexExpressionMatches.isEmpty()
+    ) {
+      this.appliedIndexExpressionMatches = null;
+    } else {
+      this.appliedIndexExpressionMatches = Maps.newLinkedHashMap();
+      for (Map.Entry<String, List<String>> entry : 
source.appliedIndexExpressionMatches
+        .entrySet()) {
+        this.appliedIndexExpressionMatches.put(entry.getKey(), new 
ArrayList<>(entry.getValue()));
+      }
+    }
+    if (
+      source.appliedIndexExpressionPairs == null || 
source.appliedIndexExpressionPairs.isEmpty()
+    ) {
+      this.appliedIndexExpressionPairs = null;
+    } else {
+      this.appliedIndexExpressionPairs = Maps.newLinkedHashMap();
+      for (Map.Entry<String, Map<String, String>> entry : 
source.appliedIndexExpressionPairs
+        .entrySet()) {
+        this.appliedIndexExpressionPairs.put(entry.getKey(), new 
LinkedHashMap<>(entry.getValue()));
+      }
+    }
+    if (source.functionalIndexNames == null || 
source.functionalIndexNames.isEmpty()) {
+      this.functionalIndexNames = null;
+    } else {
+      this.functionalIndexNames = 
Sets.newLinkedHashSet(source.functionalIndexNames);
     }
-    this.decorrelatedSubqueryAlias = new 
IdentityHashMap<>(source.decorrelatedSubqueryAlias);
-    this.ignoredHints = new EnumMap<>(source.ignoredHints);
   }
 
   public void incrementDerivedTableFlattenCount() {
@@ -587,13 +628,17 @@ public class StatementContext {
    * Records the path expressions that actually substituted against this query 
for the given
    * functional index. Used by the optimizer to label the chosen index's rule 
as
    * {@code matches <expr>} and to distinguish functional index rejections 
that matched no query
-   * expression. Deduplicated per index, preserving first seen order.
+   * expression. Deduplicated per index, preserving first seen order. No-op 
when the context has
+   * diagnostic recording disabled.
    */
   public void recordAppliedIndexExpressionMatches(String indexName,
     Collection<String> expressions) {
-    if (expressions == null || expressions.isEmpty()) {
+    if (!collectDiagnostics || expressions == null || expressions.isEmpty()) {
       return;
     }
+    if (appliedIndexExpressionMatches == null) {
+      appliedIndexExpressionMatches = Maps.newLinkedHashMap();
+    }
     List<String> matches =
       appliedIndexExpressionMatches.computeIfAbsent(indexName, k -> new 
ArrayList<>());
     for (String expression : expressions) {
@@ -608,6 +653,9 @@ public class StatementContext {
    * functional index, or an empty list if none were recorded.
    */
   public List<String> getAppliedIndexExpressionMatches(String indexName) {
+    if (appliedIndexExpressionMatches == null) {
+      return Collections.emptyList();
+    }
     List<String> matches = appliedIndexExpressionMatches.get(indexName);
     return matches == null ? Collections.emptyList() : matches;
   }
@@ -617,12 +665,15 @@ public class StatementContext {
    * actually fired against this query for the given functional index. Carries 
enough information
    * for the optimizer to emit one {@code REWRITE INDEX EXPRESSION <expr> AS 
<col>} breadcrumb per
    * applied substitution after the chosen plan is selected. Deduplicated per 
index, preserving
-   * first-seen insertion order.
+   * first-seen insertion order. No-op when the context has diagnostic 
recording disabled.
    */
   public void recordAppliedIndexExpressionPairs(String indexName, Map<String, 
String> pairs) {
-    if (pairs == null || pairs.isEmpty()) {
+    if (!collectDiagnostics || pairs == null || pairs.isEmpty()) {
       return;
     }
+    if (appliedIndexExpressionPairs == null) {
+      appliedIndexExpressionPairs = Maps.newLinkedHashMap();
+    }
     Map<String, String> existing =
       appliedIndexExpressionPairs.computeIfAbsent(indexName, k -> new 
LinkedHashMap<>());
     for (Map.Entry<String, String> entry : pairs.entrySet()) {
@@ -635,18 +686,30 @@ public class StatementContext {
    * substituted against this query for the given functional index, or an 
empty map if none.
    */
   public Map<String, String> getAppliedIndexExpressionPairs(String indexName) {
+    if (appliedIndexExpressionPairs == null) {
+      return Collections.emptyMap();
+    }
     Map<String, String> pairs = appliedIndexExpressionPairs.get(indexName);
     return pairs == null ? Collections.emptyMap() : pairs;
   }
 
-  /** Marks the given index as a functional index. */
+  /**
+   * Marks the given index as a functional index. No-op when the context has 
diagnostic recording
+   * disabled.
+   */
   public void recordFunctionalIndex(String indexName) {
+    if (!collectDiagnostics) {
+      return;
+    }
+    if (functionalIndexNames == null) {
+      functionalIndexNames = Sets.newLinkedHashSet();
+    }
     functionalIndexNames.add(indexName);
   }
 
   /** Returns whether the given index was recorded as a functional index 
during rewriting. */
   public boolean isFunctionalIndex(String indexName) {
-    return functionalIndexNames.contains(indexName);
+    return functionalIndexNames != null && 
functionalIndexNames.contains(indexName);
   }
 
   /**
@@ -695,6 +758,11 @@ public class StatementContext {
     return explainOptions != null && explainOptions.isVerbose();
   }
 
+  /** True when compile-time diagnostic recording is enabled for this context. 
*/
+  public boolean isCollectDiagnostics() {
+    return collectDiagnostics;
+  }
+
   /**
    * Tag a predicate {@link Expression} with the given origin (e.g. {@code 
"WHERE"},
    * {@code "JOIN ON"}). Tags accumulate as a set so a predicate fused from 
multiple sources carries
@@ -702,19 +770,25 @@ public class StatementContext {
    * expressions during compilation. Diagnostic only; never affects the 
compiled plan.
    */
   public void tagPredicate(Expression expression, String origin) {
-    if (expression == null || origin == null) {
+    if (!collectDiagnostics || expression == null || origin == null) {
       return;
     }
+    if (predicateOrigins == null) {
+      predicateOrigins = new IdentityHashMap<>();
+    }
     predicateOrigins.computeIfAbsent(expression, k -> new 
LinkedHashSet<>()).add(origin);
   }
 
   /** Returns the predicate origin tags accumulated during compilation. 
Identity keyed. */
   public Map<Expression, Set<String>> getPredicateOrigins() {
-    return predicateOrigins;
+    return predicateOrigins == null ? Collections.emptyMap() : 
predicateOrigins;
   }
 
   /** Returns the origin tags recorded for {@code expression}, or an empty set 
if none. */
   public Set<String> getPredicateOrigins(Expression expression) {
+    if (predicateOrigins == null) {
+      return Collections.emptySet();
+    }
     Set<String> tags = predicateOrigins.get(expression);
     return tags == null ? Collections.emptySet() : tags;
   }
@@ -724,9 +798,13 @@ public class StatementContext {
    * the given temp alias.
    */
   public void putDecorrelatedSubqueryAlias(ParseNode joinConditionNode, String 
subqueryAlias) {
-    if (joinConditionNode != null && subqueryAlias != null) {
-      decorrelatedSubqueryAlias.put(joinConditionNode, subqueryAlias);
+    if (!collectDiagnostics || joinConditionNode == null || subqueryAlias == 
null) {
+      return;
     }
+    if (decorrelatedSubqueryAlias == null) {
+      decorrelatedSubqueryAlias = new IdentityHashMap<>();
+    }
+    decorrelatedSubqueryAlias.put(joinConditionNode, subqueryAlias);
   }
 
   /**
@@ -734,7 +812,9 @@ public class StatementContext {
    * {@code null} if the node is not a decorrelated predicate.
    */
   public String getDecorrelatedSubqueryAlias(ParseNode joinConditionNode) {
-    return decorrelatedSubqueryAlias.get(joinConditionNode);
+    return decorrelatedSubqueryAlias == null
+      ? null
+      : decorrelatedSubqueryAlias.get(joinConditionNode);
   }
 
   /**
@@ -743,15 +823,18 @@ public class StatementContext {
    * comment. The first reason recorded for a hint wins. Diagnostic only.
    */
   public void recordIgnoredHint(Hint hint, String reason) {
-    if (hint == null || reason == null) {
+    if (!collectDiagnostics || hint == null || reason == null) {
       return;
     }
+    if (ignoredHints == null) {
+      ignoredHints = new EnumMap<>(Hint.class);
+    }
     ignoredHints.putIfAbsent(hint, reason);
   }
 
   /** Returns the hints the planner intentionally ignored, mapped to the 
reason. */
   public Map<Hint, String> getIgnoredHints() {
-    return ignoredHints;
+    return ignoredHints == null ? Collections.emptyMap() : ignoredHints;
   }
 
   /** Returns true if this is the top-level (root) statement context, i.e. it 
has no parent. */
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
index 4d4ac7717f..be788ce8b8 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/compile/WhereCompiler.java
@@ -265,10 +265,11 @@ public class WhereCompiler {
 
   /**
    * Tag the top-level conjuncts of the residual WHERE expression with their 
origin for VERBOSE
-   * predicate source attribution.
+   * predicate source attribution. No-op when the context has diagnostic 
recording disabled so
+   * normal queries skip the child iteration and per-conjunct tag calls 
entirely.
    */
   private static void tagWhereOrigins(StatementContext context, Expression 
expression) {
-    if (expression == null) {
+    if (expression == null || !context.isCollectDiagnostics()) {
       return;
     }
     if (expression instanceof AndExpression) {
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
index ba808630fd..f89bdb2bf1 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/jdbc/PhoenixStatement.java
@@ -307,6 +307,10 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
   protected final Calendar localCalendar = Calendar.getInstance();
   private boolean validateLastDdlTimestamp;
   private long sqlQueryParsingTime = 0;
+  // Flipped on by ExecutableExplainStatement.compilePlan (and read by 
StatementContext) so that
+  // compile-time diagnostic recording only runs when we are actually 
producing an EXPLAIN.
+  // Normal queries pay no cost for these recording sites.
+  private boolean collectDiagnostics = false;
 
   public PhoenixStatement(PhoenixConnection connection) {
     this.connection = connection;
@@ -335,6 +339,18 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
     return this.sqlQueryParsingTime;
   }
 
+  /**
+   * True when compile time diagnostic recording is enabled for this 
statement. Off by default.
+   * Normal queries pay no recording cost.
+   */
+  public boolean isCollectDiagnostics() {
+    return collectDiagnostics;
+  }
+
+  public void setCollectDiagnostics(boolean value) {
+    this.collectDiagnostics = value;
+  }
+
   public PhoenixResultSet newResultSet(ResultIterator iterator, RowProjector 
projector,
     StatementContext context) throws SQLException {
     return new PhoenixResultSet(iterator, projector, context);
@@ -974,28 +990,38 @@ public class PhoenixStatement implements 
PhoenixMonitoredStatement, SQLCloseable
     @Override
     public QueryPlan compilePlan(PhoenixStatement stmt, Sequence.ValueOp 
seqAction)
       throws SQLException {
-      CompilableStatement compilableStmt = getStatement();
-      StatementPlan compilePlan =
-        compilableStmt.compilePlan(stmt, Sequence.ValueOp.VALIDATE_SEQUENCE);
-      // if client is validating timestamps, ensure its metadata cache is up 
to date.
-      if 
(ValidateLastDDLTimestampUtil.getValidateLastDdlTimestampEnabled(stmt.getConnection()))
 {
-        Set<TableRef> tableRefs = compilePlan.getSourceRefs();
-        for (TableRef tableRef : tableRefs) {
-          new 
MetaDataClient(stmt.getConnection()).updateCache(stmt.getConnection().getTenantId(),
-            tableRef.getTable().getSchemaName().getString(),
-            tableRef.getTable().getTableName().getString(), true);
-        }
-        compilePlan = compilableStmt.compilePlan(stmt, 
Sequence.ValueOp.VALIDATE_SEQUENCE);
-      }
-      // For a QueryPlan, we need to get its optimized plan; for a 
MutationPlan, its enclosed
-      // QueryPlan
-      // has already been optimized during compilation.
-      if (compilePlan instanceof QueryPlan) {
-        QueryPlan dataPlan = (QueryPlan) compilePlan;
-        compilePlan =
-          
stmt.getConnection().getQueryServices().getOptimizer().optimize(stmt, dataPlan);
+      // Enable diagnostic recording only around the inner compile/optimize so 
that
+      // EXPLAIN sees full origin tags, optimizer decisions, and 
rejected-index reasons.
+      // try/finally scopes the flag to this compile so nested compiles (e.g. 
a partial-index
+      // predicate check) do not leak the flag beyond the EXPLAIN.
+      final boolean prevCollectDiagnostics = stmt.isCollectDiagnostics();
+      stmt.setCollectDiagnostics(true);
+      final StatementPlan plan;
+      try {
+        CompilableStatement compilableStmt = getStatement();
+        StatementPlan compilePlan =
+          compilableStmt.compilePlan(stmt, Sequence.ValueOp.VALIDATE_SEQUENCE);
+        // if client is validating timestamps, ensure its metadata cache is up 
to date.
+        if 
(ValidateLastDDLTimestampUtil.getValidateLastDdlTimestampEnabled(stmt.getConnection()))
 {
+          Set<TableRef> tableRefs = compilePlan.getSourceRefs();
+          for (TableRef tableRef : tableRefs) {
+            new 
MetaDataClient(stmt.getConnection()).updateCache(stmt.getConnection().getTenantId(),
+              tableRef.getTable().getSchemaName().getString(),
+              tableRef.getTable().getTableName().getString(), true);
+          }
+          compilePlan = compilableStmt.compilePlan(stmt, 
Sequence.ValueOp.VALIDATE_SEQUENCE);
+        }
+        // For a QueryPlan we need to get its optimized plan. For a 
MutationPlan its enclosed
+        // QueryPlan has already been optimized during compilation.
+        if (compilePlan instanceof QueryPlan) {
+          QueryPlan dataPlan = (QueryPlan) compilePlan;
+          compilePlan =
+            
stmt.getConnection().getQueryServices().getOptimizer().optimize(stmt, dataPlan);
+        }
+        plan = compilePlan;
+      } finally {
+        stmt.setCollectDiagnostics(prevCollectDiagnostics);
       }
-      final StatementPlan plan = compilePlan;
       // Propagate the parsed EXPLAIN options onto the resolved plan's context 
so the renderer has
       // access to the requested options.
       if (plan.getContext() != null) {
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
 
b/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
index 04d25dfd13..0300227a7d 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/optimize/QueryOptimizer.java
@@ -157,7 +157,8 @@ public class QueryOptimizer {
         && (select.getWhere() == null || !select.getWhere().hasSubquery())
     ) {
       return getApplicablePlansForSingleFlatQuery(dataPlan, statement, 
targetColumns,
-        parallelIteratorFactory, stopAtBestPlan, new DecisionState());
+        parallelIteratorFactory, stopAtBestPlan,
+        new DecisionState(dataPlan.getContext().isCollectDiagnostics()));
     }
 
     Map<TableRef, QueryPlan> dataPlans = null;
@@ -223,17 +224,19 @@ public class QueryOptimizer {
       
WhereCompiler.contains(index.getIndexWhereExpression(dataPlan.getContext().getConnection()),
         WhereCompiler.transformDNF(select.getWhere(), context));
     StatementContext rootContext = dataPlan.getContext();
-    String tableName = dataPlan.getTableRef().getTable().getName().getString();
-    String indexName = index.getName().getString();
-    // Dedupe so the breadcrumb is recorded once per table and index pair even 
when the optimizer
-    // scores the same index across multiple candidate paths.
-    if (rootContext.markPartialIndexChecked(tableName, indexName)) {
-      // Name the index in the breadcrumb so multiple partial indexes stay 
distinct after the
-      // rewrite list is deduped by string. Otherwise they collapse into a 
single ambiguous line.
-      rootContext.addAppliedRewrite(usable
-        ? "PARTIAL INDEX " + indexName + " APPLICABLE"
-        : "PARTIAL INDEX " + indexName
-          + " NOT APPLICABLE -- index WHERE not implied by query WHERE");
+    if (rootContext.isCollectDiagnostics()) {
+      String tableName = 
dataPlan.getTableRef().getTable().getName().getString();
+      String indexName = index.getName().getString();
+      // Dedupe so the breadcrumb is recorded once per table and index pair 
even when the optimizer
+      // scores the same index across multiple candidate paths.
+      if (rootContext.markPartialIndexChecked(tableName, indexName)) {
+        // Name the index in the breadcrumb so multiple partial indexes stay 
distinct after the
+        // rewrite list is deduped by string. Otherwise they collapse into a 
single ambiguous line.
+        rootContext.addAppliedRewrite(usable
+          ? "PARTIAL INDEX " + indexName + " APPLICABLE"
+          : "PARTIAL INDEX " + indexName
+            + " NOT APPLICABLE -- index WHERE not implied by query WHERE");
+      }
     }
     return usable;
   }
@@ -404,8 +407,13 @@ public class QueryOptimizer {
    * Carry the rewrite breadcrumbs recorded on the data plan's context onto 
the candidate plans'
    * contexts. The partial index applicability decisions are recorded once per 
index on the shared
    * root context while scoring plans, so that all of them stay visible even 
though only one wins.
+   * No-op when diagnostic recording is disabled on the source context, so 
normal queries pay no
+   * cost.
    */
   private static void carryForwardRewrites(StatementContext from, 
List<QueryPlan> plans) {
+    if (!from.isCollectDiagnostics()) {
+      return;
+    }
     List<String> rewrites = from.getAppliedRewrites();
     Map<Hint, String> ignoredHints = from.getIgnoredHints();
     if (rewrites.isEmpty() && (ignoredHints == null || 
ignoredHints.isEmpty())) {
@@ -557,14 +565,16 @@ public class QueryOptimizer {
           ParseNodeRewriter.rewrite(indexSelect, indexExpressionRewriter);
         // Record which functional index expressions actually matched this 
query, so the
         // optimizer can label the chosen index's rule and distinguish 
functional index rejections
-        // that matched nothing.
-        if (indexExpressionRewriter.hasFunctionalColumns()) {
-          
dataPlan.getContext().recordFunctionalIndex(index.getTableName().getString());
+        if (dataPlan.getContext().isCollectDiagnostics()) {
+          if (indexExpressionRewriter.hasFunctionalColumns()) {
+            
dataPlan.getContext().recordFunctionalIndex(index.getTableName().getString());
+          }
+          dataPlan.getContext().recordAppliedIndexExpressionMatches(
+            index.getTableName().getString(),
+            
indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
+          
dataPlan.getContext().recordAppliedIndexExpressionPairs(index.getTableName().getString(),
+            indexExpressionRewriter.getAppliedFunctionalSubstitutions());
         }
-        
dataPlan.getContext().recordAppliedIndexExpressionMatches(index.getTableName().getString(),
-          
indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
-        
dataPlan.getContext().recordAppliedIndexExpressionPairs(index.getTableName().getString(),
-          indexExpressionRewriter.getAppliedFunctionalSubstitutions());
         QueryCompiler compiler = new QueryCompiler(statement, 
rewrittenIndexSelect, resolver,
           targetColumns, parallelIteratorFactory, 
dataPlan.getContext().getSequenceManager(),
           isProjected, true, 
dataPlans).withRewriteContext(dataPlan.getContext());
@@ -924,15 +934,28 @@ public class QueryOptimizer {
 
   /**
    * Accumulates the rejected index entries gathered while selecting an index 
for a single flat
-   * query. Rejections are deduplicated by index name.
+   * query. Rejections are deduplicated by index name. When diagnostic 
recording is disabled every
+   * {@code reject(...)} call is a no-op and the backing list is never 
allocated.
    */
   private static final class DecisionState {
-    private final List<RejectedIndexEntry> rejections = new ArrayList<>();
+    private final boolean collectDiagnostics;
+    private List<RejectedIndexEntry> rejections;
+
+    DecisionState(boolean collectDiagnostics) {
+      this.collectDiagnostics = collectDiagnostics;
+    }
 
     void reject(String name, String reason) {
-      for (RejectedIndexEntry existing : rejections) {
-        if (existing.getName().equals(name)) {
-          return;
+      if (!collectDiagnostics) {
+        return;
+      }
+      if (rejections == null) {
+        rejections = new ArrayList<>();
+      } else {
+        for (RejectedIndexEntry existing : rejections) {
+          if (existing.getName().equals(name)) {
+            return;
+          }
         }
       }
       rejections.add(new RejectedIndexEntry(name, reason));
@@ -949,6 +972,9 @@ public class QueryOptimizer {
     }
 
     void rejectAll(List<PTable> indexes, String reason) {
+      if (!collectDiagnostics) {
+        return;
+      }
       for (PTable index : indexes) {
         reject(index, reason);
       }
@@ -961,9 +987,13 @@ public class QueryOptimizer {
 
   /**
    * Records the optimizer's index selection rationale on the chosen plan and 
returns it so callers
-   * can use this inline at a {@code return} site.
+   * can use this inline at a {@code return} site. No-op (returns the winner 
unchanged) when the
+   * winner's context has diagnostic recording disabled.
    */
   private static QueryPlan recordDecision(QueryPlan winner, String rule, 
DecisionState state) {
+    if (!winner.getContext().isCollectDiagnostics()) {
+      return winner;
+    }
     // The rule names the selection reason (e.g. hint, more bound PK columns). 
When the winner is a
     // functional index that matched a query expression, the "matches <expr>" 
disclosure is
     // recorded separately so both the selection reason and the functional 
match are surfaced.
@@ -1163,16 +1193,18 @@ public class QueryOptimizer {
       indexSelect = ParseNodeRewriter.rewrite(indexSelect, 
indexExpressionRewriter);
       // Surface which functional index expressions actually matched so the 
optimizer can label
       // the chosen index's rule and distinguish functional index rejections 
that matched nothing.
-      if (indexExpressionRewriter.hasFunctionalColumns()) {
-        breadcrumbContext
-          
.recordFunctionalIndex(indexTableRef.getTable().getTableName().getString());
+      if (breadcrumbContext.isCollectDiagnostics()) {
+        if (indexExpressionRewriter.hasFunctionalColumns()) {
+          breadcrumbContext
+            
.recordFunctionalIndex(indexTableRef.getTable().getTableName().getString());
+        }
+        breadcrumbContext.recordAppliedIndexExpressionMatches(
+          indexTableRef.getTable().getTableName().getString(),
+          
indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
+        breadcrumbContext.recordAppliedIndexExpressionPairs(
+          indexTableRef.getTable().getTableName().getString(),
+          indexExpressionRewriter.getAppliedFunctionalSubstitutions());
       }
-      breadcrumbContext.recordAppliedIndexExpressionMatches(
-        indexTableRef.getTable().getTableName().getString(),
-        indexExpressionRewriter.getAppliedFunctionalSubstitutions().values());
-      breadcrumbContext.recordAppliedIndexExpressionPairs(
-        indexTableRef.getTable().getTableName().getString(),
-        indexExpressionRewriter.getAppliedFunctionalSubstitutions());
     }
 
     return indexSelect;
diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java
index 89f6fc7ad7..c1c1f13c7d 100644
--- 
a/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java
+++ 
b/phoenix-core-client/src/main/java/org/apache/phoenix/schema/PTableImpl.java
@@ -2538,7 +2538,9 @@ public class PTableImpl implements PTable {
     
plan.getContext().setResolver(FromCompiler.getResolver(plan.getTableRef()));
     indexWhereExpression = transformDNF(where, plan.getContext());
     // Tag the partial-index WHERE predicate with its origin for VERBOSE 
attribution.
-    plan.getContext().tagPredicate(indexWhereExpression, "INDEX WHERE");
+    if (plan.getContext().isCollectDiagnostics()) {
+      plan.getContext().tagPredicate(indexWhereExpression, "INDEX WHERE");
+    }
     indexWhereColumns =
       
Sets.newHashSetWithExpectedSize(plan.getContext().getWhereConditionColumns().size());
     for (Pair<byte[], byte[]> column : 
plan.getContext().getWhereConditionColumns()) {
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java
index 8b3541e73c..9d8aaf0520 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/InListIT.java
@@ -20,6 +20,7 @@ package org.apache.phoenix.end2end;
 import static java.util.Collections.singletonList;
 import static 
org.apache.phoenix.query.QueryServices.USE_BLOOMFILTER_FOR_MULTIKEY_POINTLOOKUP;
 import static org.apache.phoenix.query.explain.ExplainPlanTestUtil.assertPlan;
+import static 
org.apache.phoenix.query.explain.ExplainPlanTestUtil.getOptimizedQueryPlan;
 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;
@@ -1024,14 +1025,14 @@ public class InListIT extends ParallelStatsDisabledIT {
 
         try (PreparedStatement preparedStmt = 
viewConn.prepareStatement("SELECT * FROM "
           + tenantView + " WHERE (ID1, ID2) " + "IN (('005xx000001Sv6o', 
'000000000000500'))")) {
-          assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+          assertPlan(getOptimizedQueryPlan(preparedStmt))
             .scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
             .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
         }
 
         try (PreparedStatement preparedStmt = 
viewConn.prepareStatement("SELECT * FROM "
           + tenantView + " WHERE (ID2, ID1) " + "IN (('000000000000500', 
'005xx000001Sv6o'))")) {
-          assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+          assertPlan(getOptimizedQueryPlan(preparedStmt))
             .scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
             .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
         }
@@ -1067,7 +1068,7 @@ public class InListIT extends ParallelStatsDisabledIT {
         try (PreparedStatement preparedStmt = 
viewConn.prepareStatement("SELECT * FROM "
           + tenantView + " WHERE (ID1, ID2) " + "IN (('005xx000001Sv6o', 
'000000000000500'),"
           + "('bar', '000000000000400')," + "('foo', '000000000000300'))")) {
-          assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+          assertPlan(getOptimizedQueryPlan(preparedStmt))
             .scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
             .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
         }
@@ -1075,7 +1076,7 @@ public class InListIT extends ParallelStatsDisabledIT {
         try (PreparedStatement preparedStmt =
           viewConn.prepareStatement("SELECT * FROM " + tenantView + " WHERE 
(ID2, ID1) IN "
             + "(('bar', '005xx000001Sv6o')," + "('foo', '005xx000001Sv6o'))")) 
{
-          assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+          assertPlan(getOptimizedQueryPlan(preparedStmt))
             .scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
             .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
         }
@@ -1115,7 +1116,7 @@ public class InListIT extends ParallelStatsDisabledIT {
           viewConn.prepareStatement("SELECT * FROM " + tenantView + " WHERE 
(ID1, ID2) IN "
             + "(('005xx000001Sv6o', '000000000000500')," + 
"('005xx000001Sv6o', '000000000000400'),"
             + "('005xx000001Sv6o', '000000000000300'))")) {
-          assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+          assertPlan(getOptimizedQueryPlan(preparedStmt))
             .scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
             .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
         }
@@ -1123,7 +1124,7 @@ public class InListIT extends ParallelStatsDisabledIT {
         try (PreparedStatement preparedStmt = 
viewConn.prepareStatement("SELECT * FROM "
           + tenantView + " WHERE (ID2, ID1) IN " + "(('000000000000400', 
'005xx000001Sv6o'),"
           + "('000000000000300', '005xx000001Sv6o'))")) {
-          assertPlan(PhoenixRuntime.getOptimizedQueryPlan(preparedStmt))
+          assertPlan(getOptimizedQueryPlan(preparedStmt))
             .scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
             .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
         }
@@ -1164,14 +1165,14 @@ public class InListIT extends ParallelStatsDisabledIT {
       PreparedStatement preparedStmt =
         viewConn.prepareStatement("SELECT * FROM " + tenantView + " WHERE 
(ID1, ID2) IN "
           + "(('005xx000001Sv6o', '000000000000500')," + "('005xx000001Sv6o', 
'000000000000400'))");
-      QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertEquals(numberOfRowsToScan, queryPlan.getEstimatedRowsToScan());
       
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
         .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
 
       viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID1, 
ID2) IN "
         + "(('005xx000001Sv6o', '000000000000500')," + "('005xx000001Sv6o', 
'000000000000400'))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertEquals(numberOfRowsToScan, queryPlan.getEstimatedRowsToScan());
       
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
         .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
@@ -1182,25 +1183,25 @@ public class InListIT extends ParallelStatsDisabledIT {
     try (Connection viewConn = 
DriverManager.getConnection(TENANT_SPECIFIC_URL1)) {
       PreparedStatement preparedStmt = viewConn.prepareStatement("SELECT * 
FROM " + tenantView
         + " WHERE (ID1) IN " + "(('005xx000001Sv6o')," + 
"('005xx000001Sv6o'))");
-      QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID1) IN 
"
         + "(('005xx000001Sv6o')," + "('005xx000001Sv6o'))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       preparedStmt = viewConn.prepareStatement("SELECT * FROM " + tenantView + 
" WHERE (ID2) IN "
         + "(('000000000000500')," + "('000000000000400'))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID2) IN 
"
         + "(('000000000000500')," + "('000000000000400'))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
     }
@@ -1210,25 +1211,25 @@ public class InListIT extends ParallelStatsDisabledIT {
     try (Connection viewConn = 
DriverManager.getConnection(TENANT_SPECIFIC_URL1)) {
       PreparedStatement preparedStmt = viewConn.prepareStatement("SELECT * 
FROM " + tenantView
         + " WHERE (ID1, ID3) IN " + "(('005xx000001Sv6o', 1)," + 
"('005xx000001Sv6o', 2))");
-      QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID1, 
ID3) IN "
         + "(('005xx000001Sv6o', 1)," + "('005xx000001Sv6o', 2))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       preparedStmt = viewConn.prepareStatement("SELECT * FROM " + tenantView
         + " WHERE (ID2, ID3) IN " + "(('000000000000500', 1)," + 
"('000000000000400', 2))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       viewConn.prepareStatement("DELETE FROM " + tenantView + " WHERE (ID2, 
ID3) IN "
         + "(('000000000000500', 1)," + "('000000000000400', 2))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
     }
@@ -1239,13 +1240,13 @@ public class InListIT extends ParallelStatsDisabledIT {
     try (Connection viewConn = 
DriverManager.getConnection(TENANT_SPECIFIC_URL1)) {
       PreparedStatement preparedStmt = viewConn.prepareStatement(
         "SELECT * FROM " + tenantView + " WHERE (ID3, ID4) IN " + "((1, 1)," + 
"(2, 2))");
-      QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
 
       viewConn.prepareStatement(
         "DELETE FROM " + tenantView + " WHERE (ID3, ID4) IN " + "((1, 1)," + 
"(2, 2))");
-      queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      queryPlan = getOptimizedQueryPlan(preparedStmt);
       assertPlan(queryPlan).iteratorType("PARALLEL 1-WAY").scanType("RANGE 
SCAN")
         .indexRule(OptimizerReasons.RULE_DATA_TABLE).indexRejectedNone();
     }
@@ -1747,21 +1748,21 @@ public class InListIT extends ParallelStatsDisabledIT {
         // TESTING for optimized scan
         PreparedStatement preparedStmt = conn.prepareStatement(
           "SELECT * FROM " + view + " WHERE (ID1, ID2) IN " + "((1, 1)," + 
"(2, 2))");
-        QueryPlan queryPlan = 
PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+        QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
         assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
         
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
           .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
 
         preparedStmt =
           conn.prepareStatement("SELECT * FROM " + view + " WHERE (ID2, ID1) 
IN ((1, 1),(2, 2))");
-        queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+        queryPlan = getOptimizedQueryPlan(preparedStmt);
         assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
         
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
           .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
 
         preparedStmt =
           conn.prepareStatement("SELECT * FROM " + view + " WHERE (ID2, ID1) 
IN ((1, 1),(2, 2))");
-        queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+        queryPlan = getOptimizedQueryPlan(preparedStmt);
         assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
         
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
           .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
@@ -1809,20 +1810,20 @@ public class InListIT extends ParallelStatsDisabledIT {
         // TESTING for optimized scan
         PreparedStatement preparedStmt = conn.prepareStatement(
           "SELECT * FROM " + view + " WHERE (ID4, ID2) IN " + "((1, 1)," + 
"(2, 2))");
-        QueryPlan queryPlan = 
PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+        QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
         assertPlan(queryPlan).scanType("RANGE 
SCAN").indexRule(OptimizerReasons.RULE_ONLY_CANDIDATE)
           .indexRejectedCount(1);
 
         preparedStmt = conn.prepareStatement(
           "SELECT ID1,ID5 FROM " + view + " WHERE (ID1, ID2) IN " + "((1, 
1),(2, 2))");
-        queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+        queryPlan = getOptimizedQueryPlan(preparedStmt);
         assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
         
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
           .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
 
         preparedStmt =
           conn.prepareStatement("SELECT * FROM " + view + " WHERE (ID2, ID1) 
IN ((1, 1),(2, 2))");
-        queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+        queryPlan = getOptimizedQueryPlan(preparedStmt);
         assertEquals(new Long(2), queryPlan.getEstimatedRowsToScan());
         
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
           .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
@@ -1862,7 +1863,7 @@ public class InListIT extends ParallelStatsDisabledIT {
 
       PreparedStatement preparedStmt = conn.prepareStatement(
         "SELECT VAL2 FROM " + fullTableName + " WHERE (ID2, ID1) IN ((1, 
1),(2, 2))");
-      QueryPlan queryPlan = PhoenixRuntime.getOptimizedQueryPlan(preparedStmt);
+      QueryPlan queryPlan = getOptimizedQueryPlan(preparedStmt);
       queryPlan.getTableRef().getTable().getType();
       
assertPlan(queryPlan).scanTypeStartsWith(ExplainTable.POINT_LOOKUP_ON_STRING)
         .indexRule(OptimizerReasons.RULE_POINT_LOOKUP).indexRejectedNone();
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java
index d0e362ae9a..6b882820fb 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryOptimizerTest.java
@@ -969,6 +969,8 @@ public class QueryOptimizerTest extends 
BaseConnectionlessQueryTest {
   public void testAdoptRewriteStateDoesNotBleedSourceMutations() throws 
Exception {
     Connection conn = DriverManager.getConnection(getUrl());
     PhoenixStatement stmt = 
conn.createStatement().unwrap(PhoenixStatement.class);
+    // Diagnostic recording is off by default. We need it for this test so 
enable it explicitly.
+    stmt.setCollectDiagnostics(true);
 
     StatementContext source = new StatementContext(stmt);
     source.addAppliedRewrite("X");
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 fef325834d..62034de010 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
@@ -1204,16 +1204,19 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
 
   /**
    * When a top-level AND is only partially tagged, renderVerboseFilters 
collapses to a single
-   * combined line. That line must still union the origins recorded on the 
tagged conjunct(s) rather
+   * combined line. That line must still union the origins recorded on the 
tagged conjuncts rather
    * than reading only the parent expression. Normal WHERE/HAVING compilation 
always tags every
-   * conjunct, so this partial-tag state is reachable only when identity is 
lost during expression
-   * rewriting.
+   * conjunct, so this state is reachable only when identity is lost during 
expression rewriting.
    */
   @Test
   public void testVerboseCombinedFilterUnionsConjunctOrigins() throws 
Exception {
     try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps())) {
-      QueryPlan plan = conn.prepareStatement("SELECT * FROM atable")
-        .unwrap(PhoenixPreparedStatement.class).optimizeQuery();
+      PhoenixPreparedStatement pstmt =
+        conn.prepareStatement("SELECT * FROM 
atable").unwrap(PhoenixPreparedStatement.class);
+      // Compile time diagnostic recording is normally only enabled for 
EXPLAIN, so enable it
+      // explicitly on this statement.
+      
pstmt.unwrap(org.apache.phoenix.jdbc.PhoenixStatement.class).setCollectDiagnostics(true);
+      QueryPlan plan = pstmt.optimizeQuery();
       StatementContext context = plan.getContext();
       Expression tagged = LiteralExpression.newConstant(true);
       Expression untagged = LiteralExpression.newConstant(false);
@@ -1955,4 +1958,65 @@ public class ExplainPlanTest extends 
BaseConnectionlessQueryTest {
     return new PColumnImpl(cName, fName, PInteger.INSTANCE, null, null, false, 
0, SortOrder.ASC, 0,
       null, false, "expression", false, false, name.getBytes(), 0L);
   }
+
+  /**
+   * A plain {@code SELECT} compile (not routed through {@code EXPLAIN}) must 
record no compile time
+   * diagnostic state.
+   */
+  @Test
+  public void testPlainSelectRecordsNoDiagnostics() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps());
+      java.sql.Statement stmt = conn.createStatement()) {
+      String base = generateUniqueName();
+      String idx = base + "_IDX";
+      stmt
+        .execute("CREATE TABLE " + base + " (rowkey VARCHAR PRIMARY KEY, c1 
VARCHAR, c2 VARCHAR)");
+      stmt.execute("CREATE LOCAL INDEX " + idx + " ON " + base + " (c1)");
+      String query =
+        "SELECT c1, max(rowkey), max(c2) FROM " + base + " WHERE rowkey <= 'z' 
GROUP BY c1";
+      // Plain compile (not through EXPLAIN) must skip all diagnostic 
recording.
+      QueryPlan plan =
+        
conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class).optimizeQuery();
+      assertTrue("plain compile must not record predicate origins",
+        plan.getContext().getPredicateOrigins().isEmpty());
+      assertTrue("plain compile must not record ignored hints",
+        plan.getContext().getIgnoredHints().isEmpty());
+      assertTrue("plain compile must not accumulate rewrite breadcrumbs",
+        plan.getContext().getAppliedRewrites().isEmpty());
+      assertEquals("plain compile must not record an optimizer decision", null,
+        plan.getOptimizerDecision());
+      assertFalse("plain compile must have collectDiagnostics=false",
+        plan.getContext().isCollectDiagnostics());
+    }
+  }
+
+  /**
+   * The same query compiled with {@code collectDiagnostics=true} (matching 
what the EXPLAIN entry
+   * point does in production) must record full diagnostics. Paired with
+   * {@link #testPlainSelectRecordsNoDiagnostics()}.
+   */
+  @Test
+  public void testExplainSelectRecordsDiagnostics() throws Exception {
+    try (Connection conn = DriverManager.getConnection(getUrl(), 
defaultProps());
+      java.sql.Statement stmt = conn.createStatement()) {
+      String base = generateUniqueName();
+      String idx = base + "_IDX";
+      stmt
+        .execute("CREATE TABLE " + base + " (rowkey VARCHAR PRIMARY KEY, c1 
VARCHAR, c2 VARCHAR)");
+      stmt.execute("CREATE LOCAL INDEX " + idx + " ON " + base + " (c1)");
+      String query =
+        "SELECT c1, max(rowkey), max(c2) FROM " + base + " WHERE rowkey <= 'z' 
GROUP BY c1";
+      // ExplainPlanTestUtil enables collectDiagnostics for the duration of 
the compile, so the
+      // optimizer records the winning index rule and every rejected candidate.
+      ExplainPlanTestUtil.assertPlan(conn, query).indexName(base)
+        
.indexRule(OptimizerReasons.RULE_MORE_BOUND_PK_COLUMNS).indexRejectedCount(1)
+        .indexRejected(0, idx, OptimizerReasons.REASON_NO_PK_PREFIX_BOUND);
+
+      // And a query with a non-PK residual predicate surfaces the WHERE 
origin on the server
+      // filter under VERBOSE.
+      String filterQuery = "SELECT c1 FROM " + base + " WHERE c2 = 'x'";
+      ExplainPlanTestUtil.assertPlanWithVerbose(conn, 
filterQuery).serverFilterCount(1)
+        .serverFilterOrigin(0, "WHERE");
+    }
+  }
 }
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 135180db83..f8f9aae5c8 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
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import java.sql.Connection;
+import java.sql.PreparedStatement;
 import java.sql.SQLException;
 import java.util.Arrays;
 import java.util.Collections;
@@ -32,6 +33,7 @@ import org.apache.phoenix.compile.ExplainPlan;
 import org.apache.phoenix.compile.ExplainPlanAttributes;
 import org.apache.phoenix.compile.QueryPlan;
 import org.apache.phoenix.jdbc.PhoenixPreparedStatement;
+import org.apache.phoenix.jdbc.PhoenixStatement;
 import org.apache.phoenix.optimize.OptimizerReasons;
 import org.apache.phoenix.optimize.RejectedIndexEntry;
 import org.apache.phoenix.parse.ExplainOptions;
@@ -48,14 +50,40 @@ public final class ExplainPlanTestUtil {
   private ExplainPlanTestUtil() {
   }
 
-  /** Optimize {@code query} and return its {@link ExplainPlan}. */
-  public static ExplainPlan getExplainPlan(Connection conn, String query) 
throws SQLException {
+  /**
+   * Compile {@code query} with compile-time diagnostic recording temporarily 
enabled on the
+   * underlying {@link PhoenixStatement} and return the resolved inner {@link 
QueryPlan}. This is
+   * what {@code ExecutableExplainStatement.compilePlan} does for a real 
{@code EXPLAIN} (flip
+   * {@code collectDiagnostics} on before compile, run compile+optimize, 
restore), without the
+   * EXPLAIN-result wrapping so tests can assert directly on the inner plan's 
attributes. The
+   * optional {@link ExplainOptions} are applied to the resolved context so 
{@code VERBOSE}- and
+   * {@code REGIONS}-gated rendering behave as they would under a real
+   * {@code EXPLAIN [(...)] query}.
+   */
+  private static QueryPlan compileWithDiagnostics(Connection conn, String 
query,
+    ExplainOptions options) throws SQLException {
     try (PhoenixPreparedStatement statement =
       conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
-      return statement.optimizeQuery().getExplainPlan();
+      PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+      boolean prev = underlying.isCollectDiagnostics();
+      underlying.setCollectDiagnostics(true);
+      try {
+        QueryPlan plan = statement.optimizeQuery();
+        if (options != null && plan.getContext() != null) {
+          plan.getContext().setExplainOptions(options);
+        }
+        return plan;
+      } finally {
+        underlying.setCollectDiagnostics(prev);
+      }
     }
   }
 
+  /** Optimize {@code query} and return its {@link ExplainPlan}. */
+  public static ExplainPlan getExplainPlan(Connection conn, String query) 
throws SQLException {
+    return compileWithDiagnostics(conn, query, 
ExplainOptions.DEFAULT).getExplainPlan();
+  }
+
   /** Optimize {@code query} and return its structured {@link 
ExplainPlanAttributes}. */
   public static ExplainPlanAttributes getExplainAttributes(Connection conn, 
String query)
     throws SQLException {
@@ -69,12 +97,7 @@ public final class ExplainPlanTestUtil {
    */
   public static ExplainPlanAttributes getExplainAttributes(Connection conn, 
String query,
     ExplainOptions options) throws SQLException {
-    try (PhoenixPreparedStatement statement =
-      conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
-      QueryPlan plan = statement.optimizeQuery();
-      plan.getContext().setExplainOptions(options);
-      return plan.getExplainPlan().getPlanStepsAsAttributes();
-    }
+    return compileWithDiagnostics(conn, query, 
options).getExplainPlan().getPlanStepsAsAttributes();
   }
 
   /** Optimize {@code query} and return its plan-steps text. */
@@ -88,20 +111,26 @@ public final class ExplainPlanTestUtil {
    */
   public static List<String> getPlanSteps(Connection conn, String query, 
ExplainOptions options)
     throws SQLException {
-    try (PhoenixPreparedStatement statement =
-      conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
-      QueryPlan plan = statement.optimizeQuery();
-      plan.getContext().setExplainOptions(options);
-      return plan.getExplainPlan().getPlanSteps();
-    }
+    return compileWithDiagnostics(conn, query, 
options).getExplainPlan().getPlanSteps();
   }
 
-  /** Compile a mutation (UPSERT/DELETE) and return its {@link ExplainPlan}. */
+  /**
+   * Compile a mutation (UPSERT/DELETE) and return its {@link ExplainPlan}. 
Mutations do not go
+   * through the {@code EXPLAIN} entry point, so {@code collectDiagnostics} is 
toggled on for the
+   * duration of the compile so the underlying scan compile records the same 
diagnostics.
+   */
   public static ExplainPlan getMutationExplainPlan(Connection conn, String 
query)
     throws SQLException {
     try (PhoenixPreparedStatement statement =
       conn.prepareStatement(query).unwrap(PhoenixPreparedStatement.class)) {
-      return statement.compileMutation().getExplainPlan();
+      PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+      boolean prev = underlying.isCollectDiagnostics();
+      underlying.setCollectDiagnostics(true);
+      try {
+        return statement.compileMutation().getExplainPlan();
+      } finally {
+        underlying.setCollectDiagnostics(prev);
+      }
     }
   }
 
@@ -145,18 +174,51 @@ public final class ExplainPlanTestUtil {
 
   /**
    * Optimize an already-prepared and, if needed, parameter-bound {@link 
PhoenixPreparedStatement}
-   * and begin assertions on its plan attributes.
+   * and begin assertions on its plan attributes. The prepared statement is 
compiled directly
+   * (rather than routed through {@code EXPLAIN}) because callers may have 
bound parameters that
+   * cannot be replayed against a re-prepared EXPLAIN statement. {@code 
collectDiagnostics} is
+   * toggled on for the duration of the compile so diagnostics are recorded.
    */
   public static ExplainPlanAssert assertPlan(PhoenixPreparedStatement 
statement)
     throws SQLException {
-    return 
assertPlan(statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes());
+    PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+    boolean prev = underlying.isCollectDiagnostics();
+    underlying.setCollectDiagnostics(true);
+    try {
+      return 
assertPlan(statement.optimizeQuery().getExplainPlan().getPlanStepsAsAttributes());
+    } finally {
+      underlying.setCollectDiagnostics(prev);
+    }
   }
 
-  /** Begin assertions on the plan attributes of a resolved {@link QueryPlan}. 
*/
+  /**
+   * Begin assertions on the plan attributes of a resolved {@link QueryPlan}. 
The plan is expected
+   * to have been produced by a compile that already had diagnostic recording 
enabled (e.g. via
+   * {@link #assertPlan(PhoenixPreparedStatement)},
+   * {@link #getOptimizedQueryPlan(PreparedStatement)} or via a direct {@code 
EXPLAIN} compile).
+   */
   public static ExplainPlanAssert assertPlan(QueryPlan queryPlan) throws 
SQLException {
     return assertPlan(queryPlan.getExplainPlan().getPlanStepsAsAttributes());
   }
 
+  /**
+   * Diagnostic drop in replacement for
+   * {@link 
org.apache.phoenix.util.PhoenixRuntime#getOptimizedQueryPlan(PreparedStatement)}.
+   * Toggles {@code collectDiagnostics=true} on the underlying {@link 
PhoenixStatement}. Use this
+   * for tests whenever the plan is fed to {@code assertPlan(...)}.
+   */
+  public static QueryPlan getOptimizedQueryPlan(PreparedStatement statement) 
throws SQLException {
+    PhoenixPreparedStatement pstmt = 
statement.unwrap(PhoenixPreparedStatement.class);
+    PhoenixStatement underlying = pstmt.unwrap(PhoenixStatement.class);
+    boolean prev = underlying.isCollectDiagnostics();
+    underlying.setCollectDiagnostics(true);
+    try {
+      return pstmt.optimizeQuery();
+    } finally {
+      underlying.setCollectDiagnostics(prev);
+    }
+  }
+
   /** Compile the mutation {@code query} on {@code conn} and begin assertions 
on its attributes. */
   public static ExplainPlanAssert assertMutationPlan(Connection conn, String 
query)
     throws SQLException {
@@ -164,12 +226,20 @@ public final class ExplainPlanTestUtil {
   }
 
   /**
-   * Compile an already-prepared and, if needed, parameter-bound mutation
+   * Compile an already prepared and, if needed, parameter bound mutation
    * {@link PhoenixPreparedStatement} and begin assertions on its attributes.
+   * {@code collectDiagnostics} is toggled on for the duration of the compile.
    */
   public static ExplainPlanAssert assertMutationPlan(PhoenixPreparedStatement 
statement)
     throws SQLException {
-    return 
assertPlan(statement.compileMutation().getExplainPlan().getPlanStepsAsAttributes());
+    PhoenixStatement underlying = statement.unwrap(PhoenixStatement.class);
+    boolean prev = underlying.isCollectDiagnostics();
+    underlying.setCollectDiagnostics(true);
+    try {
+      return 
assertPlan(statement.compileMutation().getExplainPlan().getPlanStepsAsAttributes());
+    } finally {
+      underlying.setCollectDiagnostics(prev);
+    }
   }
 
   /** Fluent assertions over {@link ExplainPlanAttributes}. */

Reply via email to