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

yujun777 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 ce99e367ba5 [fix](mtmv) CREATE MATERIALIZED VIEW IF NOT EXISTS skips 
existing sync MV (#69370)
ce99e367ba5 is described below

commit ce99e367ba5df943fec1f78d2a0f6667d01885ba
Author: yujun <[email protected]>
AuthorDate: Thu Oct 22 14:18:51 2026 +0800

    [fix](mtmv) CREATE MATERIALIZED VIEW IF NOT EXISTS skips existing sync MV 
(#69370)
    
    `CREATE MATERIALIZED VIEW IF NOT EXISTS` was accepted by the parser for
    synchronous materialized views, but the flag was never propagated to
    `CreateMaterializedViewCommand`, so it was silently ignored. Creating an
    already existing sync MV therefore failed:
    
    - same query: `Duplicate column name 'mv_k1'`
    - different query: `Materialized view[mv1] already exists`
    
    Async materialized views and `CREATE TABLE IF NOT EXISTS` already no-op
    in this case.
    
    This change propagates the flag and, under the base table write lock,
    returns without creating a job when the materialized view index already
    exists. The existence check is placed before the full-schema column
    check so the no-op path does not hit the duplicate-column error. A plain
    `CREATE MATERIALIZED VIEW` keeps the previous behavior.
---
 .../doris/alter/MaterializedViewHandler.java       |  6 ++
 .../doris/nereids/parser/LogicalPlanBuilder.java   |  3 +-
 .../commands/CreateMaterializedViewCommand.java    |  8 ++-
 .../doris/nereids/parser/NereidsParserTest.java    | 15 +++++
 .../test_create_mv_if_not_exists.out               |  9 +++
 .../test_create_mv_if_not_exists.groovy            | 74 ++++++++++++++++++++++
 6 files changed, 113 insertions(+), 2 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/alter/MaterializedViewHandler.java 
b/fe/fe-core/src/main/java/org/apache/doris/alter/MaterializedViewHandler.java
index a4aabb6de98..b98249fc181 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/alter/MaterializedViewHandler.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/alter/MaterializedViewHandler.java
@@ -205,6 +205,12 @@ public class MaterializedViewHandler extends AlterHandler {
             if (olapTable.hasColumnSeqMapping()) {
                 throw new DdlException("Can not add materialized view when 
table use column sequence mapping");
             }
+            if (createMvCommand.isIfNotExists()
+                    && 
olapTable.hasMaterializedIndex(createMvCommand.getMVName())) {
+                LOG.info("materialized view[{}] already exists on table[{}], 
skip creating it",
+                        createMvCommand.getMVName(), olapTable.getName());
+                return;
+            }
             // check no duplicate column name in full schema
             Set<String> allColumnNames = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
             for (Column column : olapTable.getFullSchema()) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
index 63e45cca83a..1a3624a3527 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilder.java
@@ -1711,7 +1711,8 @@ public class LogicalPlanBuilder extends 
DorisParserBaseVisitor<Object> {
         LogicalPlan logicalPlan = new 
UnboundResultSink<>(visitQuery(ctx.query()));
         Map<String, String> properties = ctx.propertyClause() != null
                 ? Maps.newHashMap(visitPropertyClause(ctx.propertyClause())) : 
Maps.newHashMap();
-        return new CreateMaterializedViewCommand(new TableNameInfo(nameParts), 
logicalPlan, properties);
+        return new CreateMaterializedViewCommand(ctx.EXISTS() != null, new 
TableNameInfo(nameParts),
+                logicalPlan, properties);
     }
 
     /**
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateMaterializedViewCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateMaterializedViewCommand.java
index e91ed72f7c8..ae2c59fb181 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateMaterializedViewCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateMaterializedViewCommand.java
@@ -108,6 +108,7 @@ import java.util.stream.Collectors;
  */
 public class CreateMaterializedViewCommand extends Command implements 
ForwardWithSync {
     private static final String SYNC_MV_PLANER_DISABLE_RULES = 
"HAVING_TO_FILTER";
+    private final boolean ifNotExists;
     private final TableNameInfo name;
 
     private final LogicalPlan logicalPlan;
@@ -119,9 +120,10 @@ public class CreateMaterializedViewCommand extends Command 
implements ForwardWit
     private KeysType mvKeysType;
     private OriginStatement originStatement;
 
-    public CreateMaterializedViewCommand(TableNameInfo name, LogicalPlan 
logicalPlan,
+    public CreateMaterializedViewCommand(boolean ifNotExists, TableNameInfo 
name, LogicalPlan logicalPlan,
                                          Map<String, String> properties) {
         super(PlanType.CREATE_MATERIALIZED_VIEW_COMMAND);
+        this.ifNotExists = ifNotExists;
         this.name = name;
         this.logicalPlan = logicalPlan;
         this.properties = properties;
@@ -144,6 +146,10 @@ public class CreateMaterializedViewCommand extends Command 
implements ForwardWit
         return StmtType.CREATE;
     }
 
+    public boolean isIfNotExists() {
+        return ifNotExists;
+    }
+
     public String getMVName() {
         return name.getTbl();
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
index 8ed37d2a37f..635ef0088d8 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/NereidsParserTest.java
@@ -1745,6 +1745,21 @@ public class NereidsParserTest extends ParserTestBase {
         Assertions.assertInstanceOf(CreateMaterializedViewCommand.class, 
logicalPlan);
     }
 
+    @Test
+    public void testCreateSyncMvIfNotExists() {
+        NereidsParser parser = new NereidsParser();
+        LogicalPlan logicalPlan = parser.parseSingle(
+                "CREATE MATERIALIZED VIEW IF NOT EXISTS mv1 SELECT k1 FROM 
t2");
+        CreateMaterializedViewCommand command = Assertions.assertInstanceOf(
+                CreateMaterializedViewCommand.class, logicalPlan);
+        Assertions.assertTrue(command.isIfNotExists());
+
+        LogicalPlan withoutClause = parser.parseSingle("CREATE MATERIALIZED 
VIEW mv1 SELECT k1 FROM t2");
+        CreateMaterializedViewCommand withoutClauseCommand = 
Assertions.assertInstanceOf(
+                CreateMaterializedViewCommand.class, withoutClause);
+        Assertions.assertFalse(withoutClauseCommand.isIfNotExists());
+    }
+
     @Test
     public void testDropTemporaryTable() throws Exception {
         NereidsParser parser = new NereidsParser();
diff --git 
a/regression-test/data/mv_p0/test_create_mv_if_not_exists/test_create_mv_if_not_exists.out
 
b/regression-test/data/mv_p0/test_create_mv_if_not_exists/test_create_mv_if_not_exists.out
new file mode 100644
index 00000000000..d732e365bf5
--- /dev/null
+++ 
b/regression-test/data/mv_p0/test_create_mv_if_not_exists/test_create_mv_if_not_exists.out
@@ -0,0 +1,9 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !select_k1 --
+1      10
+2      20
+
+-- !select_k2 --
+1      10
+2      20
+
diff --git 
a/regression-test/suites/mv_p0/test_create_mv_if_not_exists/test_create_mv_if_not_exists.groovy
 
b/regression-test/suites/mv_p0/test_create_mv_if_not_exists/test_create_mv_if_not_exists.groovy
new file mode 100644
index 00000000000..7fc35aea524
--- /dev/null
+++ 
b/regression-test/suites/mv_p0/test_create_mv_if_not_exists/test_create_mv_if_not_exists.groovy
@@ -0,0 +1,74 @@
+// 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.
+
+suite("test_create_mv_if_not_exists") {
+    sql """ DROP TABLE IF EXISTS t_create_mv_if_not_exists """
+    sql """
+        CREATE TABLE t_create_mv_if_not_exists (
+            k1 INT,
+            k2 INT,
+            v1 BIGINT
+        )
+        DUPLICATE KEY(k1)
+        DISTRIBUTED BY HASH(k1) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """ DROP MATERIALIZED VIEW IF EXISTS mv_create_if_not_exists_k1 ON 
t_create_mv_if_not_exists """
+    sql """ DROP MATERIALIZED VIEW IF EXISTS mv_create_if_not_exists_k2 ON 
t_create_mv_if_not_exists """
+    sql """ INSERT INTO t_create_mv_if_not_exists VALUES (1, 1, 10), (2, 2, 
20) """
+
+    def syncMvIndexId = { String tableName, String mvName ->
+        def dbId = sql("SHOW PROC '/dbs'").find { it[1].toString() == 
context.dbName }[0]
+        def tableId = sql("SHOW PROC '/dbs/${dbId}'").find { it[1].toString() 
== tableName }[0]
+        def rows = sql("SHOW PROC '/dbs/${dbId}/${tableId}/index_schema'")
+        return rows.find { it[1].toString() == mvName }[0].toString()
+    }
+
+    create_sync_mv(context.dbName, "t_create_mv_if_not_exists", 
"mv_create_if_not_exists_k1", """
+        SELECT k1 AS mv_k1, SUM(v1) AS mv_sum_v1
+        FROM t_create_mv_if_not_exists
+        GROUP BY k1""")
+    def k1IndexId = syncMvIndexId("t_create_mv_if_not_exists", 
"mv_create_if_not_exists_k1")
+
+    // Both variants of IF NOT EXISTS must succeed and leave the existing MV 
untouched.
+    sql """
+        CREATE MATERIALIZED VIEW IF NOT EXISTS mv_create_if_not_exists_k1 AS
+        SELECT k1 AS mv_k1, SUM(v1) AS mv_sum_v1
+        FROM t_create_mv_if_not_exists
+        GROUP BY k1
+    """
+    assertEquals(k1IndexId, syncMvIndexId("t_create_mv_if_not_exists", 
"mv_create_if_not_exists_k1"))
+    sql """
+        CREATE MATERIALIZED VIEW IF NOT EXISTS mv_create_if_not_exists_k1 AS
+        SELECT k2 AS mv_k2, SUM(v1) AS mv_sum_v1_2
+        FROM t_create_mv_if_not_exists
+        GROUP BY k2
+    """
+    assertEquals(k1IndexId, syncMvIndexId("t_create_mv_if_not_exists", 
"mv_create_if_not_exists_k1"))
+
+    // IF NOT EXISTS must still create the sync MV when the name is free.
+    sql """
+        CREATE MATERIALIZED VIEW IF NOT EXISTS mv_create_if_not_exists_k2 AS
+        SELECT k2 AS mv_k2, SUM(v1) AS mv_sum_v1_2
+        FROM t_create_mv_if_not_exists
+        GROUP BY k2
+    """
+    waitingMVTaskFinishedByMvName(context.dbName, "t_create_mv_if_not_exists", 
"mv_create_if_not_exists_k2")
+
+    order_qt_select_k1 "SELECT * FROM t_create_mv_if_not_exists INDEX 
mv_create_if_not_exists_k1 ORDER BY 1"
+    order_qt_select_k2 "SELECT * FROM t_create_mv_if_not_exists INDEX 
mv_create_if_not_exists_k2 ORDER BY 1"
+}


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

Reply via email to