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

starocean999 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 08203ba4140 [fix](mtmv) Fix query err when calc mv functional 
dependency which has variant and log more detailed info for troubleshoot a 
problem (#59933)
08203ba4140 is described below

commit 08203ba414012d4ee97ad8a5859626dae839a877
Author: seawinde <[email protected]>
AuthorDate: Tue Mar 24 14:23:19 2026 +0800

    [fix](mtmv) Fix query err when calc mv functional dependency which has 
variant and log more detailed info for troubleshoot a problem (#59933)
    
    Fix query err when calc mv fd by catch it and log more detailed info for
    troubleshoot a problem
    
    1. originOutputs.size() should equlas targetOutputs.size(), if not would
    throw exception, this would cause query err,
    should log err log and not cause query err
    2. current log could not find the root cause, so add detail log
    3. fix the problem by mapping the slot full path name between scan mv
    output and mv sql plan output
    
    for example as following
    ```sql
      CREATE TABLE fact_var (
        k INT,
        v VARIANT
      ) ENGINE=OLAP
      DUPLICATE KEY(k)
      DISTRIBUTED BY HASH(k) BUCKETS 1
      PROPERTIES ("replication_num" = "1");
    
      INSERT INTO fact_var VALUES
      (1, '{"a":1,"b":{"c":10,"d":20}}'),
      (2, '{"a":2,"b":{"c":30}}');
    
      CREATE MATERIALIZED VIEW mv_var
            BUILD IMMEDIATE REFRESH COMPLETE ON MANUAL
            DISTRIBUTED BY RANDOM BUCKETS 2
            PROPERTIES ('replication_num' = '1')
            AS
      SELECT k, v
      FROM fact_var;
    ```
    
    if run query as fllowing, mv_var sacn would return k, v, v['a'] but
    mv_var def plan sql, would return k, v,
    the size is different but also can work after the fix
    ```sql
    SELECT v['a'] AS c_val FROM mv_var;
    ```
    
    Related PR: #40106
---
 .../trees/plans/logical/LogicalOlapScan.java       |  70 +++++--
 .../trees/plans/logical/LogicalOlapScanTest.java   | 222 +++++++++++++++++++++
 2 files changed, 278 insertions(+), 14 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
index 13d055d01be..2186445d189 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScan.java
@@ -63,8 +63,10 @@ import org.json.JSONObject;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -903,10 +905,9 @@ public class LogicalOlapScan extends 
LogicalCatalogRelation implements OlapScan,
                 scoreRangeInfo, annOrderKeys, annLimit, tableAlias));
     }
 
-    private Map<Slot, Slot> constructReplaceMap(MTMV mtmv) {
+    @VisibleForTesting
+    Map<Slot, Slot> constructReplaceMap(MTMV mtmv) {
         Map<Slot, Slot> replaceMap = new HashMap<>();
-        // Need remove invisible column, and then mapping them
-        List<Slot> originOutputs = new ArrayList<>();
         MTMVCache cache;
         try {
             cache = mtmv.getOrGenerateCache(ConnectContext.get());
@@ -914,21 +915,62 @@ public class LogicalOlapScan extends 
LogicalCatalogRelation implements OlapScan,
             LOG.warn(String.format("LogicalOlapScan constructReplaceMap fail, 
mv name is %s", mtmv.getName()), e);
             return replaceMap;
         }
-        for (Slot originSlot : cache.getOriginalFinalPlan().getOutput()) {
-            if (!(originSlot instanceof SlotReference) || (((SlotReference) 
originSlot).isVisible())) {
-                originOutputs.add(originSlot);
+        // Get MV plan's visible output slots (ordered, matching MV definition 
SELECT list).
+        // This includes all visible slots: regular columns AND variant 
subPath columns
+        // like payload['issue'] that are real physical columns of the MV.
+        List<Slot> mvPlanVisibleOutputs = new ArrayList<>();
+        for (Slot slot : cache.getOriginalFinalPlan().getOutput()) {
+            if (slot instanceof SlotReference && ((SlotReference) 
slot).isVisible()) {
+                mvPlanVisibleOutputs.add(slot);
             }
         }
-        List<Slot> targetOutputs = new ArrayList<>();
-        for (Slot targeSlot : getOutput()) {
-            if (!(targeSlot instanceof SlotReference) || (((SlotReference) 
targeSlot).isVisible())) {
-                targetOutputs.add(targeSlot);
+        // Get MV table's visible physical columns (ordered).
+        // getBaseSchema() returns visible-only columns whose names are 
derived from the
+        // CREATE MV AS SELECT aliases. These names are guaranteed unique per 
table.
+        // Using physical column names as keys (instead of plan slot's 
originalColumn.getName())
+        // correctly handles:
+        // - Aliased columns (e.g. SELECT sum_total AS agg3): key is "agg3", 
not "sum_total"
+        // - Self-join MVs: physical column names are unique even if source 
columns collide
+        // - Variant columns (e.g. SELECT payload['issue']): they are physical 
columns in getBaseSchema()
+        List<Column> mvPhysicalColumns = mtmv.getBaseSchema();
+        if (mvPlanVisibleOutputs.size() != mvPhysicalColumns.size()) {
+            LOG.error("LogicalOlapScan constructReplaceMap: MV plan visible 
output size {} "
+                    + "doesn't match physical column size {} for mv {}",
+                    mvPlanVisibleOutputs.size(), mvPhysicalColumns.size(), 
mtmv.getName());
+            // not throw exception here to avoid query failed, compute mv fd 
should not influence query process
+            return Collections.emptyMap();
+        }
+        // Build mvOutputsMap: the i-th visible plan output corresponds to the 
i-th physical column.
+        Map<List<String>, Slot> mvOutputsMap = new HashMap<>();
+        for (int i = 0; i < mvPlanVisibleOutputs.size(); i++) {
+            String physicalColName = 
mvPhysicalColumns.get(i).getName().toLowerCase(Locale.ROOT);
+            List<String> key = Lists.newArrayList(physicalColName);
+            mvOutputsMap.put(key, mvPlanVisibleOutputs.get(i));
+        }
+        // Match scan output slots against mvOutputsMap.
+        // Scan slot's originalColumn.getName() refers to the MV's physical 
column, so keys match.
+        // Extra subPath slots added by VariantSubPathPruning during query 
optimization won't
+        // match any mvOutputsMap entry (their keys include subPath elements) 
and are safely skipped.
+        for (Slot scanSlot : getOutput()) {
+            if (scanSlot instanceof SlotReference && ((SlotReference) 
scanSlot).isVisible()) {
+                SlotReference scanRef = (SlotReference) scanSlot;
+                String scanName = 
scanRef.getOriginalColumn().map(Column::getName).orElse(scanRef.getName());
+                List<String> key = 
Lists.newArrayList(scanName.toLowerCase(Locale.ROOT));
+                key.addAll(scanRef.getSubPath());
+                Slot mvMappingSlot = mvOutputsMap.get(key);
+                if (mvMappingSlot != null) {
+                    replaceMap.put(mvMappingSlot, scanSlot);
+                }
             }
         }
-        Preconditions.checkArgument(originOutputs.size() == 
targetOutputs.size(),
-                "constructReplaceMap, the size of originOutputs and 
targetOutputs should be same");
-        for (int i = 0; i < targetOutputs.size(); i++) {
-            replaceMap.put(originOutputs.get(i), targetOutputs.get(i));
+        // Every MV plan slot must be mapped
+        if (mvOutputsMap.size() != replaceMap.size()) {
+            LOG.error(String.format("LogicalOlapScan constructReplaceMap size 
not match,"
+                    + "mv name is %s, mvOutputsMap is %s, mv output is %s, 
scan output is %s",
+                    mtmv.getName(), mvOutputsMap,
+                    cache.getOriginalFinalPlan().getOutput(), getOutput()));
+            // not throw exception here to avoid query failed, compute mv fd 
should not influence query process
+            return Collections.emptyMap();
         }
         return replaceMap;
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScanTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScanTest.java
new file mode 100644
index 00000000000..32ecb65ee98
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/logical/LogicalOlapScanTest.java
@@ -0,0 +1,222 @@
+// 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.logical;
+
+import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.MTMV;
+import org.apache.doris.catalog.OlapTable;
+import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.mtmv.MTMVCache;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.plans.Plan;
+import org.apache.doris.nereids.trees.plans.RelationId;
+import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.SessionVariable;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Tests for LogicalOlapScan
+ */
+public class LogicalOlapScanTest {
+
+    private ConnectContext connectContext;
+    private MockedStatic<ConnectContext> mockedConnectContext;
+
+    @BeforeEach
+    public void setUp() {
+        connectContext = new ConnectContext();
+        connectContext.setSessionVariable(new SessionVariable());
+        mockedConnectContext = Mockito.mockStatic(ConnectContext.class);
+        
mockedConnectContext.when(ConnectContext::get).thenReturn(connectContext);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        if (mockedConnectContext != null) {
+            mockedConnectContext.close();
+        }
+    }
+
+    private SlotReference createMockSlot(String name, boolean isVisible) {
+        SlotReference slot = Mockito.mock(SlotReference.class);
+        Mockito.when(slot.isVisible()).thenReturn(isVisible);
+        Mockito.when(slot.getName()).thenReturn(name);
+        Mockito.when(slot.getSubPath()).thenReturn(Collections.emptyList());
+        Mockito.when(slot.getOriginalColumn()).thenReturn(Optional.empty());
+        return slot;
+    }
+
+    private SlotReference createMockSlot(String name, String 
originalColumnName,
+            List<String> subPath, boolean isVisible) {
+        SlotReference slot = Mockito.mock(SlotReference.class);
+        Column column = createColumn(originalColumnName);
+        Mockito.when(slot.isVisible()).thenReturn(isVisible);
+        Mockito.when(slot.getName()).thenReturn(name);
+        Mockito.when(slot.getSubPath()).thenReturn(subPath);
+        Mockito.when(slot.getOriginalColumn()).thenReturn(Optional.of(column));
+        return slot;
+    }
+
+    private Column createColumn(String name) {
+        return new Column(name, PrimitiveType.INT);
+    }
+
+    private LogicalOlapScan createMockScan(List<Slot> outputSlots) {
+        OlapTable olapTable = Mockito.mock(OlapTable.class);
+        Mockito.when(olapTable.getId()).thenReturn(1L);
+        Mockito.when(olapTable.getName()).thenReturn("test_table");
+        
Mockito.when(olapTable.getFullQualifiers()).thenReturn(ImmutableList.of("db", 
"test_table"));
+
+        LogicalOlapScan scan = Mockito.spy(new LogicalOlapScan(
+                new RelationId(1),
+                olapTable,
+                ImmutableList.of("db"),
+                Collections.emptyList(),
+                Collections.emptyList(),
+                Optional.empty(),
+                Collections.emptyList()
+        ));
+        Mockito.doReturn(outputSlots).when(scan).getOutput();
+        return scan;
+    }
+
+    /**
+     * Test constructReplaceMap returns empty map when MV plan output size
+     * doesn't match physical column size.
+     */
+    @Test
+    public void testConstructReplaceMapSizeMismatch() throws Exception {
+        MTMV mtmv = Mockito.mock(MTMV.class);
+        MTMVCache cache = Mockito.mock(MTMVCache.class);
+        Plan originalPlan = Mockito.mock(Plan.class);
+
+        // MV plan has 3 visible output slots
+        List<Slot> originOutputs = ImmutableList.of(
+                createMockSlot("col1", true),
+                createMockSlot("col2", true),
+                createMockSlot("col3", true));
+
+        Mockito.when(mtmv.getOrGenerateCache(Mockito.any())).thenReturn(cache);
+        Mockito.when(mtmv.getName()).thenReturn("test_mv");
+        Mockito.when(cache.getOriginalFinalPlan()).thenReturn(originalPlan);
+        Mockito.when(originalPlan.getOutput()).thenReturn(originOutputs);
+
+        // But MV physical table only has 2 columns (size mismatch with plan 
output)
+        Mockito.when(mtmv.getBaseSchema()).thenReturn(ImmutableList.of(
+                createColumn("col1"), createColumn("col2")));
+
+        LogicalOlapScan scan = createMockScan(ImmutableList.of(
+                createMockSlot("col1", "col1", Collections.emptyList(), true),
+                createMockSlot("col2", "col2", Collections.emptyList(), 
true)));
+
+        Map<Slot, Slot> replaceMap = scan.constructReplaceMap(mtmv);
+
+        Assertions.assertTrue(replaceMap.isEmpty(),
+                "replaceMap should be empty when plan output size doesn't 
match physical column size");
+    }
+
+    /**
+     * Test constructReplaceMap ignores extra subPath slots in scan output
+     * (added by VariantSubPathPruning during query optimization).
+     */
+    @Test
+    public void testConstructReplaceMapIgnoresExtraScanSlots() throws 
Exception {
+        MTMV mtmv = Mockito.mock(MTMV.class);
+        MTMVCache cache = Mockito.mock(MTMVCache.class);
+        Plan originalPlan = Mockito.mock(Plan.class);
+
+        SlotReference mvSlot = createMockSlot("col1", true);
+        Mockito.when(mtmv.getOrGenerateCache(Mockito.any())).thenReturn(cache);
+        Mockito.when(mtmv.getName()).thenReturn("test_mv");
+        Mockito.when(cache.getOriginalFinalPlan()).thenReturn(originalPlan);
+        
Mockito.when(originalPlan.getOutput()).thenReturn(ImmutableList.of(mvSlot));
+        
Mockito.when(mtmv.getBaseSchema()).thenReturn(ImmutableList.of(createColumn("col1")));
+
+        // Scan has base slot + extra subPath slot from VariantSubPathPruning
+        SlotReference scanSlotBase = createMockSlot("col1", "col1", 
Collections.emptyList(), true);
+        SlotReference scanSlotHelper = createMockSlot("col1", "col1", 
Arrays.asList("a", "b"), true);
+        LogicalOlapScan scan = createMockScan(ImmutableList.of(scanSlotBase, 
scanSlotHelper));
+
+        Map<Slot, Slot> replaceMap = scan.constructReplaceMap(mtmv);
+
+        Assertions.assertEquals(1, replaceMap.size());
+        Assertions.assertSame(scanSlotBase, replaceMap.get(mvSlot));
+    }
+
+    /**
+     * Test constructReplaceMap correctly handles aliased columns.
+     * MV SQL: SELECT l_orderkey, sum_total AS agg3, max_total AS agg4 FROM mv1
+     * Plan slots have originalColumn names from source table (sum_total, 
max_total),
+     * but MV physical columns are named agg3, agg4 (the aliases).
+     * Physical column name is used as the key, so the mapping succeeds.
+     */
+    @Test
+    public void testConstructReplaceMapWithAliasedColumns() throws Exception {
+        MTMV mtmv = Mockito.mock(MTMV.class);
+        MTMVCache cache = Mockito.mock(MTMVCache.class);
+        Plan originalPlan = Mockito.mock(Plan.class);
+
+        // MV plan output: slot names are from source table.
+        // Use 2-arg createMockSlot (no originalColumn) to avoid nested mock 
Column objects
+        // causing Mockito UnfinishedStubbingException. constructReplaceMap 
only calls
+        // getOriginalColumn() on scan slots, not on MV plan output slots.
+        SlotReference mvSlot1 = createMockSlot("l_orderkey", true);
+        SlotReference mvSlot2 = createMockSlot("sum_total", true);
+        SlotReference mvSlot3 = createMockSlot("max_total", true);
+
+        Mockito.when(mtmv.getOrGenerateCache(Mockito.any())).thenReturn(cache);
+        Mockito.when(mtmv.getName()).thenReturn("test_alias_mv");
+        Mockito.when(cache.getOriginalFinalPlan()).thenReturn(originalPlan);
+        
Mockito.when(originalPlan.getOutput()).thenReturn(ImmutableList.of(mvSlot1, 
mvSlot2, mvSlot3));
+
+        // Physical columns have aliased names
+        Mockito.when(mtmv.getBaseSchema()).thenReturn(ImmutableList.of(
+                createColumn("l_orderkey"),
+                createColumn("agg3"),  // aliased from sum_total
+                createColumn("agg4")   // aliased from max_total
+        ));
+
+        // Scan slots reference MV's physical column names
+        SlotReference scanSlot1 = createMockSlot("l_orderkey", "l_orderkey", 
Collections.emptyList(), true);
+        SlotReference scanSlot2 = createMockSlot("agg3", "agg3", 
Collections.emptyList(), true);
+        SlotReference scanSlot3 = createMockSlot("agg4", "agg4", 
Collections.emptyList(), true);
+        LogicalOlapScan scan = createMockScan(ImmutableList.of(scanSlot1, 
scanSlot2, scanSlot3));
+
+        Map<Slot, Slot> replaceMap = scan.constructReplaceMap(mtmv);
+
+        // All 3 should be mapped despite plan slots having different names 
than scan slots
+        Assertions.assertEquals(3, replaceMap.size());
+        Assertions.assertSame(scanSlot1, replaceMap.get(mvSlot1));
+        Assertions.assertSame(scanSlot2, replaceMap.get(mvSlot2));
+        Assertions.assertSame(scanSlot3, replaceMap.get(mvSlot3));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to