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

w41ter 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 c16527bace4 [fix](restore) Preserve cross-database references when 
restoring views (#59580)
c16527bace4 is described below

commit c16527bace48979b493c5ec59834691286ec8f68
Author: Ryan19929 <[email protected]>
AuthorDate: Thu Jan 8 10:38:52 2026 +0800

    [fix](restore) Preserve cross-database references when restoring views 
(#59580)
    
    When restoring a view to a different database, the current
    implementation incorrectly replaces **all** database names in the view
    definition with the target database name. This breaks cross-database
    references.
    
    **Example:**
    - Original: `view_db` has a view referencing tables from both `base_db`
    and `view_db`
    ```sql
      SELECT * FROM `internal`.`base_db`.`table1`
      JOIN `internal`.`view_db`.`table2`
    ```
      - After restore to `restore_db` (BEFORE this PR):
    ```sql
      SELECT * FROM `internal`.`restore_db`.`table1`  -- ❌ Wrong! Should be 
base_db
      JOIN `internal`.`restore_db`.`table2`           -- ✅ Correct
    ```
      - After restore to `restore_db` (AFTER this PR):
    ```sql
      SELECT * FROM `internal`.`base_db`.`table1`     -- ✅ Correct! Preserved
      JOIN `internal`.`restore_db`.`table2`           -- ✅ Correct
    ```
      **Root Cause:**
    The regex pattern `(?<=\`internal\`\\.\`)([^\`]+)(?=\`\\.\`)` matches
    **any** database name, not just the source database name.
---
 .../main/java/org/apache/doris/catalog/View.java   |   7 +-
 .../org/apache/doris/catalog/CreateViewTest.java   |  24 ++++
 .../test_backup_restore_with_view.groovy           | 153 +++++++++++++++++++++
 3 files changed, 182 insertions(+), 2 deletions(-)

diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/View.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/View.java
index 180969775fb..49e808b086f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/View.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/View.java
@@ -33,6 +33,7 @@ import java.io.DataInput;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Pattern;
 
 /**
  * Table metadata representing a catalog view or a local view from a WITH 
clause.
@@ -158,8 +159,10 @@ public class View extends Table implements 
GsonPostProcessable, ViewIf {
     public void resetViewDefForRestore(String srcDbName, String dbName) {
         // the source db name is not setted in old BackupMeta, keep compatible 
with the old one.
         if (srcDbName != null) {
-            // replace dbName with a regular expression
-            inlineViewDef = 
inlineViewDef.replaceAll("(?<=`internal`\\.`)([^`]+)(?=`\\.`)", dbName);
+            // Only replace the source database name, preserve cross-database 
references
+            // Pattern: `internal`.`srcDbName`.`table` -> 
`internal`.`dbName`.`table`
+            String pattern = "(?<=`internal`\\.`)" + Pattern.quote(srcDbName) 
+ "(?=`\\.`)";
+            inlineViewDef = inlineViewDef.replaceAll(pattern, dbName);
         }
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java
index fc842a35b69..ec17ca18a07 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java
@@ -229,4 +229,28 @@ public class CreateViewTest {
         Assert.assertEquals("SELECT `internal`.`test1`.`test`.`k2` AS `k1`, "
                 + "FROM `internal`.`test1`.`test`;", view.getInlineViewDef());
     }
+
+    @Test
+    public void testResetViewDefForRestoreWithCrossDbReference() {
+        // Test that cross-database references are preserved
+        View view = new View();
+        // View in db_b references tables from both db_a and db_b
+        view.setInlineViewDefWithSessionVariables(
+                "SELECT t1.k1, t2.k2 "
+                + "FROM `internal`.`db_a`.`table1` t1 "
+                + "JOIN `internal`.`db_b`.`table2` t2 "
+                + "ON t1.id = t2.id;",
+                new HashMap<>());
+
+        // Restore db_b to db_b_new
+        view.resetViewDefForRestore("db_b", "db_b_new");
+
+        // db_a reference should be preserved, only db_b should be changed to 
db_b_new
+        Assert.assertEquals(
+                "SELECT t1.k1, t2.k2 "
+                + "FROM `internal`.`db_a`.`table1` t1 "
+                + "JOIN `internal`.`db_b_new`.`table2` t2 "
+                + "ON t1.id = t2.id;",
+                view.getInlineViewDef());
+    }
 }
diff --git 
a/regression-test/suites/backup_restore/test_backup_restore_with_view.groovy 
b/regression-test/suites/backup_restore/test_backup_restore_with_view.groovy
index 91328355eca..a9a92ab6810 100644
--- a/regression-test/suites/backup_restore/test_backup_restore_with_view.groovy
+++ b/regression-test/suites/backup_restore/test_backup_restore_with_view.groovy
@@ -128,6 +128,159 @@ suite("test_backup_restore_with_view", "backup_restore") {
     def res = sql "SHOW VIEW FROM ${dbName1}.${tableName}"
     assertTrue(res.size() > 0)
 
+    // Test cross-database view references preservation
+    logger.info("========== Testing cross-database view references ==========")
+    
+    String baseDbName = "${suiteName}_base_db"
+    String viewDbName = "${suiteName}_view_db"
+    String restoreDbName = "${suiteName}_restore_db"
+    String baseTableName = "base_table"
+    String localTableName = "local_table"
+    String crossDbViewName = "cross_db_view"
+    String mixedViewName = "mixed_view"
+
+    try {
+        // Create base database with base table
+        sql "DROP DATABASE IF EXISTS ${baseDbName} FORCE"
+        sql "DROP DATABASE IF EXISTS ${viewDbName} FORCE"
+        sql "DROP DATABASE IF EXISTS ${restoreDbName} FORCE"
+        
+        sql "CREATE DATABASE ${baseDbName}"
+        sql "CREATE DATABASE ${viewDbName}"
+
+        sql """
+            CREATE TABLE ${baseDbName}.${baseTableName} (
+                id INT,
+                name VARCHAR(100),
+                value INT
+            )
+            DUPLICATE KEY(id)
+            DISTRIBUTED BY HASH(id) BUCKETS 2
+            PROPERTIES ("replication_num" = "1")
+        """
+
+        sql """
+            CREATE TABLE ${viewDbName}.${localTableName} (
+                id INT,
+                category VARCHAR(100)
+            )
+            DUPLICATE KEY(id)
+            DISTRIBUTED BY HASH(id) BUCKETS 2
+            PROPERTIES ("replication_num" = "1")
+        """
+
+        sql """
+            INSERT INTO ${baseDbName}.${baseTableName} VALUES
+            (1, 'Alice', 100),
+            (2, 'Bob', 200),
+            (3, 'Charlie', 300)
+        """
+
+        sql """
+            INSERT INTO ${viewDbName}.${localTableName} VALUES
+            (1, 'TypeA'),
+            (2, 'TypeB'),
+            (3, 'TypeC')
+        """
+
+        // Create cross-database view (references base_db only)
+        sql """
+            CREATE VIEW ${viewDbName}.${crossDbViewName} AS
+            SELECT id, name, value 
+            FROM `internal`.`${baseDbName}`.`${baseTableName}`
+            WHERE value > 100
+        """
+
+        // Create mixed view (references both base_db and view_db)
+        sql """
+            CREATE VIEW ${viewDbName}.${mixedViewName} AS
+            SELECT 
+                t1.id,
+                t1.name,
+                t1.value,
+                t2.category
+            FROM `internal`.`${baseDbName}`.`${baseTableName}` t1
+            JOIN `internal`.`${viewDbName}`.`${localTableName}` t2
+            ON t1.id = t2.id
+        """
+
+        // Verify original views work
+        def crossDbResult = sql "SELECT * FROM 
${viewDbName}.${crossDbViewName} ORDER BY id"
+        assertTrue(crossDbResult.size() == 2)
+        assertTrue(crossDbResult[0][0] == 2)
+        assertTrue(crossDbResult[0][1] == "Bob")
+
+        def mixedResult = sql "SELECT * FROM ${viewDbName}.${mixedViewName} 
ORDER BY id"
+        assertTrue(mixedResult.size() == 3)
+
+        // Backup view_db
+        String crossDbSnapshot = "${suiteName}_cross_db_snapshot"
+        sql """
+            BACKUP SNAPSHOT ${viewDbName}.${crossDbSnapshot}
+            TO `${repoName}`
+        """
+
+        syncer.waitSnapshotFinish(viewDbName)
+        def crossDbSnapshotTs = syncer.getSnapshotTimestamp(repoName, 
crossDbSnapshot)
+        assertTrue(crossDbSnapshotTs != null)
+        logger.info("Cross-DB snapshot timestamp: ${crossDbSnapshotTs}")
+
+        // Create target database before restore (FIX: prevent database not 
exist error)
+        sql "CREATE DATABASE IF NOT EXISTS ${restoreDbName}"
+
+        // Restore to different database
+        sql """
+            RESTORE SNAPSHOT ${restoreDbName}.${crossDbSnapshot}
+            FROM `${repoName}`
+            PROPERTIES
+            (
+                "backup_timestamp" = "${crossDbSnapshotTs}",
+                "reserve_replica" = "true"
+            )
+        """
+
+        syncer.waitAllRestoreFinish(restoreDbName)
+
+        // Verify restore success
+        def restoreResult = sql_return_maparray """ SHOW RESTORE FROM 
${restoreDbName} WHERE Label = "${crossDbSnapshot}" """
+        logger.info("Cross-DB restore result: ${restoreResult}")
+        assertTrue(restoreResult.last().State == "FINISHED")
+
+        // Critical verification: Check view definitions
+        def crossDbViewDef = sql "SHOW CREATE VIEW 
${restoreDbName}.${crossDbViewName}"
+        logger.info("Cross-DB view definition after restore: 
${crossDbViewDef[0][1]}")
+        
+        // Cross-DB view should still reference base_db, not restore_db
+        assertTrue(crossDbViewDef[0][1].contains("`${baseDbName}`"), 
+            "Cross-DB view should preserve base_db reference")
+        
+        // Mixed view should preserve base_db reference but update view_db to 
restore_db
+        def mixedViewDef = sql "SHOW CREATE VIEW 
${restoreDbName}.${mixedViewName}"
+        logger.info("Mixed view definition after restore: 
${mixedViewDef[0][1]}")
+        
+        assertTrue(mixedViewDef[0][1].contains("`${baseDbName}`"),
+            "Mixed view should preserve base_db reference")
+        assertTrue(mixedViewDef[0][1].contains("`${restoreDbName}`"),
+            "Mixed view should reference restore_db for local tables")
+
+        // Verify views still work after restore
+        def restoredCrossDbResult = sql "SELECT * FROM 
${restoreDbName}.${crossDbViewName} ORDER BY id"
+        assertTrue(restoredCrossDbResult.size() == 2)
+        assertTrue(restoredCrossDbResult[0][0] == 2)
+        assertTrue(restoredCrossDbResult[0][1] == "Bob")
+
+        def restoredMixedResult = sql "SELECT * FROM 
${restoreDbName}.${mixedViewName} ORDER BY id"
+        assertTrue(restoredMixedResult.size() == 3)
+        assertTrue(restoredMixedResult[0][0] == 1)
+        assertTrue(restoredMixedResult[0][1] == "Alice")
+    } finally {
+        // Clean up cross-DB test resources
+        sql "DROP DATABASE IF EXISTS ${baseDbName} FORCE"
+        sql "DROP DATABASE IF EXISTS ${viewDbName} FORCE"
+        sql "DROP DATABASE IF EXISTS ${restoreDbName} FORCE"
+    }
+
+    // Clean up original test resources
     sql "DROP TABLE ${dbName}.${tableName} FORCE"
     sql "DROP VIEW ${dbName}.${viewName}"
     sql "DROP DATABASE ${dbName} FORCE"


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

Reply via email to