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]