This is an automated email from the ASF dual-hosted git repository.
Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new cad4abb82d4 [fix](nereids) Fix dead visitor dispatch of
PhysicalLazyMaterializeTVFScan (#68871)
cad4abb82d4 is described below
commit cad4abb82d40336178a96b3d2069aed03a6ef38c
Author: daidai <[email protected]>
AuthorDate: Mon Oct 12 15:32:57 2026 +0800
[fix](nereids) Fix dead visitor dispatch of PhysicalLazyMaterializeTVFScan
(#68871)
### What problem does this PR solve?
Issue Number: N/A
Problem Summary:
`PhysicalLazyMaterializeTVFScan.accept()` dispatched to
`visitPhysicalTVFRelation`, so the dedicated
`PhysicalPlanTranslator.visitPhysicalLazyMaterializeTVFScan` was
unreachable dead code and every TVF scan under TopN lazy materialization
was translated by the parent method.
This PR dispatches `accept()` to `visitPhysicalLazyMaterializeTVFScan`,
consistent with `PhysicalLazyMaterializeOlapScan`, so the dedicated
translation path stays reachable. No behavior change today since both
translator methods are identical, and the added unit test pins the
dispatch.
### Release note
None
### Check List (For Author)
- Test
- [ ] Regression test
- [x] Unit Test
- `PhysicalLazyMaterializeTVFScanVisitorTest` (new), plus
`TopnLazyMaterializeTest`, `MaterializeProbeVisitorTest`,
`LazyMaterializeTopNTest`, `TopNRuntimeFilterTest` (27 tests, all pass)
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- Behavior changed:
- [x] No.
- [ ] Yes.
- Does this need documentation?
- [x] No.
- [ ] Yes.
### Check List (For Reviewer who merge this PR)
- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
---
.../physical/PhysicalLazyMaterializeTVFScan.java | 2 +-
.../physical/PhysicalPlanVisitorDispatchTest.java | 181 +++++++++++++++++++++
2 files changed, 182 insertions(+), 1 deletion(-)
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterializeTVFScan.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterializeTVFScan.java
index 2a6a6343612..0cd411f080c 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterializeTVFScan.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/physical/PhysicalLazyMaterializeTVFScan.java
@@ -85,7 +85,7 @@ public class PhysicalLazyMaterializeTVFScan extends
PhysicalTVFRelation {
@Override
public <R, C> R accept(PlanVisitor<R, C> visitor, C context) {
- return visitor.visitPhysicalTVFRelation(this, context);
+ return visitor.visitPhysicalLazyMaterializeTVFScan(this, context);
}
@Override
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPlanVisitorDispatchTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPlanVisitorDispatchTest.java
new file mode 100644
index 00000000000..ae97dcc2fc2
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/physical/PhysicalPlanVisitorDispatchTest.java
@@ -0,0 +1,181 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.plans.physical;
+
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.visitor.PlanVisitor;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Consistency check of the physical plan visitor dispatch.
+ *
+ * <p>Every plan node that implements {@code accept()} itself hard codes the
{@code visitXxx} method of
+ * {@link PlanVisitor} that handles it. When such a node has a dedicated hook
in {@link PlanVisitor},
+ * the dispatch has to reach that hook: dispatching to a parent type silently
turns the dedicated hook
+ * into unreachable dead code, and nothing catches it - the compiler is happy
and no runtime test fails.
+ *
+ * <p>The check reads the invoked method out of the compiled {@code accept()}
body, so it covers every
+ * physical plan node instead of a hand picked one. Nodes that inherit {@code
accept()} from a parent
+ * plan are out of scope by construction.
+ */
+public class PhysicalPlanVisitorDispatchTest {
+
+ /** Guards against the class path scan silently finding nothing and making
this test vacuous. */
+ private static final int MIN_CHECKED_PLAN_CLASSES = 30;
+
+ private static final String PLAN_PACKAGE =
"org.apache.doris.nereids.trees.plans.physical";
+
+ private static final String ACCEPT_DESCRIPTOR =
+
"(Lorg/apache/doris/nereids/trees/plans/visitor/PlanVisitor;Ljava/lang/Object;)Ljava/lang/Object;";
+
+ @Test
+ public void acceptDispatchesToItsOwnMostSpecificVisitorMethod() throws
Exception {
+ Path classesRoot = classesRoot();
+ Path packageRoot = classesRoot.resolve(PLAN_PACKAGE.replace('.', '/'));
+ List<String> violations = new ArrayList<>();
+ int checked = 0;
+
+ for (Path classFile : planClassFiles(packageRoot)) {
+ Class<?> planClass = Class.forName(toClassName(packageRoot,
classFile), false,
+ PhysicalPlanVisitorDispatchTest.class.getClassLoader());
+ if (!Plan.class.isAssignableFrom(planClass) ||
Modifier.isAbstract(planClass.getModifiers())
+ || !declaresAccept(planClass)) {
+ continue;
+ }
+ String dispatched = dispatchedVisitorMethod(classFile);
+ if (dispatched == null) {
+ // accept() does not dispatch through a visit method, nothing
to verify.
+ continue;
+ }
+ checked++;
+ String expected = mostSpecificVisitorMethod(planClass);
+ if (!dispatched.equals(expected)) {
+ violations.add(planClass.getSimpleName() + ".accept()
dispatches to " + dispatched
+ + "(), but the most specific visitor method is " +
expected + "()");
+ }
+ }
+
+ Assertions.assertTrue(violations.isEmpty(),
+ "accept() dispatches to the wrong visitor method:\n" +
String.join("\n", violations));
+ Assertions.assertTrue(checked >= MIN_CHECKED_PLAN_CLASSES,
+ "only " + checked + " physical plan classes were checked, the
class path scan is broken");
+ }
+
+ private static Path classesRoot() throws Exception {
+ URL location =
PlanVisitor.class.getProtectionDomain().getCodeSource().getLocation();
+ Path root = Paths.get(location.toURI());
+ Assumptions.assumeTrue(Files.isDirectory(root),
+ "plan classes are not available as a directory: " + root);
+ return root;
+ }
+
+ private static List<Path> planClassFiles(Path packageRoot) throws
IOException {
+ Assumptions.assumeTrue(Files.isDirectory(packageRoot), "no plans
directory: " + packageRoot);
+ try (Stream<Path> files = Files.walk(packageRoot)) {
+ return files.filter(path -> path.toString().endsWith(".class"))
+ .filter(path -> !path.toString().contains("$"))
+ .filter(path ->
!path.getFileName().toString().equals("package-info.class"))
+ .collect(Collectors.toList());
+ }
+ }
+
+ private static String toClassName(Path packageRoot, Path classFile) {
+ String relative = packageRoot.relativize(classFile).toString();
+ return PLAN_PACKAGE + "."
+ + relative.substring(0, relative.length() -
".class".length()).replace('/', '.');
+ }
+
+ private static boolean declaresAccept(Class<?> planClass) {
+ try {
+ planClass.getDeclaredMethod("accept", PlanVisitor.class,
Object.class);
+ return true;
+ } catch (NoSuchMethodException e) {
+ return false;
+ }
+ }
+
+ /** Reads the visitor method invoked inside {@code accept()} from the
compiled class file. */
+ private static String dispatchedVisitorMethod(Path classFile) throws
IOException {
+ Set<String> invoked = new LinkedHashSet<>();
+ byte[] bytes;
+ try (InputStream in = Files.newInputStream(classFile)) {
+ bytes = in.readAllBytes();
+ }
+ new ClassReader(bytes).accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String
descriptor,
+ String signature, String[] exceptions) {
+ if (!"accept".equals(name) ||
!ACCEPT_DESCRIPTOR.equals(descriptor)) {
+ return null;
+ }
+ return new MethodVisitor(Opcodes.ASM9) {
+ @Override
+ public void visitMethodInsn(int opcode, String owner,
String methodName,
+ String methodDescriptor, boolean isInterface) {
+ if (methodName.startsWith("visit")) {
+ invoked.add(methodName);
+ }
+ }
+ };
+ }
+ }, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+ return invoked.size() == 1 ? invoked.iterator().next() : null;
+ }
+
+ /**
+ * The visit method whose visited parameter is the closest super type of
{@code planClass}, i.e. the
+ * one declared for this exact plan node when such a hook exists.
+ */
+ private static String mostSpecificVisitorMethod(Class<?> planClass) {
+ Method mostSpecific = null;
+ for (Method method : PlanVisitor.class.getMethods()) {
+ if (!method.getName().startsWith("visit") ||
method.getParameterCount() != 2) {
+ continue;
+ }
+ Class<?> visited = method.getParameterTypes()[0];
+ if (visited.isAssignableFrom(planClass)
+ && (mostSpecific == null
+ ||
mostSpecific.getParameterTypes()[0].isAssignableFrom(visited))) {
+ mostSpecific = method;
+ }
+ }
+ return mostSpecific == null ? null : mostSpecific.getName();
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]