morrySnow commented on code in PR #63974:
URL: https://github.com/apache/doris/pull/63974#discussion_r3370631767
##########
fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsRecorder.java:
##########
@@ -260,6 +302,27 @@ private static void walkPlan(Plan plan,
for (NamedExpression expr : agg.getOutputExpressions()) {
Review Comment:
**Code duplication with `PhysicalSetOperation` handler (lines 366–385).**
The `PhysicalRecursiveUnion` handler is nearly identical to the
`PhysicalSetOperation` handler above. Both call `getRegularChildrenOutputs()`
and iterate `SlotReference` lists to record queryHit. Consider extracting a
shared private helper method to avoid ~15 lines of duplicated code and prevent
future drift between the two handlers.
##########
fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsRecorder.java:
##########
@@ -125,19 +128,42 @@ static Map<String, StatsDelta> collectDeltas(PhysicalPlan
plan) {
: plan.getOutput();
for (NamedExpression ne : rootExprs) {
SlotReference sr = unwrapAlias(ne);
- if (sr == null) {
- continue;
+ if (sr != null) {
+ PhysicalOlapScan sourceScan = exprIdToScan.get(sr.getExprId());
+ if (sourceScan != null) {
+ StatsDelta delta = getOrCreateDelta(deltas, sourceScan);
+ if (delta != null) {
+ String colName = sr.getOriginalColumn().map(col ->
col.getName())
+ .orElseGet(() ->
exprIdToColName.get(sr.getExprId()));
+ if (colName != null) {
+ delta.addQueryStats(colName);
+ }
+ }
+ continue;
+ }
}
- PhysicalOlapScan sourceScan = exprIdToScan.get(sr.getExprId());
- if (sourceScan == null) {
+ // Slot from a computed alias, or a complex root alias
(Alias(a+b)): expand via map.
+ ExprId lookupId = (sr != null) ? sr.getExprId() : ne.getExprId();
+ Set<Slot> inputSlots = aggOutputToInputSlots.get(lookupId);
+ if (inputSlots == null) {
continue;
}
Review Comment:
**Minor consistency issue:** `PhysicalCTEProducer` handler uses `.child()`
to walk the single child, while all other plan nodes (including
`PhysicalStorageLayerAggregate`) rely on `plan.children()` iteration (line
243). Since `PhysicalCTEProducer` extends `PhysicalUnary`, `.child()` and
`.children().get(0)` return the same instance. Using `.children()` would be
more consistent and more robust if the class hierarchy changes.
##########
regression-test/suites/query_p0/stats/query_stats_test.groovy:
##########
@@ -198,6 +198,41 @@ suite("query_stats_test") {
def joinResult = sql "show query stats from ${tbName} all"
assertTrue((joinResult[0][1] as int) >= 1)
+ // UNION ALL: both branches contribute queryHit for k1; WHERE on right
branch adds k2.filterHit.
+ sql "clean all query stats"
+ sql "select k1 from ${tbName} union all select k1 from ${tbName} where k2
= 100"
+ def unionStats = sql "show query stats from ${tbName}"
+ def uK1 = unionStats.find { it[0] == "k1" }
+ def uK2 = unionStats.find { it[0] == "k2" }
+ assertNotNull(uK1)
+ assertNotNull(uK2)
+ assertTrue((uK1[1] as int) >= 1, "k1: UNION ALL queryHit")
+ assertTrue((uK2[2] as int) >= 1, "k2: WHERE on right UNION branch
filterHit")
+
+ // HAVING: k1 queryHit from GROUP BY + SELECT, k2 queryHit from SUM input,
+ // k2 filterHit from HAVING SUM(k2) > -1000.
+ sql "clean all query stats"
+ sql "select k1, sum(k2) from ${tbName} group by k1 having sum(k2) > -1000"
+ def havingStats = sql "show query stats from ${tbName}"
+ def hvK1 = havingStats.find { it[0] == "k1" }
+ def hvK2 = havingStats.find { it[0] == "k2" }
+ assertNotNull(hvK1)
+ assertNotNull(hvK2)
+ assertTrue((hvK1[1] as int) >= 1, "k1: GROUP BY / SELECT queryHit")
+ assertTrue((hvK2[1] as int) >= 1, "k2: SUM aggregate input queryHit")
+ assertTrue((hvK2[2] as int) >= 1, "k2: HAVING SUM(k2) filterHit")
+
+ // CTE: consumer query records queryHit on k2 and filterHit on k1.
+ sql "clean all query stats"
+ sql """with cte as (select k2 from ${tbName} where k1 = 1) select k2 from
cte"""
+ def cteStats = sql "show query stats from ${tbName}"
+ def cteK2 = cteStats.find { it[0] == "k2" }
+ def cteK1 = cteStats.find { it[0] == "k1" }
+ assertNotNull(cteK2)
+ assertNotNull(cteK1)
Review Comment:
**Test coverage gap:** The regression tests cover UNION ALL, HAVING
(single-input), and non-recursive CTE. However, the following new code paths
lack end-to-end regression validation:
- **LATERAL VIEW / EXPLODE** (`PhysicalGenerate`) — very common in production
- **Computed SELECT expressions** (e.g., `SELECT k1+k2 FROM t`) — extremely
common
- **INTERSECT / EXCEPT**, **Recursive CTE**, **IN/EXISTS mark join**,
**Multi-input HAVING** (e.g., `HAVING SUM(k2+k3) > 0`)
Consider adding at least LATERAL VIEW and computed SELECT regression tests,
which are the most common production patterns.
##########
fe/fe-core/src/main/java/org/apache/doris/statistics/query/QueryStatsRecorder.java:
##########
@@ -125,19 +128,42 @@ static Map<String, StatsDelta> collectDeltas(PhysicalPlan
plan) {
: plan.getOutput();
for (NamedExpression ne : rootExprs) {
SlotReference sr = unwrapAlias(ne);
- if (sr == null) {
- continue;
+ if (sr != null) {
+ PhysicalOlapScan sourceScan = exprIdToScan.get(sr.getExprId());
+ if (sourceScan != null) {
+ StatsDelta delta = getOrCreateDelta(deltas, sourceScan);
+ if (delta != null) {
+ String colName = sr.getOriginalColumn().map(col ->
col.getName())
+ .orElseGet(() ->
exprIdToColName.get(sr.getExprId()));
+ if (colName != null) {
+ delta.addQueryStats(colName);
+ }
+ }
+ continue;
+ }
}
- PhysicalOlapScan sourceScan = exprIdToScan.get(sr.getExprId());
- if (sourceScan == null) {
+ // Slot from a computed alias, or a complex root alias
(Alias(a+b)): expand via map.
+ ExprId lookupId = (sr != null) ? sr.getExprId() : ne.getExprId();
+ Set<Slot> inputSlots = aggOutputToInputSlots.get(lookupId);
+ if (inputSlots == null) {
continue;
}
- StatsDelta delta = getOrCreateDelta(deltas, sourceScan);
- if (delta != null) {
- String colName = sr.getOriginalColumn().map(col ->
col.getName())
- .orElseGet(() -> exprIdToColName.get(sr.getExprId()));
- if (colName != null) {
- delta.addQueryStats(colName);
+ for (Slot slot : inputSlots) {
+ if (!(slot instanceof SlotReference)) {
+ continue;
+ }
+ SlotReference inputSr = (SlotReference) slot;
Review Comment:
**Defensive design concern:** The comment says "leaf node: no children to
walk" and returns early. While true today, `PhysicalOlapScan` and
`PhysicalLazyMaterializeOlapScan` also have no plan children but don't
explicitly return — they fall through to the empty `children()` iteration
naturally. If `PhysicalCTEConsumer` ever gains children in the future, this
early return would silently skip them. Consider removing the explicit `return`
and moving the consumer slot mapping logic before the `children()` loop (above
line 243), following the same pattern as scan handlers above.
##########
fe/fe-core/src/test/java/org/apache/doris/statistics/query/QueryStatsRecorderTest.java:
##########
@@ -923,6 +930,433 @@ public void
testPhysicalPartitionTopNRegistersPartitionAndOrderKeysAsQueryHit()
// ── helpers
──────────────────────────────────────────────────────────────
+ /**
+ * Plan: CTEConsumer (consumerSlot#3 → producerSlot#1=k1) → Scan[k1(#1)]
+ * Expected: k1.queryHit=true via consumer slot.
+ */
+ @Test
+ public void testCteConsumerMapsToProducerScan() {
+ ExprId prodId = new ExprId(1);
+ ExprId consId = new ExprId(3);
+ SlotReference prodSlot = mockSlot(prodId, "k1");
+ SlotReference consSlot = mockSlot(consId, "k1");
+
+ PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(prodSlot));
+
+ PhysicalCTEConsumer consumer = Mockito.mock(PhysicalCTEConsumer.class);
+
Mockito.when(consumer.getOutput()).thenReturn(ImmutableList.of(consSlot));
+ Mockito.when(consumer.getProducerSlot(consSlot)).thenReturn(prodSlot);
+ Mockito.when(consumer.children()).thenReturn(ImmutableList.of());
+
+ // Root plan: scan (producer) visited first, consumer slots then get
registered.
+ PhysicalFilter<?> root = Mockito.mock(PhysicalFilter.class);
+ Mockito.when(root.children()).thenReturn(ImmutableList.of(scan,
consumer));
+ Mockito.when(root.getConjuncts()).thenReturn(ImmutableSet.of());
+ Mockito.when(root.getOutput()).thenReturn(ImmutableList.of(consSlot));
+
+ Map<String, StatsDelta> deltas =
QueryStatsRecorder.collectDeltas((PhysicalPlan) root);
+
+ Assertions.assertEquals(1, deltas.size());
+ StatsDelta delta = deltas.values().iterator().next();
+ Assertions.assertNotNull(delta.getColumnStats().get("k1"));
+ Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit);
+ }
+
+ /**
+ * Plan: UNION of Scan1[k1(#1)] and Scan2[k1(#3)]
+ * Expected: k1.queryHit=true on both scan tables.
+ */
+ @Test
+ public void testUnionRecordsQueryHitOnAllBranches() {
+ ExprId id1 = new ExprId(1);
+ ExprId id3 = new ExprId(3);
+ SlotReference k1Left = mockSlot(id1, "k1");
+ SlotReference k1Right = mockSlot(id3, "k1");
+
+ PhysicalOlapScan scan1 = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(k1Left));
+ PhysicalOlapScan scan2 = mockScan(1L, 2L, 2L, 1L,
ImmutableList.of(k1Right));
+
+ PhysicalSetOperation union = Mockito.mock(PhysicalSetOperation.class);
+ Mockito.when(union.children()).thenReturn(ImmutableList.of(scan1,
scan2));
+ Mockito.when(union.getRegularChildrenOutputs())
+ .thenReturn(ImmutableList.of(ImmutableList.of(k1Left),
ImmutableList.of(k1Right)));
+ Mockito.when(union.getOutput()).thenReturn(ImmutableList.of());
+
+ Map<String, StatsDelta> deltas =
QueryStatsRecorder.collectDeltas((PhysicalPlan) union);
+
+ Assertions.assertEquals(2, deltas.size());
+ for (StatsDelta delta : deltas.values()) {
+ Assertions.assertNotNull(delta.getColumnStats().get("k1"));
+ Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit);
+ }
+ }
+
+ /**
+ * Plan: Filter(SUM(k2)#5 > 0) → Agg[SUM(k2)] → Scan[k2(#2)]
+ * Expected: k2.filterHit=true from HAVING SUM(k2) > 0.
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testHavingAggregateFilterHitRecorded() {
+ ExprId k2Id = new ExprId(2);
+ ExprId sumId = new ExprId(5);
+ SlotReference k2Slot = mockSlot(k2Id, "k2");
+ // Aggregate output slots have no originalColumn in real Doris; use
Optional.empty()
+ // so recordInputSlotsAsFilterHit falls back to
exprIdToColName.get(sumId) = "k2".
+ SlotReference sumSlot = Mockito.mock(SlotReference.class);
+ Mockito.when(sumSlot.getExprId()).thenReturn(sumId);
+ Mockito.when(sumSlot.getOriginalColumn()).thenReturn(Optional.empty());
+
+ PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(k2Slot));
+
+ NamedExpression sumExpr = Mockito.mock(NamedExpression.class);
+ Mockito.when(sumExpr.getExprId()).thenReturn(sumId);
+
Mockito.when(sumExpr.getInputSlots()).thenReturn(ImmutableSet.of(k2Slot));
+
+ Aggregate<?> agg = Mockito.mock(Aggregate.class);
+ Mockito.when(agg.children()).thenReturn(ImmutableList.of(scan));
+
Mockito.when(agg.getGroupByExpressions()).thenReturn(ImmutableList.of());
+ Mockito.when(agg.getOutputExpressions())
+ .thenReturn((List<NamedExpression>) (List<?>)
ImmutableList.of(sumExpr));
+ Mockito.when(agg.getOutput()).thenReturn(ImmutableList.of(sumSlot));
+
+ Expression havingConjunct = Mockito.mock(Expression.class);
+
Mockito.when(havingConjunct.getInputSlots()).thenReturn(ImmutableSet.of(sumSlot));
+
+ PhysicalFilter<?> havingFilter = Mockito.mock(PhysicalFilter.class);
+
Mockito.when(havingFilter.children()).thenReturn(ImmutableList.of(agg));
+
Mockito.when(havingFilter.getConjuncts()).thenReturn(ImmutableSet.of(havingConjunct));
+ Mockito.when(havingFilter.getOutput()).thenReturn(ImmutableList.of());
+
+ Map<String, StatsDelta> deltas =
+ QueryStatsRecorder.collectDeltas((PhysicalPlan) havingFilter);
+
+ Assertions.assertEquals(1, deltas.size());
+ StatsDelta delta = deltas.values().iterator().next();
+ Assertions.assertNotNull(delta.getColumnStats().get("k2"));
+ Assertions.assertTrue(delta.getColumnStats().get("k2").filterHit);
+ }
+
+ /**
+ * Plan: Generate(EXPLODE(tags#2)) → Scan[tags(#2)]
+ * Expected: tags.queryHit=true from the generator input column.
+ */
+ @Test
+ public void testLateralViewExplodeRecordsGeneratorInputAsQueryHit() {
+ ExprId tagsId = new ExprId(2);
+ SlotReference tagsSlot = mockSlot(tagsId, "tags");
+
+ PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(tagsSlot));
+
+ Function explodeFn = Mockito.mock(Function.class);
+
Mockito.when(explodeFn.getInputSlots()).thenReturn(ImmutableSet.of(tagsSlot));
+
+ PhysicalGenerate<?> generate = Mockito.mock(PhysicalGenerate.class);
+ Mockito.when(generate.children()).thenReturn(ImmutableList.of(scan));
+
Mockito.when(generate.getGenerators()).thenReturn(ImmutableList.of(explodeFn));
+ Mockito.when(generate.getOutput()).thenReturn(ImmutableList.of());
+
+ Map<String, StatsDelta> deltas =
+ QueryStatsRecorder.collectDeltas((PhysicalPlan) generate);
+
+ Assertions.assertEquals(1, deltas.size());
+ StatsDelta delta = deltas.values().iterator().next();
+ Assertions.assertNotNull(delta.getColumnStats().get("tags"));
+ Assertions.assertTrue(delta.getColumnStats().get("tags").queryHit);
+ Assertions.assertFalse(delta.getColumnStats().get("tags").filterHit);
+ }
+
+ /**
+ * Plan: PhysicalCTEProducer(Scan[k1(#1)]) + CTEConsumer(consSlot#3 →
prodSlot#1)
+ * PhysicalCTEProducer must walk its child so the scan is registered
before the
+ * consumer is processed by the sibling walk.
+ * Expected: k1.queryHit=true via consumer slot resolved through producer
scan.
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testCTEProducerRegistersScansForConsumer() {
+ ExprId prodId = new ExprId(1);
+ ExprId consId = new ExprId(3);
+ SlotReference prodSlot = mockSlot(prodId, "k1");
+ SlotReference consSlot = mockSlot(consId, "k1");
+
+ PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(prodSlot));
+
+ PhysicalCTEProducer<?> producer =
Mockito.mock(PhysicalCTEProducer.class);
+ Mockito.when(producer.child()).thenReturn(scan);
+
+ PhysicalCTEConsumer consumer = Mockito.mock(PhysicalCTEConsumer.class);
+
Mockito.when(consumer.getOutput()).thenReturn(ImmutableList.of(consSlot));
+ Mockito.when(consumer.getProducerSlot(consSlot)).thenReturn(prodSlot);
+ Mockito.when(consumer.children()).thenReturn(ImmutableList.of());
+
+ PhysicalPlan root = Mockito.mock(PhysicalPlan.class);
+ Mockito.when(root.children()).thenReturn(ImmutableList.of(producer,
consumer));
+ Mockito.when(root.getOutput()).thenReturn(ImmutableList.of(consSlot));
+
+ Map<String, StatsDelta> deltas =
QueryStatsRecorder.collectDeltas(root);
+
+ Assertions.assertEquals(1, deltas.size());
+ StatsDelta delta = deltas.values().iterator().next();
+ Assertions.assertNotNull(delta.getColumnStats().get("k1"),
+ "k1 must be registered via PhysicalCTEProducer→scan path");
+ Assertions.assertTrue(delta.getColumnStats().get("k1").queryHit,
+ "k1.queryHit must be true via CTE producer→scan→consumer
chain");
+ }
+
+ /**
+ * Plan: Filter(SUM(k2+k3)#5 > 0) → Agg[SUM(k2+k3)] → Scan[k2(#2), k3(#3)]
+ * Multi-input aggregate output is stored in aggOutputToInputSlots so that
a HAVING
+ * filter on SUM(k2+k3) records filterHit on both k2 and k3.
+ * Expected: k2.filterHit=true AND k3.filterHit=true.
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testHavingMultiInputAggregateFilterHitRecorded() {
+ ExprId k2Id = new ExprId(2);
+ ExprId k3Id = new ExprId(3);
+ ExprId sumId = new ExprId(5);
+ SlotReference k2Slot = mockSlot(k2Id, "k2");
+ SlotReference k3Slot = mockSlot(k3Id, "k3");
+ SlotReference sumSlot = mockSlot(sumId, "sum_k2_k3");
+
+ PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(k2Slot, k3Slot));
+
+ // SUM(k2+k3): two input slots → must go into aggOutputToInputSlots
+ NamedExpression sumExpr = Mockito.mock(NamedExpression.class);
+ Mockito.when(sumExpr.getExprId()).thenReturn(sumId);
+
Mockito.when(sumExpr.getInputSlots()).thenReturn(ImmutableSet.of(k2Slot,
k3Slot));
+
+ Aggregate<?> agg = Mockito.mock(Aggregate.class);
+ Mockito.when(agg.children()).thenReturn(ImmutableList.of(scan));
+
Mockito.when(agg.getGroupByExpressions()).thenReturn(ImmutableList.of());
+ Mockito.when(agg.getOutputExpressions())
+ .thenReturn((List<NamedExpression>) (List<?>)
ImmutableList.of(sumExpr));
+ Mockito.when(agg.getOutput()).thenReturn(ImmutableList.of(sumSlot));
+
+ // HAVING SUM(k2+k3) > 0: conjunct references the aggregate output slot
+ Expression havingConjunct = Mockito.mock(Expression.class);
+
Mockito.when(havingConjunct.getInputSlots()).thenReturn(ImmutableSet.of(sumSlot));
+
+ PhysicalFilter<?> havingFilter = Mockito.mock(PhysicalFilter.class);
+
Mockito.when(havingFilter.children()).thenReturn(ImmutableList.of(agg));
+
Mockito.when(havingFilter.getConjuncts()).thenReturn(ImmutableSet.of(havingConjunct));
+ Mockito.when(havingFilter.getOutput()).thenReturn(ImmutableList.of());
+
+ Map<String, StatsDelta> deltas =
+ QueryStatsRecorder.collectDeltas((PhysicalPlan) havingFilter);
+
+ Assertions.assertEquals(1, deltas.size());
+ StatsDelta delta = deltas.values().iterator().next();
+ Assertions.assertNotNull(delta.getColumnStats().get("k2"),
+ "k2 must be recorded via multi-input HAVING expansion");
+ Assertions.assertTrue(delta.getColumnStats().get("k2").filterHit,
+ "k2.filterHit must be true from HAVING SUM(k2+k3)");
+ Assertions.assertNotNull(delta.getColumnStats().get("k3"),
+ "k3 must be recorded via multi-input HAVING expansion");
+ Assertions.assertTrue(delta.getColumnStats().get("k3").filterHit,
+ "k3.filterHit must be true from HAVING SUM(k2+k3)");
+ }
+
+ /**
+ * Mark join conjuncts (from IN/EXISTS subquery rewriting) are stored
separately
+ * in AbstractPhysicalJoin and must be processed independently of
hash/other conjuncts.
+ * Plan: HashJoin with k1=k2 in hashJoinConjuncts and k3>0 in
markJoinConjuncts.
+ * Expected: k1.filterHit, k2.filterHit (hash), k3.filterHit (mark).
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testMarkJoinConjunctsRecordFilterHit() {
+ ExprId id1 = new ExprId(1);
+ ExprId id2 = new ExprId(2);
+ ExprId id3 = new ExprId(3);
+ SlotReference k1Slot = mockSlot(id1, "k1");
+ SlotReference k2Slot = mockSlot(id2, "k2");
+ SlotReference k3Slot = mockSlot(id3, "k3");
+
+ PhysicalOlapScan left = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(k1Slot, k3Slot));
+ PhysicalOlapScan right = mockScan(2L, 2L, 2L, 2L,
ImmutableList.of(k2Slot));
+
+ Expression hashConjunct = Mockito.mock(Expression.class);
+
Mockito.when(hashConjunct.getInputSlots()).thenReturn(ImmutableSet.of(k1Slot,
k2Slot));
+
+ // Mark conjunct: k3 > 0 — only reachable via getMarkJoinConjuncts()
+ Expression markConjunct = Mockito.mock(Expression.class);
+
Mockito.when(markConjunct.getInputSlots()).thenReturn(ImmutableSet.of(k3Slot));
+
+ org.apache.doris.nereids.trees.plans.physical.AbstractPhysicalJoin<?,
?> join =
+
Mockito.mock(org.apache.doris.nereids.trees.plans.physical.PhysicalHashJoin.class);
+ Mockito.when(join.children()).thenReturn(ImmutableList.of(left,
right));
+
Mockito.when(join.getHashJoinConjuncts()).thenReturn(ImmutableList.of(hashConjunct));
+
Mockito.when(join.getOtherJoinConjuncts()).thenReturn(ImmutableList.of());
+
Mockito.when(join.getMarkJoinConjuncts()).thenReturn(ImmutableList.of(markConjunct));
+ Mockito.when(join.getOutput()).thenReturn(ImmutableList.of());
+
+ Map<String, StatsDelta> deltas =
QueryStatsRecorder.collectDeltas((PhysicalPlan) join);
+
+ Assertions.assertTrue(deltas.containsKey("1_1_1_1"), "left scan delta
missing");
+ Assertions.assertTrue(deltas.containsKey("2_2_2_2"), "right scan delta
missing");
+
Assertions.assertTrue(deltas.get("1_1_1_1").getColumnStats().get("k1").filterHit,
+ "k1.filterHit: hash join conjunct");
+
Assertions.assertTrue(deltas.get("2_2_2_2").getColumnStats().get("k2").filterHit,
+ "k2.filterHit: hash join conjunct");
+
Assertions.assertTrue(deltas.get("1_1_1_1").getColumnStats().get("k3").filterHit,
+ "k3.filterHit: mark join conjunct — previously missed");
+ }
+
+ /**
+ * PhysicalRecursiveUnion extends PhysicalBinary, not
PhysicalSetOperation, so its
+ * getRegularChildrenOutputs() was never called. The base-case slots (from
an OlapScan)
+ * must get queryHit; recursive-case slots (from WorkTableReference) are
silently skipped.
+ * Plan: RecursiveUnion with base-case output [k1(#1)] from scan,
recursive-case [k1(#5)] skipped.
+ * Expected: k1.queryHit=true (base case only).
+ */
+ @Test
+ @SuppressWarnings("unchecked")
+ public void testRecursiveUnionBaseColumnGetsQueryHit() {
+ ExprId baseScanId = new ExprId(1);
+ ExprId recursiveId = new ExprId(5); // WorkTableReference slot — not
in exprIdToScan
+ SlotReference baseScanSlot = mockSlot(baseScanId, "k1");
+ SlotReference recursiveSlot = mockSlot(recursiveId, "k1");
+
+ PhysicalOlapScan scan = mockScan(1L, 1L, 1L, 1L,
ImmutableList.of(baseScanSlot));
+
Review Comment:
**Minor: Mock accuracy.** `PhysicalRecursiveUnion` extends `PhysicalBinary`
and has two children (base case + recursive case), but the mock only returns
one
child:`Mockito.when(recUnion.children()).thenReturn(ImmutableList.of(scan));`.
Consider adding a second non-scan child (e.g., WorkTableReference mock) to
verify that recursive-case slot references are correctly skipped by the
null-check in the handler.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]