This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch PHOENIX-7876-feature in repository https://gitbox.apache.org/repos/asf/phoenix.git
commit a9fa25659f01ccf40bacb66628e7581ed1e54374 Author: Andrew Purtell <[email protected]> AuthorDate: Sat Jun 6 13:54:04 2026 -0700 [WIP] Recursive UNION ALL EXPLAIN: surface nested structure via subPlans Rewrite UnionPlan.getExplainPlan() to compose recursively from each branch's explain plan. A new UnionResultIterators explainBranches helper provides the source of truth for branch composition. The two ResultIterators.explain overrides on UnionResultIterators are replaced with thin delegates. EXPLAIN of union plans no longer triggers sub-plan execution so connectionless tests can check them. --- .../java/org/apache/phoenix/execute/UnionPlan.java | 47 +++++++++-- .../phoenix/iterate/UnionResultIterators.java | 54 ++++++++---- .../phoenix/end2end/CostBasedDecisionIT.java | 25 +++--- .../phoenix/end2end/QueryWithTableSampleIT.java | 10 +-- .../phoenix/end2end/SortMergeJoinMoreIT.java | 2 +- .../org/apache/phoenix/end2end/UnionAllIT.java | 97 +++++++++++++++++++--- .../phoenix/query/explain/ExplainPlanTest.java | 54 +++++++++++- 7 files changed, 229 insertions(+), 60 deletions(-) diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java index 1a1e76f690..ca6bf3d69c 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/execute/UnionPlan.java @@ -233,19 +233,49 @@ public class UnionPlan implements QueryPlan { @Override public ExplainPlan getExplainPlan() throws SQLException { + // Build EXPLAIN by recursively composing each branch's explain plan. List<String> steps = new ArrayList<String>(); ExplainPlanAttributesBuilder builder = new ExplainPlanAttributesBuilder(); String abstractExplainPlan = "UNION ALL OVER " + this.plans.size() + " QUERIES"; builder.setAbstractExplainPlan(abstractExplainPlan); steps.add(abstractExplainPlan); - ResultIterator iterator = iterator(); - iterator.explain(steps, builder); - // Indent plans steps nested under union, except last client-side merge/concat step (if there is - // one) - int offset = - !orderBy.getOrderByExpressions().isEmpty() && limit != null ? 2 : limit != null ? 1 : 0; - for (int i = 1; i < steps.size() - offset; i++) { - steps.set(i, " " + steps.get(i)); + + UnionResultIterators.explainBranches(plans, steps, builder); + + // Reconstruct the outer client-side pipeline. + boolean useMergeSort = !orderBy.isEmpty() || supportOrderByOptimize; + if (useMergeSort) { + // MergeSortTopN path: CLIENT MERGE SORT, then optional offset/limit (offset first). + builder.setClientSortAlgo("CLIENT MERGE SORT"); + String mergeStep = "CLIENT MERGE SORT"; + steps.add(mergeStep); + builder.addClientStep(mergeStep); + if (offset != null && offset > 0) { + builder.setClientOffset(offset); + String step = "CLIENT OFFSET " + offset; + steps.add(step); + builder.addClientStep(step); + } + if (limit != null && limit > 0) { + builder.setClientRowLimit(limit); + String step = "CLIENT LIMIT " + limit; + steps.add(step); + builder.addClientStep(step); + } + } else { + // Concat path: optional offset, then optional CLIENT n ROW LIMIT. + if (offset != null) { + builder.setClientOffset(offset); + String step = "CLIENT OFFSET " + offset; + steps.add(step); + builder.addClientStep(step); + } + if (limit != null) { + builder.setClientRowLimit(limit); + String step = "CLIENT " + limit + " ROW LIMIT"; + steps.add(step); + builder.addClientStep(step); + } } return new ExplainPlan(steps, builder.build()); } @@ -307,7 +337,6 @@ public class UnionPlan implements QueryPlan { @Override public Set<TableRef> getSourceRefs() { - // TODO is this correct? Set<TableRef> sources = Sets.newHashSetWithExpectedSize(plans.size()); for (QueryPlan plan : plans) { sources.addAll(plan.getSourceRefs()); diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/UnionResultIterators.java b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/UnionResultIterators.java index e5b981e7a6..8223fa06a2 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/UnionResultIterators.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/iterate/UnionResultIterators.java @@ -18,9 +18,11 @@ package org.apache.phoenix.iterate; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.apache.hadoop.hbase.client.Scan; +import org.apache.phoenix.compile.ExplainPlan; import org.apache.phoenix.compile.ExplainPlanAttributes; import org.apache.phoenix.compile.ExplainPlanAttributes.ExplainPlanAttributesBuilder; import org.apache.phoenix.compile.QueryPlan; @@ -41,12 +43,14 @@ public class UnionResultIterators implements ResultIterators { private final List<PeekingResultIterator> iterators; private final List<ReadMetricQueue> readMetricsList; private final List<OverAllQueryMetrics> overAllQueryMetricsList; + private final List<QueryPlan> plans; private boolean closed; private final StatementContext parentStmtCtx; public UnionResultIterators(List<QueryPlan> plans, StatementContext parentStmtCtx) throws SQLException { this.parentStmtCtx = parentStmtCtx; + this.plans = plans; int nPlans = plans.size(); iterators = Lists.newArrayListWithExpectedSize(nPlans); splits = Lists.newArrayListWithExpectedSize(nPlans * 30); @@ -125,9 +129,7 @@ public class UnionResultIterators implements ResultIterators { @Override public void explain(List<String> planSteps) { - for (PeekingResultIterator iterator : iterators) { - iterator.explain(planSteps); - } + explainBranches(plans, planSteps, null); } @Override @@ -138,22 +140,38 @@ public class UnionResultIterators implements ResultIterators { @Override public void explain(List<String> planSteps, ExplainPlanAttributesBuilder explainPlanAttributesBuilder) { - boolean moreThanOneIters = false; - ExplainPlanAttributesBuilder lhsPointer = null; - // For more than one iterators, explainPlanAttributes will create - // chain of objects as lhs and rhs query plans. - for (PeekingResultIterator iterator : iterators) { - if (moreThanOneIters) { - ExplainPlanAttributesBuilder rhsBuilder = new ExplainPlanAttributesBuilder(); - iterator.explain(planSteps, rhsBuilder); - ExplainPlanAttributes rhsPlans = rhsBuilder.build(); - lhsPointer.setRhsJoinQueryExplainPlan(rhsPlans); - lhsPointer = rhsBuilder; - } else { - iterator.explain(planSteps, explainPlanAttributesBuilder); - lhsPointer = explainPlanAttributesBuilder; + explainBranches(plans, planSteps, explainPlanAttributesBuilder); + } + + /** + * Canonical union branch composition shared between {@link #explain(List)}/ + * {@link #explain(List, ExplainPlanAttributesBuilder)} and + * {@link org.apache.phoenix.execute.UnionPlan#getExplainPlan()}. For each branch, appends the + * branch's {@link ExplainPlan#getPlanSteps()} indented by four spaces to {@code planSteps}, and, + * when {@code rootBuilder} is non-null, collects each branch's + * {@link ExplainPlan#getPlanStepsAsAttributes()} into {@code rootBuilder}'s {@code subPlans} + * list. Indentation compounds naturally for nested unions/joins. + */ + public static void explainBranches(List<QueryPlan> plans, List<String> planSteps, + ExplainPlanAttributesBuilder rootBuilder) { + List<ExplainPlanAttributes> subPlans = + rootBuilder == null ? null : new ArrayList<ExplainPlanAttributes>(plans.size()); + for (QueryPlan plan : plans) { + ExplainPlan branchPlan; + try { + branchPlan = plan.getExplainPlan(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + for (String step : branchPlan.getPlanSteps()) { + planSteps.add(" " + step); } - moreThanOneIters = true; + if (subPlans != null) { + subPlans.add(branchPlan.getPlanStepsAsAttributes()); + } + } + if (rootBuilder != null) { + rootBuilder.setSubPlans(subPlans); } } } diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java index 02d85a4f2e..a18f1bcc97 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/CostBasedDecisionIT.java @@ -296,11 +296,11 @@ public class CostBasedDecisionIT extends BaseTest { + " where rowkey <= 'z' GROUP BY c1 " + "UNION ALL SELECT c1, max(rowkey), max(c2) FROM " + tableName + " where rowkey >= 'a' GROUP BY c1"; // Use the default plan when stats are not available. - assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES") - .iteratorType("PARALLEL").scanType("RANGE SCAN").table(tableName).keyRanges(" [*] - ['z']") - .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]") - .clientSortAlgo("CLIENT MERGE SORT").rhs().iteratorType("PARALLEL").scanType("RANGE SCAN") - .table(tableName).keyRanges(" ['a'] - [*]") + assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES").subPlanCount(2) + .subPlan(0).iteratorType("PARALLEL").scanType("RANGE SCAN").table(tableName) + .keyRanges(" [*] - ['z']").serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]") + .clientSortAlgo("CLIENT MERGE SORT").end().subPlan(1).iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(tableName).keyRanges(" ['a'] - [*]") .serverAggregate("SERVER AGGREGATE INTO DISTINCT ROWS BY [C1]") .clientSortAlgo("CLIENT MERGE SORT"); @@ -317,14 +317,15 @@ public class CostBasedDecisionIT extends BaseTest { conn.createStatement().execute("UPDATE STATISTICS " + tableName); // Use the optimal plan based on cost when stats become available. - assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES") - .iteratorType("PARALLEL").scanType("RANGE SCAN").table(indexName + "(" + tableName + ")") - .keyRanges(" [1]").serverMergeColumns("[0.C2]").serverFirstKeyOnlyProjection(true) - .serverWhereFilter("\"ROWKEY\" <= 'z'") - .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]") - .clientSortAlgo("CLIENT MERGE SORT").rhs().iteratorType("PARALLEL").scanType("RANGE SCAN") + assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES").subPlanCount(2) + .subPlan(0).iteratorType("PARALLEL").scanType("RANGE SCAN") .table(indexName + "(" + tableName + ")").keyRanges(" [1]").serverMergeColumns("[0.C2]") - .serverFirstKeyOnlyProjection(true).serverWhereFilter("\"ROWKEY\" >= 'a'") + .serverFirstKeyOnlyProjection(true).serverWhereFilter("\"ROWKEY\" <= 'z'") + .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]") + .clientSortAlgo("CLIENT MERGE SORT").end().subPlan(1).iteratorType("PARALLEL") + .scanType("RANGE SCAN").table(indexName + "(" + tableName + ")").keyRanges(" [1]") + .serverMergeColumns("[0.C2]").serverFirstKeyOnlyProjection(true) + .serverWhereFilter("\"ROWKEY\" >= 'a'") .serverAggregate("SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [\"C1\"]") .clientSortAlgo("CLIENT MERGE SORT"); } finally { diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java index 1a90a1c04a..5f8b966a7a 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithTableSampleIT.java @@ -225,11 +225,11 @@ public class QueryWithTableSampleIT extends ParallelStatsEnabledIT { String query = "SELECT * FROM " + tableName + " tablesample (100) where i1<2 union all SELECT * FROM " + tableName + " tablesample (2) where i2<6000"; - assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES").scanType("RANGE SCAN") - .table(tableName).keyRanges(" [*] - [2]").samplingRate(1.0d) - .serverFirstKeyOnlyProjection(true).rhs().scanType("FULL SCAN").table(tableName) - .samplingRate(0.02d).serverFirstKeyOnlyProjection(true).serverWhereFilter("I2 < 6000") - .end(); + assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES").subPlanCount(2) + .subPlan(0).scanType("RANGE SCAN").table(tableName).keyRanges(" [*] - [2]") + .samplingRate(1.0d).serverFirstKeyOnlyProjection(true).end().subPlan(1) + .scanType("FULL SCAN").table(tableName).samplingRate(0.02d) + .serverFirstKeyOnlyProjection(true).serverWhereFilter("I2 < 6000").end(); } finally { conn.close(); } diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java index 4a626c01a7..1d0980b60f 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/SortMergeJoinMoreIT.java @@ -427,13 +427,13 @@ public class SortMergeJoinMoreIT extends ParallelStatsDisabledIT { String rhsClientSortedBy = i == 0 ? "[BUCKET, \"TIMESTAMP\"]" : null; assertPlan(conn, q).abstractExplainPlan("SORT-MERGE-JOIN (INNER)").sortMergeSkipMerge(true) + .clientAggregate("CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [E.BUCKET, E.TIMESTAMP]") .lhs().iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 RANGES") .table(eventCountTableName).keyRanges(lhsKeyRanges).serverFirstKeyOnlyProjection(true) .serverDistinctFilter("SERVER DISTINCT PREFIX FILTER OVER [BUCKET, TIMESTAMP, LOCATION]") .serverAggregate( "SERVER AGGREGATE INTO ORDERED DISTINCT ROWS BY [BUCKET, TIMESTAMP, LOCATION]") .clientSortAlgo("CLIENT MERGE SORT").clientSortedBy("[BUCKET, TIMESTAMP]") - .clientAggregate("CLIENT AGGREGATE INTO ORDERED DISTINCT ROWS BY [E.BUCKET, E.TIMESTAMP]") .end().rhs().iteratorType("PARALLEL").scanType("SKIP SCAN ON 2 RANGES").table(t[i]) .keyRanges(rhsKeyRanges).serverFirstKeyOnlyProjection(true) .serverWhereFilter("SRC_LOCATION = DST_LOCATION") diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java index 7144653792..85e535b16f 100644 --- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java +++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/UnionAllIT.java @@ -30,6 +30,7 @@ import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; import java.util.Properties; import org.apache.phoenix.exception.SQLExceptionCode; import org.apache.phoenix.query.QueryServices; @@ -616,34 +617,108 @@ public class UnionAllIT extends ParallelStatsDisabledIT { + " CONSTRAINT pk PRIMARY KEY (a_string))\n"; createTestTable(getUrl(), ddl); + // Each branch is sorted/limited server-side, and the union itself adds an outer CLIENT MERGE + // SORT + CLIENT LIMIT pipeline on the root. String orderLimit = "select a_string, col1 from " + tableName1 + " union all select a_string, col1 from " + tableName2 + " order by col1 limit 1"; assertPlan(conn, orderLimit).abstractExplainPlan("UNION ALL OVER 2 QUERIES") + .clientSortAlgo("CLIENT MERGE SORT").clientRowLimit(1) + .clientSteps("CLIENT MERGE SORT", "CLIENT LIMIT 1").subPlanCount(2).subPlan(0) .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName1).serverSortedBy("[COL1]") - .serverRowLimit(1L).clientRowLimit(1).clientSortAlgo("CLIENT MERGE SORT").rhs() + .serverRowLimit(1L).clientRowLimit(1).clientSortAlgo("CLIENT MERGE SORT").end().subPlan(1) .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName2).serverSortedBy("[COL1]") .serverRowLimit(1L).clientRowLimit(1).clientSortAlgo("CLIENT MERGE SORT"); + // Each branch carries its own pushed-down limit, and the union root adds an outer CLIENT n + // ROW LIMIT. String limitOnly = "select a_string, col1 from " + tableName1 + " union all select a_string, col1 from " + tableName2 + " limit 2"; - // The LHS branch's builder is shared with the union plan's root builder, so the LHS - // root carries both the inner branch's CLIENT 2 ROW LIMIT and the union's outer - // CLIENT 2 ROW LIMIT in its clientSteps. The RHS branch has its own separate builder. - assertPlan(conn, limitOnly).abstractExplainPlan("UNION ALL OVER 2 QUERIES") - .iteratorType("SERIAL").scanType("FULL SCAN").table(tableName1).serverSortedBy(null) - .serverRowLimit(2L).clientRowLimit(2) - .clientSteps("CLIENT 2 ROW LIMIT", "CLIENT 2 ROW LIMIT").rhs().iteratorType("SERIAL") + assertPlan(conn, limitOnly).abstractExplainPlan("UNION ALL OVER 2 QUERIES").clientRowLimit(2) + .clientSteps("CLIENT 2 ROW LIMIT").subPlanCount(2).subPlan(0).iteratorType("SERIAL") + .scanType("FULL SCAN").table(tableName1).serverSortedBy(null).serverRowLimit(2L) + .clientRowLimit(2).clientSteps("CLIENT 2 ROW LIMIT").end().subPlan(1).iteratorType("SERIAL") .scanType("FULL SCAN").table(tableName2).serverSortedBy(null).serverRowLimit(2L) .clientRowLimit(2).clientSteps("CLIENT 2 ROW LIMIT"); + // UNION ALL root has no outer client pipeline, just two sub-plans. String noLimit = "select a_string, col1 from " + tableName1 + " union all select a_string, col1 from " + tableName2; - assertPlan(conn, noLimit).abstractExplainPlan("UNION ALL OVER 2 QUERIES") - .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName1).rhs() - .iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName2); + assertPlan(conn, noLimit).abstractExplainPlan("UNION ALL OVER 2 QUERIES").clientStepCount(0) + .subPlanCount(2).subPlan(0).iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName1) + .end().subPlan(1).iteratorType("PARALLEL").scanType("FULL SCAN").table(tableName2); } } + /** + * A {@code UNION ALL} of two hash joins must surface each branch's full join structure under + * {@code subPlans}, and the indented join detail lines must appear in the EXPLAIN result-set + * text. + */ + @Test + public void testExplainUnionOfJoins() throws Exception { + String t1 = generateUniqueName(); + String t2 = generateUniqueName(); + String t3 = generateUniqueName(); + String t4 = generateUniqueName(); + Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES); + try (Connection conn = DriverManager.getConnection(getUrl(), props)) { + for (String t : new String[] { t1, t2, t3, t4 }) { + createTestTable(getUrl(), "CREATE TABLE " + t + + " (a_string varchar not null, col1 integer CONSTRAINT pk PRIMARY KEY (a_string))"); + } + String query = "SELECT x.a_string, x.col1 FROM " + t1 + " x JOIN " + t2 + + " y ON x.a_string = y.a_string" + " UNION ALL " + "SELECT p.a_string, p.col1 FROM " + t3 + + " p JOIN " + t4 + " q ON p.a_string = q.a_string"; + + // In UNION ALL each branch is a hash-join node with its own delegate scan, dynamic server + // filter, and a single hash sub-plan for the build side. + assertPlan(conn, query).abstractExplainPlan("UNION ALL OVER 2 QUERIES").clientStepCount(0) + .subPlanCount(2).subPlan(0).table(t1) + .dynamicServerFilter("DYNAMIC SERVER FILTER BY X.A_STRING IN (Y.A_STRING)").subPlanCount(1) + .subPlan(0).table(t2).end().end().subPlan(1).table(t3) + .dynamicServerFilter("DYNAMIC SERVER FILTER BY P.A_STRING IN (Q.A_STRING)").subPlanCount(1) + .subPlan(0).table(t4); + + // Branch level hash-join lines must be indented +4 under the union, and the inner scan + // lines must be indented +4 again. + List<String> textSteps = new java.util.ArrayList<>(); + try (ResultSet rs = conn.createStatement().executeQuery("EXPLAIN " + query)) { + while (rs.next()) { + textSteps.add(rs.getString(1)); + } + } + assertTrue("EXPLAIN must start with union header, was: " + textSteps, + textSteps.get(0).startsWith("UNION ALL OVER 2 QUERIES")); + assertTrue("EXPLAIN must include indented join probe scan over " + t1 + ", was: " + textSteps, + containsLine(textSteps, " CLIENT", "FULL SCAN OVER " + t1)); + assertTrue( + "EXPLAIN must include hash sub-plan header indented under branch, was: " + textSteps, + containsLine(textSteps, " PARALLEL INNER-JOIN TABLE 0")); + assertTrue("EXPLAIN must include indented build-side scan over " + t2 + ", was: " + textSteps, + containsLine(textSteps, " CLIENT", "FULL SCAN OVER " + t2)); + assertTrue("EXPLAIN must include dynamic server filter for branch 0, was: " + textSteps, + containsLine(textSteps, "DYNAMIC SERVER FILTER BY X.A_STRING IN (Y.A_STRING)")); + assertTrue("EXPLAIN must include indented build-side scan over " + t4 + ", was: " + textSteps, + containsLine(textSteps, " CLIENT", "FULL SCAN OVER " + t4)); + } + } + + /** Returns true iff some entry in {@code lines} contains every {@code needle}. */ + private static boolean containsLine(java.util.List<String> lines, String... needles) { + outer: for (String l : lines) { + int from = 0; + for (String n : needles) { + int idx = l.indexOf(n, from); + if (idx < 0) { + continue outer; + } + from = idx + n.length(); + } + return true; + } + return false; + } + @Test public void testBug2295() throws Exception { String tableName1 = generateUniqueName(); 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 e0880a2c2d..1ba3ce1a8d 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 @@ -546,7 +546,9 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { @Test public void testUnionAll() throws Exception { - ObjectNode rhs = scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000002']"); + ArrayNode subPlans = mapper.createArrayNode(); + subPlans.add(scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']")); + subPlans.add(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'", @@ -555,9 +557,53 @@ public class ExplainPlanTest extends BaseConnectionlessQueryTest { " INDEX ATABLE", " REGIONS PLANNED <N>", " CLIENT PARALLEL <N>-WAY RANGE SCAN OVER ATABLE ['00D000000000002']", " INDEX ATABLE", " REGIONS PLANNED <N>"), - scanAttrs("RANGE SCAN ", "ATABLE", " ['00D000000000001']") - .put("abstractExplainPlan", "UNION ALL OVER 2 QUERIES") - .set("rhsJoinQueryExplainPlan", rhs)); + defaultAttrs().put("abstractExplainPlan", "UNION ALL OVER 2 QUERIES").set("subPlans", + subPlans)); + } + + /** + * Deeply nested EXPLAIN test. A {@code UNION ALL} of two hash joins must surface each branch's + * full join structure under {@code subPlans}, with the indentation compounding correctly in the + * textual output. + */ + @Test + public void testUnionAllOfHashJoins() throws Exception { + ObjectNode child0 = scanAttrs("FULL SCAN ", "ATABLE", "").put("abstractExplainPlan", + "PARALLEL INNER-JOIN TABLE 0"); + ObjectNode branch0 = scanAttrs("FULL SCAN ", "ATABLE", ""); + branch0.set("subPlans", mapper.createArrayNode().add(child0)); + branch0.put("dynamicServerFilter", + "DYNAMIC SERVER FILTER BY A.ORGANIZATION_ID IN (B.ORGANIZATION_ID)"); + + ObjectNode child1 = scanAttrs("FULL SCAN ", "ATABLE", "").put("abstractExplainPlan", + "PARALLEL INNER-JOIN TABLE 0"); + ObjectNode branch1 = scanAttrs("FULL SCAN ", "ATABLE", ""); + branch1.set("subPlans", mapper.createArrayNode().add(child1)); + + ArrayNode subPlans = mapper.createArrayNode().add(branch0).add(branch1); + + verifyQuery("unionAllOfHashJoins", + "SELECT a.a_string, b.b_string FROM atable a" + + " JOIN atable b ON a.organization_id = b.organization_id" + " UNION ALL" + + " SELECT a.a_string, b.b_string FROM atable a" + + " JOIN atable b ON a.entity_id = b.entity_id", + text("UNION ALL OVER 2 QUERIES", + // Branch 0: hash join, indented +4 under the union. + " CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", " INDEX ATABLE", + " REGIONS PLANNED <N>", + " PARALLEL INNER-JOIN TABLE 0 /* HASH BUILD RIGHT */", + // Inner build-side scan, indented +4 again under the join. + " CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", " INDEX ATABLE", + " REGIONS PLANNED <N>", + " DYNAMIC SERVER FILTER BY A.ORGANIZATION_ID IN (B.ORGANIZATION_ID)", + // Branch 1: hash join on a non-leading PK column, no dynamic filter. + " CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", " INDEX ATABLE", + " REGIONS PLANNED <N>", + " PARALLEL INNER-JOIN TABLE 0 /* HASH BUILD RIGHT */", + " CLIENT PARALLEL <N>-WAY FULL SCAN OVER ATABLE", " INDEX ATABLE", + " REGIONS PLANNED <N>"), + defaultAttrs().put("abstractExplainPlan", "UNION ALL OVER 2 QUERIES").set("subPlans", + subPlans)); } @Test
