Skip to content

Commit ab37738

Browse files
Ryan19929Your Name
authored andcommitted
[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.
1 parent 24a3d05 commit ab37738

File tree

3 files changed

+182
-2
lines changed

3 files changed

+182
-2
lines changed

fe/fe-core/src/main/java/org/apache/doris/catalog/View.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.io.IOException;
3434
import java.util.List;
3535
import java.util.Map;
36+
import java.util.regex.Pattern;
3637

3738
/**
3839
* Table metadata representing a catalog view or a local view from a WITH clause.
@@ -158,8 +159,10 @@ public void resetIdsForRestore(Env env) {
158159
public void resetViewDefForRestore(String srcDbName, String dbName) {
159160
// the source db name is not setted in old BackupMeta, keep compatible with the old one.
160161
if (srcDbName != null) {
161-
// replace dbName with a regular expression
162-
inlineViewDef = inlineViewDef.replaceAll("(?<=`internal`\\.`)([^`]+)(?=`\\.`)", dbName);
162+
// Only replace the source database name, preserve cross-database references
163+
// Pattern: `internal`.`srcDbName`.`table` -> `internal`.`dbName`.`table`
164+
String pattern = "(?<=`internal`\\.`)" + Pattern.quote(srcDbName) + "(?=`\\.`)";
165+
inlineViewDef = inlineViewDef.replaceAll(pattern, dbName);
163166
}
164167
}
165168

fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,4 +229,28 @@ public void testResetViewDefForRestore() {
229229
Assert.assertEquals("SELECT `internal`.`test1`.`test`.`k2` AS `k1`, "
230230
+ "FROM `internal`.`test1`.`test`;", view.getInlineViewDef());
231231
}
232+
233+
@Test
234+
public void testResetViewDefForRestoreWithCrossDbReference() {
235+
// Test that cross-database references are preserved
236+
View view = new View();
237+
// View in db_b references tables from both db_a and db_b
238+
view.setInlineViewDefWithSessionVariables(
239+
"SELECT t1.k1, t2.k2 "
240+
+ "FROM `internal`.`db_a`.`table1` t1 "
241+
+ "JOIN `internal`.`db_b`.`table2` t2 "
242+
+ "ON t1.id = t2.id;",
243+
new HashMap<>());
244+
245+
// Restore db_b to db_b_new
246+
view.resetViewDefForRestore("db_b", "db_b_new");
247+
248+
// db_a reference should be preserved, only db_b should be changed to db_b_new
249+
Assert.assertEquals(
250+
"SELECT t1.k1, t2.k2 "
251+
+ "FROM `internal`.`db_a`.`table1` t1 "
252+
+ "JOIN `internal`.`db_b_new`.`table2` t2 "
253+
+ "ON t1.id = t2.id;",
254+
view.getInlineViewDef());
255+
}
232256
}

regression-test/suites/backup_restore/test_backup_restore_with_view.groovy

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,159 @@ suite("test_backup_restore_with_view", "backup_restore") {
128128
def res = sql "SHOW VIEW FROM ${dbName1}.${tableName}"
129129
assertTrue(res.size() > 0)
130130

131+
// Test cross-database view references preservation
132+
logger.info("========== Testing cross-database view references ==========")
133+
134+
String baseDbName = "${suiteName}_base_db"
135+
String viewDbName = "${suiteName}_view_db"
136+
String restoreDbName = "${suiteName}_restore_db"
137+
String baseTableName = "base_table"
138+
String localTableName = "local_table"
139+
String crossDbViewName = "cross_db_view"
140+
String mixedViewName = "mixed_view"
141+
142+
try {
143+
// Create base database with base table
144+
sql "DROP DATABASE IF EXISTS ${baseDbName} FORCE"
145+
sql "DROP DATABASE IF EXISTS ${viewDbName} FORCE"
146+
sql "DROP DATABASE IF EXISTS ${restoreDbName} FORCE"
147+
148+
sql "CREATE DATABASE ${baseDbName}"
149+
sql "CREATE DATABASE ${viewDbName}"
150+
151+
sql """
152+
CREATE TABLE ${baseDbName}.${baseTableName} (
153+
id INT,
154+
name VARCHAR(100),
155+
value INT
156+
)
157+
DUPLICATE KEY(id)
158+
DISTRIBUTED BY HASH(id) BUCKETS 2
159+
PROPERTIES ("replication_num" = "1")
160+
"""
161+
162+
sql """
163+
CREATE TABLE ${viewDbName}.${localTableName} (
164+
id INT,
165+
category VARCHAR(100)
166+
)
167+
DUPLICATE KEY(id)
168+
DISTRIBUTED BY HASH(id) BUCKETS 2
169+
PROPERTIES ("replication_num" = "1")
170+
"""
171+
172+
sql """
173+
INSERT INTO ${baseDbName}.${baseTableName} VALUES
174+
(1, 'Alice', 100),
175+
(2, 'Bob', 200),
176+
(3, 'Charlie', 300)
177+
"""
178+
179+
sql """
180+
INSERT INTO ${viewDbName}.${localTableName} VALUES
181+
(1, 'TypeA'),
182+
(2, 'TypeB'),
183+
(3, 'TypeC')
184+
"""
185+
186+
// Create cross-database view (references base_db only)
187+
sql """
188+
CREATE VIEW ${viewDbName}.${crossDbViewName} AS
189+
SELECT id, name, value
190+
FROM `internal`.`${baseDbName}`.`${baseTableName}`
191+
WHERE value > 100
192+
"""
193+
194+
// Create mixed view (references both base_db and view_db)
195+
sql """
196+
CREATE VIEW ${viewDbName}.${mixedViewName} AS
197+
SELECT
198+
t1.id,
199+
t1.name,
200+
t1.value,
201+
t2.category
202+
FROM `internal`.`${baseDbName}`.`${baseTableName}` t1
203+
JOIN `internal`.`${viewDbName}`.`${localTableName}` t2
204+
ON t1.id = t2.id
205+
"""
206+
207+
// Verify original views work
208+
def crossDbResult = sql "SELECT * FROM ${viewDbName}.${crossDbViewName} ORDER BY id"
209+
assertTrue(crossDbResult.size() == 2)
210+
assertTrue(crossDbResult[0][0] == 2)
211+
assertTrue(crossDbResult[0][1] == "Bob")
212+
213+
def mixedResult = sql "SELECT * FROM ${viewDbName}.${mixedViewName} ORDER BY id"
214+
assertTrue(mixedResult.size() == 3)
215+
216+
// Backup view_db
217+
String crossDbSnapshot = "${suiteName}_cross_db_snapshot"
218+
sql """
219+
BACKUP SNAPSHOT ${viewDbName}.${crossDbSnapshot}
220+
TO `${repoName}`
221+
"""
222+
223+
syncer.waitSnapshotFinish(viewDbName)
224+
def crossDbSnapshotTs = syncer.getSnapshotTimestamp(repoName, crossDbSnapshot)
225+
assertTrue(crossDbSnapshotTs != null)
226+
logger.info("Cross-DB snapshot timestamp: ${crossDbSnapshotTs}")
227+
228+
// Create target database before restore (FIX: prevent database not exist error)
229+
sql "CREATE DATABASE IF NOT EXISTS ${restoreDbName}"
230+
231+
// Restore to different database
232+
sql """
233+
RESTORE SNAPSHOT ${restoreDbName}.${crossDbSnapshot}
234+
FROM `${repoName}`
235+
PROPERTIES
236+
(
237+
"backup_timestamp" = "${crossDbSnapshotTs}",
238+
"reserve_replica" = "true"
239+
)
240+
"""
241+
242+
syncer.waitAllRestoreFinish(restoreDbName)
243+
244+
// Verify restore success
245+
def restoreResult = sql_return_maparray """ SHOW RESTORE FROM ${restoreDbName} WHERE Label = "${crossDbSnapshot}" """
246+
logger.info("Cross-DB restore result: ${restoreResult}")
247+
assertTrue(restoreResult.last().State == "FINISHED")
248+
249+
// Critical verification: Check view definitions
250+
def crossDbViewDef = sql "SHOW CREATE VIEW ${restoreDbName}.${crossDbViewName}"
251+
logger.info("Cross-DB view definition after restore: ${crossDbViewDef[0][1]}")
252+
253+
// Cross-DB view should still reference base_db, not restore_db
254+
assertTrue(crossDbViewDef[0][1].contains("`${baseDbName}`"),
255+
"Cross-DB view should preserve base_db reference")
256+
257+
// Mixed view should preserve base_db reference but update view_db to restore_db
258+
def mixedViewDef = sql "SHOW CREATE VIEW ${restoreDbName}.${mixedViewName}"
259+
logger.info("Mixed view definition after restore: ${mixedViewDef[0][1]}")
260+
261+
assertTrue(mixedViewDef[0][1].contains("`${baseDbName}`"),
262+
"Mixed view should preserve base_db reference")
263+
assertTrue(mixedViewDef[0][1].contains("`${restoreDbName}`"),
264+
"Mixed view should reference restore_db for local tables")
265+
266+
// Verify views still work after restore
267+
def restoredCrossDbResult = sql "SELECT * FROM ${restoreDbName}.${crossDbViewName} ORDER BY id"
268+
assertTrue(restoredCrossDbResult.size() == 2)
269+
assertTrue(restoredCrossDbResult[0][0] == 2)
270+
assertTrue(restoredCrossDbResult[0][1] == "Bob")
271+
272+
def restoredMixedResult = sql "SELECT * FROM ${restoreDbName}.${mixedViewName} ORDER BY id"
273+
assertTrue(restoredMixedResult.size() == 3)
274+
assertTrue(restoredMixedResult[0][0] == 1)
275+
assertTrue(restoredMixedResult[0][1] == "Alice")
276+
} finally {
277+
// Clean up cross-DB test resources
278+
sql "DROP DATABASE IF EXISTS ${baseDbName} FORCE"
279+
sql "DROP DATABASE IF EXISTS ${viewDbName} FORCE"
280+
sql "DROP DATABASE IF EXISTS ${restoreDbName} FORCE"
281+
}
282+
283+
// Clean up original test resources
131284
sql "DROP TABLE ${dbName}.${tableName} FORCE"
132285
sql "DROP VIEW ${dbName}.${viewName}"
133286
sql "DROP DATABASE ${dbName} FORCE"

0 commit comments

Comments
 (0)