From e33d8acb960e1744b3fe433192b630949dd24974 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sat, 1 Aug 2026 23:43:58 +0800 Subject: [PATCH] fix(dameng): normalize copied table target names --- .../src/components/objects/ObjectBrowser.vue | 1 + .../ObjectBrowser.crossDatabasePaste.spec.ts | 1 + .../sidebar/SidebarTreeRuntimeHost.vue | 1 + .../__tests__/crossDatabaseTablePaste.spec.ts | 1 + apps/desktop/src/lib/database/dbAdminSql.ts | 1 + crates/dbx-core/src/db_admin_sql.rs | 77 ++++++++++++++++--- 6 files changed, 73 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 30cc49e3d..93b04f5a6 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -2115,6 +2115,7 @@ async function confirmPasteTable() { schema, sourceName: entry.sourceName, targetName, + normalizeNewTargetName: mode === "structure-and-data", ...dataCopyColumnOptions, }); const executed = await executeObjectBrowserSqlWithProductionGuard(dataSql, () => api.executeQuery(props.connection.id, props.database, dataSql, schema)); diff --git a/apps/desktop/src/components/objects/__tests__/ObjectBrowser.crossDatabasePaste.spec.ts b/apps/desktop/src/components/objects/__tests__/ObjectBrowser.crossDatabasePaste.spec.ts index 4ff9e6816..ef2a6d7ce 100644 --- a/apps/desktop/src/components/objects/__tests__/ObjectBrowser.crossDatabasePaste.spec.ts +++ b/apps/desktop/src/components/objects/__tests__/ObjectBrowser.crossDatabasePaste.spec.ts @@ -16,5 +16,6 @@ describe("ObjectBrowser cross-database table paste", () => { expect(objectBrowserSource).toContain("if (canTransferTableClipboard()) {"); expect(objectBrowserSource).toContain("if (!canPasteTableClipboard() && !canTransferTableClipboard()) return;"); expect(objectBrowserSource).toContain("pasteTableMode.value = defaultPasteTableMode(effectiveDatabaseType.value);"); + expect(objectBrowserSource).toContain('normalizeNewTargetName: mode === "structure-and-data"'); }); }); diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 7f9097527..d92fefd9c 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -3075,6 +3075,7 @@ async function confirmPasteTable() { schema: entry.schema, sourceName: entry.sourceName, targetName, + normalizeNewTargetName: mode === "structure-and-data", ...dataCopyColumnOptions, }); const dataExecuted = await executeTreeNodeSqlWithProductionGuard(entry, dataSql, { database: entry.database, schema: entry.schema }); diff --git a/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts b/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts index 21970e77e..cd010cc11 100644 --- a/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts +++ b/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts @@ -16,6 +16,7 @@ describe("cross-database table paste", () => { it("retains the local duplicate-table paste path", () => { expect(runtimeSource).toContain("if (canTransferTreeClipboardToCurrentNode()) return openTransferFromTreeClipboard();"); expect(runtimeSource).toContain("pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());"); + expect(runtimeSource).toContain('normalizeNewTargetName: mode === "structure-and-data"'); }); it("carries table comments through the local sidebar paste path", () => { diff --git a/apps/desktop/src/lib/database/dbAdminSql.ts b/apps/desktop/src/lib/database/dbAdminSql.ts index cff13afa6..ff77cec46 100644 --- a/apps/desktop/src/lib/database/dbAdminSql.ts +++ b/apps/desktop/src/lib/database/dbAdminSql.ts @@ -74,6 +74,7 @@ export interface CopyTableDataSqlOptions { columns?: string[]; postgresOverridingSystemValue?: boolean; sqlserverIdentityInsert?: boolean; + normalizeNewTargetName?: boolean; } export function buildDropObjectSql(options: DropObjectSqlOptions): Promise { diff --git a/crates/dbx-core/src/db_admin_sql.rs b/crates/dbx-core/src/db_admin_sql.rs index c5bac4d52..46cd63fb9 100644 --- a/crates/dbx-core/src/db_admin_sql.rs +++ b/crates/dbx-core/src/db_admin_sql.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use crate::models::connection::DatabaseType; -use crate::sql_dialect::{is_schema_aware, qualified_table_name, quote_table_identifier}; +use crate::sql_dialect::{is_schema_aware, profile_for, qualified_table_name, quote_table_identifier}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] @@ -190,6 +190,8 @@ pub struct CopyTableDataSqlOptions { pub postgres_overriding_system_value: bool, #[serde(default)] pub sqlserver_identity_insert: bool, + #[serde(default)] + pub normalize_new_target_name: bool, } const MYSQL_COMPATIBLE_PROFILES: &[&str] = &["mysql", "mariadb", "tidb", "oceanbase", "custom_mysql"]; @@ -599,7 +601,8 @@ pub fn build_drop_schema_sql(options: SchemaNameSqlOptions) -> String { pub fn build_duplicate_table_structure_sql(options: DuplicateTableStructureSqlOptions) -> String { let source = qualified_name(options.database_type, options.schema.as_deref(), &options.source_name); - let target = qualified_name(options.database_type, options.schema.as_deref(), &options.target_name); + let target = + qualified_duplicate_target_name(options.database_type, options.schema.as_deref(), &options.target_name); let structure_sql = if options.database_type == Some(DatabaseType::Mysql) { format!("CREATE TABLE {target} LIKE {source};") } else if options.database_type == Some(DatabaseType::Questdb) { @@ -642,7 +645,11 @@ pub fn build_duplicate_table_structure_sql(options: DuplicateTableStructureSqlOp pub fn build_copy_table_data_sql(options: CopyTableDataSqlOptions) -> String { let source = qualified_name(options.database_type, options.schema.as_deref(), &options.source_name); - let target = qualified_name(options.database_type, options.schema.as_deref(), &options.target_name); + let target = if options.normalize_new_target_name { + qualified_duplicate_target_name(options.database_type, options.schema.as_deref(), &options.target_name) + } else { + qualified_name(options.database_type, options.schema.as_deref(), &options.target_name) + }; let Some(columns) = options.columns.filter(|columns| !columns.is_empty()) else { return format!("INSERT INTO {target} SELECT * FROM {source};"); }; @@ -821,6 +828,18 @@ fn qualified_name(database_type: Option, schema: Option<&str>, nam } } +fn qualified_duplicate_target_name(database_type: Option, schema: Option<&str>, name: &str) -> String { + if database_type != Some(DatabaseType::Dameng) { + return qualified_name(database_type, schema, name); + } + let target = profile_for(DatabaseType::Dameng).quote_ident(name); + if schema.is_some_and(|schema| !schema.is_empty()) { + format!("{}.{}", quote_rename_identifier(database_type, schema.unwrap()), target) + } else { + target + } +} + fn iotdb_timeseries_pattern(path: &str) -> String { let path = path.trim().trim_end_matches(';'); if path.ends_with(".*") || path.ends_with(".**") { @@ -1604,7 +1623,7 @@ mod tests { database_type: Some(DatabaseType::Dameng), schema: Some("APP".to_string()), source_name: "USERS".to_string(), - target_name: "USERS_COPY".to_string(), + target_name: "users_copy".to_string(), table_comment: None, column_comments: vec![ DuplicateTableColumnComment { @@ -1617,17 +1636,27 @@ mod tests { }); assert_eq!( dameng_sql, - "CREATE TABLE \"APP\".\"USERS_COPY\" AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0;\nCOMMENT ON COLUMN \"APP\".\"USERS_COPY\".\"DISPLAY\"\"NAME\" IS ' Owner''s; display name';\nCOMMENT ON COLUMN \"APP\".\"USERS_COPY\".\"STATUS\" IS 'active ';" + "CREATE TABLE \"APP\".USERS_COPY AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0;\nCOMMENT ON COLUMN \"APP\".USERS_COPY.\"DISPLAY\"\"NAME\" IS ' Owner''s; display name';\nCOMMENT ON COLUMN \"APP\".USERS_COPY.\"STATUS\" IS 'active ';" ); assert_eq!( crate::sql::split_sql_statements_for_database(&dameng_sql, DatabaseType::Dameng), vec![ - "CREATE TABLE \"APP\".\"USERS_COPY\" AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0".to_string(), - "COMMENT ON COLUMN \"APP\".\"USERS_COPY\".\"DISPLAY\"\"NAME\" IS ' Owner''s; display name'" - .to_string(), - "COMMENT ON COLUMN \"APP\".\"USERS_COPY\".\"STATUS\" IS 'active '".to_string(), + "CREATE TABLE \"APP\".USERS_COPY AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0".to_string(), + "COMMENT ON COLUMN \"APP\".USERS_COPY.\"DISPLAY\"\"NAME\" IS ' Owner''s; display name'".to_string(), + "COMMENT ON COLUMN \"APP\".USERS_COPY.\"STATUS\" IS 'active '".to_string(), ] ); + assert_eq!( + build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { + database_type: Some(DatabaseType::Dameng), + schema: Some("APP".to_string()), + source_name: "USERS".to_string(), + target_name: "UsersCopy".to_string(), + table_comment: None, + column_comments: vec![], + }), + "CREATE TABLE \"APP\".\"UsersCopy\" AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0" + ); for database_type in [ DatabaseType::Postgres, DatabaseType::Redshift, @@ -1692,6 +1721,7 @@ mod tests { columns: None, postgres_overriding_system_value: false, sqlserver_identity_insert: false, + normalize_new_target_name: false, }), "INSERT INTO \"users_copy\" SELECT * FROM \"users\";" ); @@ -1704,6 +1734,7 @@ mod tests { columns: Some(vec!["id".to_string(), "name".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: false, + normalize_new_target_name: false, }), "INSERT INTO `users_copy` (`id`, `name`) SELECT `id`, `name` FROM `users`;" ); @@ -1716,6 +1747,7 @@ mod tests { columns: Some(vec!["id".to_string(), "name".to_string()]), postgres_overriding_system_value: true, sqlserver_identity_insert: false, + normalize_new_target_name: false, }), "INSERT INTO \"public\".\"users_copy\" (\"id\", \"name\") OVERRIDING SYSTEM VALUE SELECT \"id\", \"name\" FROM \"public\".\"users\";" ); @@ -1728,9 +1760,36 @@ mod tests { columns: Some(vec!["id".to_string(), "name".to_string()]), postgres_overriding_system_value: false, sqlserver_identity_insert: true, + normalize_new_target_name: false, }), "SET IDENTITY_INSERT [dbo].[users_copy] ON;\nINSERT INTO [dbo].[users_copy] ([id], [name]) SELECT [id], [name] FROM [dbo].[users];\nSET IDENTITY_INSERT [dbo].[users_copy] OFF;" ); + assert_eq!( + build_copy_table_data_sql(CopyTableDataSqlOptions { + database_type: Some(DatabaseType::Dameng), + schema: Some("APP".to_string()), + source_name: "users".to_string(), + target_name: "users_copy".to_string(), + columns: None, + postgres_overriding_system_value: false, + sqlserver_identity_insert: false, + normalize_new_target_name: true, + }), + "INSERT INTO \"APP\".USERS_COPY SELECT * FROM \"APP\".\"users\";" + ); + assert_eq!( + build_copy_table_data_sql(CopyTableDataSqlOptions { + database_type: Some(DatabaseType::Dameng), + schema: Some("APP".to_string()), + source_name: "users".to_string(), + target_name: "users_copy".to_string(), + columns: None, + postgres_overriding_system_value: false, + sqlserver_identity_insert: false, + normalize_new_target_name: false, + }), + "INSERT INTO \"APP\".\"users_copy\" SELECT * FROM \"APP\".\"users\";" + ); } #[test]