Repository: alibaba/GraphScope (language: python)

Issue:
[BUG] Unexpected `ExpandE` and `GetV` fusion 
**Describe the bug**

Currently in the new compilation stack, we will fused EdgeExpand and GetVertex when the edges output by EdgeExpand is not necessary. However, this may occur a bug in the following query on LDBC:
```
MATCH (friend:PERSON)<-[:HASCREATOR]-(comment:COMMENT)
Return count(friend)
```
In this case, `EdgeExpand(type=HASCREATOR, OPT=EDGE)` and `GetV(type=COMMENT, OPT=SRCV)` are fused into `EdgeExpand(type=HASCREATOR, OPT=VERTEX)`, while the fused edge expansion actually expands `HASCREATOR-COMMENT` and `HASCREATOR-POST`, and it is not as expected.

This should fixed by defining edge type as a triplet (instead of a single type_id).



Discussion / hints:
/cc @BingqingLyu, this issus/pr has had no activity for for a long time, could you folks help to review the status ? 
 To suppress further notifications,
 - for issues, 
   - if it is waiting for further response from the reporter/author, please help to add the label `requires-further-info`, 
   - if you have already started working on it, please add the label `work-in-progress` to the issue, 
   - if this issue requires further designing discussion and not in current plan, or won't be fixed, please add the label `requires-further-discussion` or `wontfix` to the issue, 
 - for pull requests, 
   - if you are still working on it and it is not ready for reviewing, please convert this pull request as draft PR, 
   - if you have decided to hold this development on, please add the `requires-further-discussion` label to the pull request. 
 Thanks!

Fix patch:
diff --git a/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/planner/rules/ExpandGetVFusionRule.java b/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/planner/rules/ExpandGetVFusionRule.java
index 8b885b086d0c..5ba60dee80ca 100644
--- a/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/planner/rules/ExpandGetVFusionRule.java
+++ b/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/planner/rules/ExpandGetVFusionRule.java
@@ -16,6 +16,7 @@
 
 package com.alibaba.graphscope.common.ir.planner.rules;
 
+import com.alibaba.graphscope.common.ir.meta.schema.GraphOptTable;
 import com.alibaba.graphscope.common.ir.rel.graph.GraphLogicalExpand;
 import com.alibaba.graphscope.common.ir.rel.graph.GraphLogicalGetV;
 import com.alibaba.graphscope.common.ir.rel.graph.GraphLogicalPathExpand;
@@ -23,10 +24,12 @@
 import com.alibaba.graphscope.common.ir.rel.graph.GraphPhysicalGetV;
 import com.alibaba.graphscope.common.ir.tools.AliasInference;
 import com.alibaba.graphscope.common.ir.tools.config.GraphOpt;
+import com.alibaba.graphscope.common.ir.type.GraphLabelType;
 import com.google.common.collect.ImmutableList;
 
 import org.apache.calcite.plan.GraphOptCluster;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.rules.TransformationRule;
@@ -34,6 +37,10 @@
 import org.apache.commons.lang3.ObjectUtils;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
 // This rule try to fuse GraphLogicalExpand and GraphLogicalGetV if GraphLogicalExpand has no alias
 // (that it won't be visited individually later):
 // 1. if GraphLogicalGetV has no filters, then:
@@ -57,7 +64,7 @@ protected RelNode transform(GraphLogicalGetV getV, GraphLogicalExpand expand, Re
                         && getV.getOpt().equals(GraphOpt.GetV.START)
                 || expand.getOpt().equals(GraphOpt.Expand.BOTH)
                         && getV.getOpt().equals(GraphOpt.GetV.OTHER)) {
-            if (ObjectUtils.isEmpty(getV.getFilters())) {
+            if (canFuse(getV, expand)) {
                 GraphPhysicalExpand physicalExpand =
                         GraphPhysicalExpand.create(
                                 expand.getCluster(),
@@ -95,6 +102,107 @@ protected RelNode transform(GraphLogicalGetV getV, GraphLogicalExpand expand, Re
         }
     }
 
+    private boolean canFuse(GraphLogicalGetV getV, GraphLogicalExpand expand) {
+        // If GraphLogicalGetV has filters, then we cannot fuse them directly. Instead, we create a
+        // EdgeExpand(V) with an Auxilia for the filters.
+        // If GraphLogicalGetV has no filters, then:
+        // 1. if we want to expand "person-knows->person", and the type in getV is "person",
+        // we can fuse them into one EdgeExpand with type "knows" (where "knows" will be given in
+        // EdgeExpand's QueryParam in PhysicalPlan)
+        // 2. if we want to expand "person-create->post", while in schema, a "create" actually
+        // consists of "person-create->post" and "person-create->comment",
+        // we do not fuse them directly. Instead, we create a EdgeExpand(V) with type "create" and
+        // an Auxilia with type "post" as the filter.
+        // 3. if we want to expand "person-islocatedin->city", while in schema, a "islocatedin"
+        // actually
+        // consists of "person-islocatedin->city" and "post-islocatedin->country".
+        // Thought the edge type of "islocatedin" may generate "city" and "country", we can still
+        // fuse them into a single EdgeExpand(V) with type "islocatedin" directly if we can confirm
+        // that the expand starts from "person".
+        // 4. a special case is that, currently, for gremlin query like g.V().out("create"), we have
+        // not infer precise types for getV yet (getV may contain all vertex types).
+        // In this case, if getV's types contains all the types that expand will generate, we can
+        // fuse them.
+
+        Set<Integer> edgeExpandedVLabels = new HashSet<>();
+        // the optTables in expand preserves the full schema information for the edges,
+        // that is, for edge type "create", it contains both "person-create->post" and
+        // "person-create->comment", "user-create->post" etc.
+        List<RelOptTable> optTables = expand.getTableConfig().getTables();
+        // the edgeParamLabels in expand preserves the inferred schema information for the edges,
+        // that is, for edge type "create", it contains only "person-create->post" if user queries
+        // like g.V().hasLabel("person").out("create").hasLabel("post")
+        GraphLabelType edgeParamType =
+                com.alibaba.graphscope.common.ir.tools.Utils.getGraphLabels(expand.getRowType());
+        List<GraphLabelType.Entry> edgeParamLabels = edgeParamType.getLabelsEntry();
+        GraphOpt.Expand direction = expand.getOpt();
+        // First, we get all the source vertex types where the edge will be expanded from.
+        // e.g., expand from "person"
+        Set<Integer> edgeExpandedSrcVLabels = new HashSet<>();
+        for (GraphLabelType.Entry edgeLabel : edgeParamLabels) {
+            switch (direction) {
+                case OUT:
+                    edgeExpandedSrcVLabels.add(edgeLabel.getSrcLabelId());
+                    break;
+                case IN:
+                    edgeExpandedSrcVLabels.add(edgeLabel.getDstLabelId());
+                    break;
+                case BOTH:
+                    edgeExpandedSrcVLabels.add(edgeLabel.getDstLabelId());
+                    edgeExpandedSrcVLabels.add(edgeLabel.getSrcLabelId());
+                    break;
+            }
+        }
+        // Then, we get all the destination vertex types where the edge will be expanded to.
+        // e.g., expand "likes"
+        for (RelOptTable optTable : optTables) {
+            if (optTable instanceof GraphOptTable) {
+                GraphOptTable graphOptTable = (GraphOptTable) optTable;
+                List<GraphLabelType.Entry> edgeUserGivenParamLabels =
+                        com.alibaba.graphscope.common.ir.tools.Utils.getGraphLabels(
+                                        graphOptTable.getRowType())
+                                .getLabelsEntry();
+                for (GraphLabelType.Entry edgeLabel : edgeUserGivenParamLabels) {
+                    switch (direction) {
+                        case OUT:
+                            if (edgeExpandedSrcVLabels.contains(edgeLabel.getSrcLabelId())) {
+                                edgeExpandedVLabels.add(edgeLabel.getDstLabelId());
+                            }
+                            break;
+                        case IN:
+                            if (edgeExpandedSrcVLabels.contains(edgeLabel.getDstLabelId())) {
+                                edgeExpandedVLabels.add(edgeLabel.getSrcLabelId());
+                            }
+                            break;
+                        case BOTH:
+                            if (edgeExpandedSrcVLabels.contains(edgeLabel.getSrcLabelId())) {
+                                edgeExpandedVLabels.add(edgeLabel.getDstLabelId());
+                            }
+                            if (edgeExpandedSrcVLabels.contains(edgeLabel.getDstLabelId())) {
+                                edgeExpandedVLabels.add(edgeLabel.getSrcLabelId());
+                            }
+                            break;
+                    }
+                }
+            }
+        }
+
+        // Finally, we check if the vertex types in getV to see if the type filter for the expanded
+        // vertex is necessary.
+        //  e.g., if getV is "post" and expand type is "likes", then we cannot fuse them directly.
+        // Instead, we should create an EdgeExpand(V) with type "likes" and an Auxilia with type
+        // "post" as the filter.
+        List<GraphLabelType.Entry> vertexParamLabels =
+                com.alibaba.graphscope.common.ir.tools.Utils.getGraphLabels(getV.getRowType())
+                        .getLabelsEntry();
+        Set<Integer> vertexExpandedVLabels = new HashSet<>();
+        for (GraphLabelType.Entry vertexLabel : vertexParamLabels) {
+            vertexExpandedVLabels.add(vertexLabel.getLabelId());
+        }
+        return ObjectUtils.isEmpty(getV.getFilters())
+                && vertexExpandedVLabels.containsAll(edgeExpandedVLabels);
+    }
+
     // transform expande + getv to GraphPhysicalExpandGetV
     public static class BasicExpandGetVFusionRule
             extends ExpandGetVFusionRule<BasicExpandGetVFusionRule.Config> {
diff --git a/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/GraphRelToProtoConverter.java b/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/GraphRelToProtoConverter.java
index b98667944f1f..53e920b3a355 100644
--- a/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/GraphRelToProtoConverter.java
+++ b/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/runtime/proto/GraphRelToProtoConverter.java
@@ -33,7 +33,6 @@
 import com.alibaba.graphscope.common.ir.tools.config.GraphOpt.PhysicalGetVOpt;
 import com.alibaba.graphscope.common.ir.type.GraphLabelType;
 import com.alibaba.graphscope.common.ir.type.GraphNameOrId;
-import com.alibaba.graphscope.common.ir.type.GraphSchemaType;
 import com.alibaba.graphscope.gaia.proto.GraphAlgebra;
 import com.alibaba.graphscope.gaia.proto.GraphAlgebraPhysical;
 import com.alibaba.graphscope.gaia.proto.OuterExpression;
@@ -170,7 +169,10 @@ public RelNode visit(GraphLogicalGetV getV) {
                     Utils.protoGetVOpt(PhysicalGetVOpt.valueOf(getV.getOpt().name())));
             // 1. build adjV without filter
             GraphAlgebra.QueryParams.Builder adjParamsBuilder = defaultQueryParams();
-            addQueryTables(adjParamsBuilder, getGraphLabels(getV).getLabelsEntry());
+            addQueryTables(
+                    adjParamsBuilder,
+                    com.alibaba.graphscope.common.ir.tools.Utils.getGraphLabels(getV.getRowType())
+                            .getLabelsEntry());
             adjVertexBuilder.setParams(adjParamsBuilder);
             if (getV.getStartAlias().getAliasId() != AliasInference.DEFAULT_ID) {
                 adjVertexBuilder.setTag(Utils.asAliasId(getV.getStartAlias().getAliasId()));
@@ -975,16 +977,6 @@ private GraphAlgebra.IndexPredicate buildIndexPredicates(RexNode uniqueKeyFilter
         return indexPredicate;
     }
 
-    private GraphLabelType getGraphLabels(AbstractBindableTableScan tableScan) {
-        List<RelDataTypeField> fields = tableScan.getRowType().getFieldList();
-        Preconditions.checkArgument(
-                !fields.isEmpty() && fields.get(0).getType() instanceof GraphSchemaType,
-                "data type of graph operators should be %s ",
-                GraphSchemaType.class);
-        GraphSchemaType schemaType = (GraphSchemaType) fields.get(0).getType();
-        return schemaType.getLabelType();
-    }
-
     private GraphAlgebra.QueryParams.Builder defaultQueryParams() {
         GraphAlgebra.QueryParams.Builder paramsBuilder = GraphAlgebra.QueryParams.newBuilder();
         // TODO: currently no sample rate fused into tableScan, so directly set 1.0 as default.
@@ -1022,7 +1014,10 @@ private void addQueryColumns(
 
     private GraphAlgebra.QueryParams.Builder buildQueryParams(AbstractBindableTableScan tableScan) {
         GraphAlgebra.QueryParams.Builder paramsBuilder = defaultQueryParams();
-        addQueryTables(paramsBuilder, getGraphLabels(tableScan).getLabelsEntry());
+        addQueryTables(
+                paramsBuilder,
+                com.alibaba.graphscope.common.ir.tools.Utils.getGraphLabels(tableScan.getRowType())
+                        .getLabelsEntry());
         addQueryFilters(paramsBuilder, tableScan.getFilters());
         return paramsBuilder;
     }
diff --git a/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/Utils.java b/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/Utils.java
index 0d38252fa7e9..3e5c72e610d0 100644
--- a/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/Utils.java
+++ b/interactive_engine/compiler/src/main/java/com/alibaba/graphscope/common/ir/tools/Utils.java
@@ -18,6 +18,9 @@
 
 import com.alibaba.graphscope.common.ir.meta.schema.CommonOptTable;
 import com.alibaba.graphscope.common.ir.rel.CommonTableScan;
+import com.alibaba.graphscope.common.ir.type.GraphLabelType;
+import com.alibaba.graphscope.common.ir.type.GraphSchemaType;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Range;
 import com.google.common.collect.Sets;
@@ -81,6 +84,20 @@ public static List<Comparable> getValuesAsList(Comparable value) {
         return values;
     }
 
+    public static GraphLabelType getGraphLabels(RelDataType rowType) {
+        if (rowType instanceof GraphSchemaType) {
+            return ((GraphSchemaType) rowType).getLabelType();
+        } else {
+            List<RelDataTypeField> fields = rowType.getFieldList();
+            Preconditions.checkArgument(
+                    !fields.isEmpty() && fields.get(0).getType() instanceof GraphSchemaType,
+                    "data type of graph operators should be %s ",
+                    GraphSchemaType.class);
+            GraphSchemaType schemaType = (GraphSchemaType) fields.get(0).getType();
+            return schemaType.getLabelType();
+        }
+    }
+
     /**
      * print root {@code RelNode} and nested {@code RelNode}s in each {@code CommonTableScan}
      * @param node
diff --git a/interactive_engine/executor/ir/runtime/src/process/operator/map/get_v.rs b/interactive_engine/executor/ir/runtime/src/process/operator/map/get_v.rs
index c8eee6a06f12..359f818b0bfd 100644
--- a/interactive_engine/executor/ir/runtime/src/process/operator/map/get_v.rs
+++ b/interactive_engine/executor/ir/runtime/src/process/operator/map/get_v.rs
@@ -156,7 +156,15 @@ impl FilterMapFunction<Record, Record> for AuxiliaOperator {
                     // pruning by labels
                     return Ok(None);
                 } else if !self.query_params.has_predicates() && !self.query_params.has_columns() {
-                    // if only filter by labels, directly return the results.
+                    // if only filter by labels, return the results.
+                    // if it has alias, append without moving head
+                    if let Some(alias) = self.alias {
+                        // append without moving head
+                        let entry_clone = entry.clone();
+                        input
+                            .get_columns_mut()
+                            .insert(alias as usize, entry_clone);
+                    }
                     return Ok(Some(input));
                 }
             }


Test patch:
diff --git a/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/BITest.java b/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/BITest.java
index a70d3eafcc78..479d4514704f 100644
--- a/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/BITest.java
+++ b/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/BITest.java
@@ -196,13 +196,15 @@ public void bi3_test() {
                     + "                GraphPhysicalExpand(tableConfig=[{isAll=false,"
                     + " tables=[HASMODERATOR]}], alias=[forum], startAlias=[person], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "                  GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN,"
-                    + " PERSON, CITY)]], alias=[person], startAlias=[PATTERN_VERTEX$1], opt=[IN],"
+                    + "                  GraphPhysicalGetV(tableConfig=[{isAll=false,"
+                    + " tables=[PERSON]}], alias=[person], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "                    GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN,"
+                    + " PERSON, CITY)]], alias=[_], startAlias=[PATTERN_VERTEX$1], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "                    GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISPARTOF,"
+                    + "                      GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISPARTOF,"
                     + " CITY, COUNTRY)]], alias=[PATTERN_VERTEX$1], startAlias=[country], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "                      GraphLogicalSource(tableConfig=[{isAll=false,"
+                    + "                        GraphLogicalSource(tableConfig=[{isAll=false,"
                     + " tables=[COUNTRY]}], alias=[country], fusedFilter=[[=(_.name,"
                     + " _UTF-8'China')]], opt=[VERTEX])",
                 com.alibaba.graphscope.common.ir.tools.Utils.toString(after).trim());
@@ -258,10 +260,12 @@ public void bi5_test() {
                     + "            GraphPhysicalExpand(tableConfig=[{isAll=false,"
                     + " tables=[HASCREATOR]}], alias=[person], startAlias=[message], opt=[OUT],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "              GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT,"
-                    + " TAG), EdgeLabel(HASTAG, POST, TAG)]], alias=[message], startAlias=[tag],"
+                    + "              GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST,"
+                    + " COMMENT]}], alias=[message], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "                GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT,"
+                    + " TAG), EdgeLabel(HASTAG, POST, TAG)]], alias=[_], startAlias=[tag],"
                     + " opt=[IN], physicalOpt=[VERTEX])\n"
-                    + "                GraphLogicalSource(tableConfig=[{isAll=false,"
+                    + "                  GraphLogicalSource(tableConfig=[{isAll=false,"
                     + " tables=[TAG]}], alias=[tag], fusedFilter=[[=(_.name, ?0)]], opt=[VERTEX])",
                 com.alibaba.graphscope.common.ir.tools.Utils.toString(after).trim());
     }
@@ -306,10 +310,12 @@ public void bi6_test() {
                     + "            GraphPhysicalExpand(tableConfig=[{isAll=false,"
                     + " tables=[HASCREATOR]}], alias=[person1], startAlias=[message1], opt=[OUT],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "              GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT,"
-                    + " TAG), EdgeLabel(HASTAG, POST, TAG)]], alias=[message1], startAlias=[tag],"
+                    + "              GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST,"
+                    + " COMMENT]}], alias=[message1], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "                GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT,"
+                    + " TAG), EdgeLabel(HASTAG, POST, TAG)]], alias=[_], startAlias=[tag],"
                     + " opt=[IN], physicalOpt=[VERTEX])\n"
-                    + "                GraphLogicalSource(tableConfig=[{isAll=false,"
+                    + "                  GraphLogicalSource(tableConfig=[{isAll=false,"
                     + " tables=[TAG]}], alias=[tag], fusedFilter=[[=(_.name, ?0)]], opt=[VERTEX])",
                 com.alibaba.graphscope.common.ir.tools.Utils.toString(after).trim());
     }
@@ -348,14 +354,18 @@ public void bi7_test() {
                     + " physicalOpt=[VERTEX])\n"
                     + "        GraphPhysicalExpand(tableConfig=[{isAll=false, tables=[REPLYOF]}],"
                     + " alias=[comment], startAlias=[message], opt=[IN], physicalOpt=[VERTEX])\n"
-                    + "          GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT, TAG),"
-                    + " EdgeLabel(HASTAG, POST, TAG)]], alias=[message], startAlias=[tag],"
+                    + "          GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST,"
+                    + " COMMENT]}], alias=[message], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "            GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT,"
+                    + " TAG), EdgeLabel(HASTAG, POST, TAG)]], alias=[_], startAlias=[tag],"
                     + " opt=[IN], physicalOpt=[VERTEX])\n"
-                    + "            GraphLogicalSource(tableConfig=[{isAll=false, tables=[TAG]}],"
+                    + "              GraphLogicalSource(tableConfig=[{isAll=false, tables=[TAG]}],"
                     + " alias=[tag], fusedFilter=[[=(_.name, ?0)]], opt=[VERTEX])\n"
-                    + "      GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT, TAG)]],"
-                    + " alias=[comment], startAlias=[tag], opt=[IN], physicalOpt=[VERTEX])\n"
-                    + "        GraphLogicalSource(tableConfig=[{isAll=false, tables=[TAG]}],"
+                    + "      GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[COMMENT]}],"
+                    + " alias=[comment], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "        GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, COMMENT, TAG)]],"
+                    + " alias=[_], startAlias=[tag], opt=[IN], physicalOpt=[VERTEX])\n"
+                    + "          GraphLogicalSource(tableConfig=[{isAll=false, tables=[TAG]}],"
                     + " alias=[tag], fusedFilter=[[=(_.name, ?0)]], opt=[VERTEX])",
                 after.explain().trim());
     }
@@ -529,12 +539,12 @@ public void bi11_test() {
                     + "        GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN, PERSON,"
                     + " CITY)]], alias=[PATTERN_VERTEX$1], startAlias=[a], opt=[OUT],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "          CommonTableScan(table=[[common#-1096947686]])\n"
+                    + "          CommonTableScan(table=[[common#1257237722]])\n"
                     + "        GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISPARTOF, CITY,"
                     + " COUNTRY)]], alias=[PATTERN_VERTEX$1], startAlias=[s], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "          CommonTableScan(table=[[common#-1096947686]])\n"
-                    + "common#-1096947686:\n"
+                    + "          CommonTableScan(table=[[common#1257237722]])\n"
+                    + "common#1257237722:\n"
                     + "MultiJoin(joinFilter=[=(a, a)], isFullOuterJoin=[false], joinTypes=[[INNER,"
                     + " INNER]], outerJoinConditions=[[NULL, NULL]], projFields=[[ALL, ALL]])\n"
                     + "  GraphLogicalGetV(tableConfig=[{isAll=false, tables=[PERSON]}], alias=[a],"
@@ -542,37 +552,39 @@ public void bi11_test() {
                     + "    GraphLogicalExpand(tableConfig=[{isAll=false, tables=[KNOWS]}],"
                     + " alias=[k1], startAlias=[b], fusedFilter=[[AND(>=(_.creationDate, ?0),"
                     + " <=(_.creationDate, ?1))]], opt=[BOTH])\n"
-                    + "      CommonTableScan(table=[[common#1348927974]])\n"
+                    + "      CommonTableScan(table=[[common#-393103770]])\n"
                     + "  GraphLogicalGetV(tableConfig=[{isAll=false, tables=[PERSON]}], alias=[a],"
                     + " opt=[OTHER])\n"
                     + "    GraphLogicalExpand(tableConfig=[{isAll=false, tables=[KNOWS]}],"
                     + " alias=[k2], startAlias=[c], fusedFilter=[[AND(>=(_.creationDate, ?0),"
                     + " <=(_.creationDate, ?1))]], opt=[BOTH])\n"
-                    + "      CommonTableScan(table=[[common#1348927974]])\n"
-                    + "common#1348927974:\n"
+                    + "      CommonTableScan(table=[[common#-393103770]])\n"
+                    + "common#-393103770:\n"
                     + "MultiJoin(joinFilter=[=(PATTERN_VERTEX$5, PATTERN_VERTEX$5)],"
                     + " isFullOuterJoin=[false], joinTypes=[[INNER, INNER]],"
                     + " outerJoinConditions=[[NULL, NULL]], projFields=[[ALL, ALL]])\n"
                     + "  GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN, PERSON, CITY)]],"
                     + " alias=[PATTERN_VERTEX$5], startAlias=[b], opt=[OUT],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "    CommonTableScan(table=[[common#613791020]])\n"
+                    + "    CommonTableScan(table=[[common#1460176135]])\n"
                     + "  GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISPARTOF, CITY, COUNTRY)]],"
                     + " alias=[PATTERN_VERTEX$5], startAlias=[s], opt=[IN], physicalOpt=[VERTEX])\n"
-                    + "    CommonTableScan(table=[[common#613791020]])\n"
-                    + "common#613791020:\n"
+                    + "    CommonTableScan(table=[[common#1460176135]])\n"
+                    + "common#1460176135:\n"
                     + "GraphLogicalGetV(tableConfig=[{isAll=false, tables=[PERSON]}], alias=[b],"
                     + " opt=[OTHER])\n"
                     + "  GraphLogicalExpand(tableConfig=[{isAll=false, tables=[KNOWS]}],"
                     + " alias=[k3], startAlias=[c], fusedFilter=[[AND(>=(_.creationDate, ?0),"
                     + " <=(_.creationDate, ?1))]], opt=[BOTH])\n"
-                    + "    GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN, PERSON,"
-                    + " CITY)]], alias=[c], startAlias=[PATTERN_VERTEX$9], opt=[IN],"
+                    + "    GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[PERSON]}],"
+                    + " alias=[c], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "      GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN, PERSON,"
+                    + " CITY)]], alias=[_], startAlias=[PATTERN_VERTEX$9], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "      GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISPARTOF, CITY,"
+                    + "        GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISPARTOF, CITY,"
                     + " COUNTRY)]], alias=[PATTERN_VERTEX$9], startAlias=[s], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "        GraphLogicalSource(tableConfig=[{isAll=false, tables=[COUNTRY]}],"
+                    + "          GraphLogicalSource(tableConfig=[{isAll=false, tables=[COUNTRY]}],"
                     + " alias=[s], fusedFilter=[[=(_.name, _UTF-8'India')]], opt=[VERTEX])",
                 com.alibaba.graphscope.common.ir.tools.Utils.toString(after).trim());
     }
diff --git a/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/LdbcTest.java b/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/LdbcTest.java
index d3963ee6314e..9a0e9db8c622 100644
--- a/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/LdbcTest.java
+++ b/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/cbo/LdbcTest.java
@@ -334,13 +334,15 @@ public void ldbc4_test() {
                     + " post]}], values=[[]])\n"
                     + "            GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, POST,"
                     + " TAG)]], alias=[tag], startAlias=[post], opt=[OUT], physicalOpt=[VERTEX])\n"
-                    + "              GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR, POST,"
-                    + " PERSON)]], alias=[post], startAlias=[friend], opt=[IN],"
+                    + "              GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST]}],"
+                    + " alias=[post], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "                GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR,"
+                    + " POST, PERSON)]], alias=[_], startAlias=[friend], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "                GraphPhysicalExpand(tableConfig=[{isAll=false,"
+                    + "                  GraphPhysicalExpand(tableConfig=[{isAll=false,"
                     + " tables=[KNOWS]}], alias=[friend], startAlias=[person], opt=[BOTH],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "                  GraphLogicalSource(tableConfig=[{isAll=false,"
+                    + "                    GraphLogicalSource(tableConfig=[{isAll=false,"
                     + " tables=[PERSON]}], alias=[person], opt=[VERTEX], uniqueKeyFilters=[=(_.id,"
                     + " ?0)])",
                 after.explain().trim());
@@ -388,10 +390,12 @@ public void ldbc5_test() {
                     + " tables=[CONTAINEROF]}], alias=[post], startAlias=[forum], opt=[OUT],"
                     + " physicalOpt=[VERTEX], optional=[true])\n"
                     + "            CommonTableScan(table=[[common#391831169]])\n"
-                    + "          GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR, POST,"
-                    + " PERSON)]], alias=[post], startAlias=[friend], opt=[IN],"
-                    + " physicalOpt=[VERTEX], optional=[true])\n"
-                    + "            CommonTableScan(table=[[common#391831169]])\n"
+                    + "          GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST]}],"
+                    + " alias=[post], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "            GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR, POST,"
+                    + " PERSON)]], alias=[_], startAlias=[friend], opt=[IN], physicalOpt=[VERTEX],"
+                    + " optional=[true])\n"
+                    + "              CommonTableScan(table=[[common#391831169]])\n"
                     + "common#391831169:\n"
                     + "GraphLogicalGetV(tableConfig=[{isAll=false, tables=[FORUM]}], alias=[forum],"
                     + " opt=[START])\n"
@@ -440,17 +444,18 @@ public void ldbc6_test() {
                     + " opt=[END], physicalOpt=[ITSELF])\n"
                     + "          GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, POST, TAG)]],"
                     + " alias=[_], startAlias=[post], opt=[OUT], physicalOpt=[VERTEX])\n"
-                    + "            GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR, POST,"
-                    + " PERSON)]], alias=[post], startAlias=[other], opt=[IN],"
-                    + " physicalOpt=[VERTEX])\n"
-                    + "              GraphLogicalGetV(tableConfig=[{isAll=false, tables=[PERSON]}],"
-                    + " alias=[other], opt=[END])\n"
-                    + "               "
+                    + "            GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST]}],"
+                    + " alias=[post], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "              GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR, POST,"
+                    + " PERSON)]], alias=[_], startAlias=[other], opt=[IN], physicalOpt=[VERTEX])\n"
+                    + "                GraphLogicalGetV(tableConfig=[{isAll=false,"
+                    + " tables=[PERSON]}], alias=[other], opt=[END])\n"
+                    + "                 "
                     + " GraphLogicalPathExpand(fused=[GraphPhysicalExpand(tableConfig=[{isAll=false,"
                     + " tables=[KNOWS]}], alias=[_], opt=[BOTH], physicalOpt=[VERTEX])\n"
                     + "], offset=[1], fetch=[2], path_opt=[ARBITRARY], result_opt=[END_V],"
                     + " alias=[_], start_alias=[person])\n"
-                    + "                  GraphLogicalSource(tableConfig=[{isAll=false,"
+                    + "                    GraphLogicalSource(tableConfig=[{isAll=false,"
                     + " tables=[PERSON]}], alias=[person], opt=[VERTEX], uniqueKeyFilters=[=(_.id,"
                     + " 2199023382370)])",
                 after.explain().trim());
@@ -687,37 +692,43 @@ public void ldbc10_test() {
                     + "              MultiJoin(joinFilter=[=(post1, post1)],"
                     + " isFullOuterJoin=[false], joinTypes=[[INNER, INNER]],"
                     + " outerJoinConditions=[[NULL, NULL]], projFields=[[ALL, ALL]])\n"
-                    + "                GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR,"
-                    + " POST, PERSON)]], alias=[post1], startAlias=[friend], opt=[IN],"
+                    + "                GraphPhysicalGetV(tableConfig=[{isAll=false,"
+                    + " tables=[POST]}], alias=[post1], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "                  GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR,"
+                    + " POST, PERSON)]], alias=[_], startAlias=[friend], opt=[IN],"
                     + " physicalOpt=[VERTEX], optional=[true])\n"
-                    + "                  CommonTableScan(table=[[common#-1774131414]])\n"
-                    + "                GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, POST,"
-                    + " TAG)]], alias=[post1], startAlias=[tag], opt=[IN], physicalOpt=[VERTEX],"
+                    + "                    CommonTableScan(table=[[common#-1626533514]])\n"
+                    + "                GraphPhysicalGetV(tableConfig=[{isAll=false,"
+                    + " tables=[POST]}], alias=[post1], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "                  GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, POST,"
+                    + " TAG)]], alias=[_], startAlias=[tag], opt=[IN], physicalOpt=[VERTEX],"
                     + " optional=[true])\n"
-                    + "                  CommonTableScan(table=[[common#-1774131414]])\n"
+                    + "                    CommonTableScan(table=[[common#-1626533514]])\n"
                     + "            GraphPhysicalExpand(tableConfig=[{isAll=false, tables=[KNOWS]}],"
                     + " alias=[friend], startAlias=[person], opt=[BOTH], physicalOpt=[VERTEX])\n"
                     + "              GraphLogicalSource(tableConfig=[{isAll=false,"
                     + " tables=[PERSON]}], alias=[person], opt=[VERTEX], uniqueKeyFilters=[=(_.id,"
                     + " ?0)])\n"
-                    + "common#-1774131414:\n"
-                    + "GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR, POST, PERSON)]],"
-                    + " alias=[post], startAlias=[friend], opt=[IN], physicalOpt=[VERTEX],"
+                    + "common#-1626533514:\n"
+                    + "GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST]}], alias=[post],"
+                    + " opt=[START], physicalOpt=[ITSELF])\n"
+                    + "  GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR, POST, PERSON)]],"
+                    + " alias=[_], startAlias=[friend], opt=[IN], physicalOpt=[VERTEX],"
                     + " optional=[true])\n"
-                    + "  GraphPhysicalExpand(tableConfig=[{isAll=false, tables=[HASINTEREST]}],"
+                    + "    GraphPhysicalExpand(tableConfig=[{isAll=false, tables=[HASINTEREST]}],"
                     + " alias=[tag], startAlias=[person], opt=[OUT], physicalOpt=[VERTEX],"
                     + " optional=[true])\n"
-                    + "    GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN, PERSON,"
+                    + "      GraphPhysicalExpand(tableConfig=[[EdgeLabel(ISLOCATEDIN, PERSON,"
                     + " PLACE)]], alias=[city], startAlias=[friend], opt=[OUT],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "      GraphLogicalGetV(tableConfig=[{isAll=false, tables=[PERSON]}],"
+                    + "        GraphLogicalGetV(tableConfig=[{isAll=false, tables=[PERSON]}],"
                     + " alias=[friend], opt=[END])\n"
-                    + "       "
+                    + "         "
                     + " GraphLogicalPathExpand(fused=[GraphPhysicalExpand(tableConfig=[{isAll=false,"
                     + " tables=[KNOWS]}], alias=[_], opt=[BOTH], physicalOpt=[VERTEX])\n"
                     + "], offset=[2], fetch=[1], path_opt=[ARBITRARY], result_opt=[END_V],"
                     + " alias=[_], start_alias=[person])\n"
-                    + "          GraphLogicalSource(tableConfig=[{isAll=false, tables=[PERSON]}],"
+                    + "            GraphLogicalSource(tableConfig=[{isAll=false, tables=[PERSON]}],"
                     + " alias=[person], opt=[VERTEX], uniqueKeyFilters=[=(_.id, ?0)])",
                 com.alibaba.graphscope.common.ir.tools.Utils.toString(after).trim());
     }
@@ -826,16 +837,20 @@ public void ldbc12_test() {
                     + "          GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASTAG, POST, TAG)]],"
                     + " alias=[tag], startAlias=[PATTERN_VERTEX$5], opt=[OUT],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "            GraphPhysicalExpand(tableConfig=[[EdgeLabel(REPLYOF, COMMENT,"
-                    + " POST)]], alias=[PATTERN_VERTEX$5], startAlias=[comment], opt=[OUT],"
+                    + "            GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[POST]}],"
+                    + " alias=[PATTERN_VERTEX$5], opt=[END], physicalOpt=[ITSELF])\n"
+                    + "              GraphPhysicalExpand(tableConfig=[[EdgeLabel(REPLYOF, COMMENT,"
+                    + " POST)]], alias=[_], startAlias=[comment], opt=[OUT],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "              GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR,"
-                    + " COMMENT, PERSON)]], alias=[comment], startAlias=[friend], opt=[IN],"
+                    + "                GraphPhysicalGetV(tableConfig=[{isAll=false,"
+                    + " tables=[COMMENT]}], alias=[comment], opt=[START], physicalOpt=[ITSELF])\n"
+                    + "                  GraphPhysicalExpand(tableConfig=[[EdgeLabel(HASCREATOR,"
+                    + " COMMENT, PERSON)]], alias=[_], startAlias=[friend], opt=[IN],"
                     + " physicalOpt=[VERTEX])\n"
-                    + "                GraphPhysicalExpand(tableConfig=[{isAll=false,"
+                    + "                    GraphPhysicalExpand(tableConfig=[{isAll=false,"
                     + " tables=[KNOWS]}], alias=[friend], startAlias=[PATTERN_VERTEX$0],"
                     + " opt=[BOTH], physicalOpt=[VERTEX])\n"
-                    + "                  GraphLogicalSource(tableConfig=[{isAll=false,"
+                    + "                      GraphLogicalSource(tableConfig=[{isAll=false,"
                     + " tables=[PERSON]}], alias=[PATTERN_VERTEX$0], opt=[VERTEX],"
                     + " uniqueKeyFilters=[=(_.id, 2199023382370)])",
                 after.explain().trim());
diff --git a/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/rbo/ExpandGetVFusionTest.java b/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/rbo/ExpandGetVFusionTest.java
index dbfce24c370d..cd71d1a92696 100644
--- a/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/rbo/ExpandGetVFusionTest.java
+++ b/interactive_engine/compiler/src/test/java/com/alibaba/graphscope/common/ir/planner/rbo/ExpandGetVFusionTest.java
@@ -332,6 +332,83 @@ public void expand_getv_fusion_6_test() {
                 after.explain().trim());
     }
 
+    // g.V().hasLabel("person").outE("likes").inV().hasLabel("comment"), can not be fused into a
+    // single GraphPhysicalExpand since we actually have person-likes-comment and person-likes-post.
+    @Test
+    public void expand_getv_fusion_7_test() {
+        GraphBuilder builder = Utils.mockGraphBuilder("schema/ldbc.json");
+        RelNode before =
+                builder.source(
+                                new SourceConfig(
+                                        GraphOpt.Source.VERTEX,
+                                        new LabelConfig(false).addLabel("PERSON")))
+                        .expand(
+                                new ExpandConfig(
+                                        GraphOpt.Expand.OUT,
+                                        new LabelConfig(false).addLabel("LIKES")))
+                        .getV(
+                                new GetVConfig(
+                                        GraphOpt.GetV.END,
+                                        new LabelConfig(false).addLabel("COMMENT")))
+                        .build();
+        Assert.assertEquals(
+                "GraphLogicalGetV(tableConfig=[{isAll=false, tables=[COMMENT]}],"
+                        + " alias=[_], opt=[END])\n"
+                        + "  GraphLogicalExpand(tableConfig=[{isAll=false, tables=[LIKES]}],"
+                        + " alias=[_], opt=[OUT])\n"
+                        + "    GraphLogicalSource(tableConfig=[{isAll=false, tables=[PERSON]}],"
+                        + " alias=[_], opt=[VERTEX])",
+                before.explain().trim());
+        RelOptPlanner planner =
+                Utils.mockPlanner(ExpandGetVFusionRule.BasicExpandGetVFusionRule.Config.DEFAULT);
+        planner.setRoot(before);
+        RelNode after = planner.findBestExp();
+        Assert.assertEquals(
+                "GraphPhysicalGetV(tableConfig=[{isAll=false, tables=[COMMENT]}], alias=[_],"
+                        + " opt=[END], physicalOpt=[ITSELF])\n"
+                        + "  GraphPhysicalExpand(tableConfig=[{isAll=false, tables=[LIKES]}],"
+                        + " alias=[_], opt=[OUT], physicalOpt=[VERTEX])\n"
+                        + "    GraphLogicalSource(tableConfig=[{isAll=false, tables=[PERSON]}],"
+                        + " alias=[_], opt=[VERTEX])",
+                after.explain().trim());
+    }
+
+    // g.V().hasLabel("person").outE("likes").inV(), can be fused into a single GraphPhysicalExpand
+    // even without type infer for GetV
+    @Test
+    public void expand_getv_fusion_8_test() {
+        GraphBuilder builder = Utils.mockGraphBuilder("schema/ldbc.json");
+        RelNode before =
+                builder.source(
+                                new SourceConfig(
+                                        GraphOpt.Source.VERTEX,
+                                        new LabelConfig(false).addLabel("PERSON")))
+                        .expand(
+                                new ExpandConfig(
+                                        GraphOpt.Expand.OUT,
+                                        new LabelConfig(false).addLabel("LIKES")))
+                        .getV(new GetVConfig(GraphOpt.GetV.END, new LabelConfig(true)))
+                        .build();
+        Assert.assertEquals(
+                "GraphLogicalGetV(tableConfig=[{isAll=true, tables=[PERSON, POST, TAG,"
+                    + " ORGANISATION, PLACE, TAGCLASS, COMMENT, FORUM]}], alias=[_], opt=[END])\n"
+                    + "  GraphLogicalExpand(tableConfig=[{isAll=false, tables=[LIKES]}], alias=[_],"
+                    + " opt=[OUT])\n"
+                    + "    GraphLogicalSource(tableConfig=[{isAll=false, tables=[PERSON]}],"
+                    + " alias=[_], opt=[VERTEX])",
+                before.explain().trim());
+        RelOptPlanner planner =
+                Utils.mockPlanner(ExpandGetVFusionRule.BasicExpandGetVFusionRule.Config.DEFAULT);
+        planner.setRoot(before);
+        RelNode after = planner.findBestExp();
+        Assert.assertEquals(
+                "GraphPhysicalExpand(tableConfig=[{isAll=false, tables=[LIKES]}], alias=[_],"
+                        + " opt=[OUT], physicalOpt=[VERTEX])\n"
+                        + "  GraphLogicalSource(tableConfig=[{isAll=false, tables=[PERSON]}],"
+                        + " alias=[_], opt=[VERTEX])",
+                after.explain().trim());
+    }
+
     // path expand: g.V().hasLabel("person").out('1..3', "knows").with('PATH_OPT',
     // SIMPLE).with('RESULT_OPT', ALL_V)
     @Test