Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions fe/fe-core/src/main/java/org/apache/doris/catalog/View.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import java.io.StringReader;
import java.lang.ref.SoftReference;
import java.util.List;
import java.util.regex.Pattern;

/**
* Table metadata representing a catalog view or a local view from a WITH clause.
Expand Down Expand Up @@ -274,8 +275,10 @@ public void resetIdsForRestore(Env env) {
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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,28 @@ public void testResetViewDefForRestore() {
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.setInlineViewDefWithSqlMode(
"SELECT t1.k1, t2.k2 "
+ "FROM `internal`.`db_a`.`table1` t1 "
+ "JOIN `internal`.`db_b`.`table2` t2 "
+ "ON t1.id = t2.id;",
0L);

// 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down