diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 39ccf1d6e..7cff02f48 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -116,6 +116,7 @@ import { buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, + collectDuplicateTableColumnComments, duplicateTableStructureRequiresScript, supportsDropTableCascade, supportsTruncateTableCascade, @@ -3100,6 +3101,19 @@ function isDuplicateStructureSource(node: TreeNode): node is DuplicateStructureS return node.type === "table" && !!node.connectionId && !!node.database; } +/** Dameng CTAS does not copy comments; load column comments for COMMENT ON COLUMN. */ +async function loadDamengDuplicateColumnComments(connectionId: string, database: string, schema: string | undefined, sourceName: string, catalog?: string, sourceColumns?: ColumnInfo[]): Promise<{ columns?: ColumnInfo[]; columnComments: Array<{ name: string; comment: string }> }> { + let columns = sourceColumns; + if (!columns) { + try { + columns = await api.getColumns(connectionId, database, schema || "", sourceName, catalog); + } catch (error) { + console.warn(`Failed to load Dameng column comments for table clone: ${sourceName}`, error); + } + } + return { columns, columnComments: collectDuplicateTableColumnComments(columns ?? []) }; +} + async function confirmDuplicateStructure() { const node = duplicateStructureSource.value || (isDuplicateStructureSource(activeNode.value) ? activeNode.value : null); const newName = duplicateTableName.value.trim(); @@ -3109,12 +3123,14 @@ async function confirmDuplicateStructure() { try { await connectionStore.ensureConnected(node.connectionId); const databaseType = databaseTypeForNode(node); + const columnComments = databaseType === "dameng" ? (await loadDamengDuplicateColumnComments(node.connectionId, node.database, node.schema, node.label, node.catalog)).columnComments : []; const sql = await buildDuplicateTableStructureSql({ databaseType, schema: node.schema, sourceName: node.label, targetName: newName, tableComment: node.comment, + columnComments, }); await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, @@ -3155,13 +3171,21 @@ async function confirmPasteTable() { try { await connectionStore.ensureConnected(entry.connectionId); const databaseType = entry.connectionId ? effectiveDatabaseTypeForConnection(connectionStore.getConfig(entry.connectionId)) : undefined; + let sourceColumns: ColumnInfo[] | undefined; if (mode === "structure-and-data" || mode === "structure-only") { + let columnComments: Array<{ name: string; comment: string }> = []; + if (databaseType === "dameng") { + const loaded = await loadDamengDuplicateColumnComments(entry.connectionId, entry.database, entry.schema, entry.sourceName); + sourceColumns = loaded.columns; + columnComments = loaded.columnComments; + } const structureSql = await buildDuplicateTableStructureSql({ databaseType, schema: entry.schema, sourceName: entry.sourceName, targetName, tableComment: entry.tableComment, + columnComments, }); const structureExecuted = await executeTreeNodeSqlWithProductionGuard(entry, structureSql, { database: entry.database, @@ -3176,7 +3200,9 @@ async function confirmPasteTable() { queueRefreshTarget(entry); } if (copyData) { - const sourceColumns = await api.getColumns(entry.connectionId, entry.database, entry.schema || "", entry.sourceName); + if (!sourceColumns) { + sourceColumns = await api.getColumns(entry.connectionId, entry.database, entry.schema || "", entry.sourceName); + } const dataCopyColumnOptions = tableDataCopyColumnOptions(databaseType, sourceColumns); if (dataCopyColumnOptions.columns.length === 0) { throw new Error("No writable columns available for table data copy."); diff --git a/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts b/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts index 76528d1c2..d705f9bb6 100644 --- a/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts +++ b/apps/desktop/src/components/sidebar/__tests__/crossDatabaseTablePaste.spec.ts @@ -33,4 +33,13 @@ describe("cross-database table paste", () => { expect(runtimeSource).toMatch(/targetName,\s*tableComment: entry\.tableComment/); expect(runtimeSource).toContain("executeAsScript: duplicateTableStructureRequiresScript(structureSql)"); }); + + it("loads Dameng column comments for sidebar duplicate and paste structure clone", () => { + expect(runtimeSource).toContain("collectDuplicateTableColumnComments"); + expect(runtimeSource).toContain("async function loadDamengDuplicateColumnComments("); + expect(runtimeSource).toMatch(/databaseType === "dameng"[\s\S]*?loadDamengDuplicateColumnComments\([\s\S]*?node\.connectionId[\s\S]*?columnComments/); + expect(runtimeSource).toMatch(/if \(databaseType === "dameng"\) \{[\s\S]*?loadDamengDuplicateColumnComments\([\s\S]*?entry\.connectionId[\s\S]*?columnComments = loaded\.columnComments/); + expect(runtimeSource).toMatch(/tableComment: node\.comment,\s*columnComments,/); + expect(runtimeSource).toMatch(/tableComment: entry\.tableComment,\s*columnComments,/); + }); }); diff --git a/crates/dbx-core/src/db_admin_sql.rs b/crates/dbx-core/src/db_admin_sql.rs index 46cd63fb9..1ccb597f4 100644 --- a/crates/dbx-core/src/db_admin_sql.rs +++ b/crates/dbx-core/src/db_admin_sql.rs @@ -794,6 +794,7 @@ fn supports_duplicate_table_comment(database_type: DatabaseType) -> bool { | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss + | DatabaseType::Dameng ) } @@ -875,6 +876,11 @@ fn quote_sql_string(value: &str) -> String { } fn quote_duplicate_table_comment(database_type: DatabaseType, value: &str) -> String { + // Dameng rejects Postgres E'...' escape strings; keep standard '' escaping + // (same as DamengAgent COMMENT ON / COMMENT ON COLUMN generation). + if database_type == DatabaseType::Dameng { + return quote_sql_string(value); + } if !value.contains('\\') && !value.chars().any(|character| character.is_ascii_control()) { return quote_sql_string(value); } @@ -1624,7 +1630,7 @@ mod tests { schema: Some("APP".to_string()), source_name: "USERS".to_string(), target_name: "users_copy".to_string(), - table_comment: None, + table_comment: Some("测试'克隆".to_string()), column_comments: vec![ DuplicateTableColumnComment { name: "DISPLAY\"NAME".to_string(), @@ -1636,16 +1642,31 @@ 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 TABLE \"APP\".USERS_COPY IS '测试''克隆';\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 TABLE \"APP\".USERS_COPY IS '测试''克隆'".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(), ] ); + // Control chars / backslashes must not switch Dameng to Postgres E'...' literals. + let dameng_escape_sql = build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { + database_type: Some(DatabaseType::Dameng), + schema: Some("APP".to_string()), + source_name: "USERS".to_string(), + target_name: "users_copy".to_string(), + table_comment: Some("line1\\path\nline2".to_string()), + column_comments: vec![], + }); + assert_eq!( + dameng_escape_sql, + "CREATE TABLE \"APP\".USERS_COPY AS SELECT * FROM \"APP\".\"USERS\" WHERE 1=0;\nCOMMENT ON TABLE \"APP\".USERS_COPY IS 'line1\\path\nline2';" + ); + assert!(!dameng_escape_sql.contains("E'")); assert_eq!( build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions { database_type: Some(DatabaseType::Dameng), diff --git a/docs/data/contributors.json b/docs/data/contributors.json index 0eadc7266..befea309d 100644 --- a/docs/data/contributors.json +++ b/docs/data/contributors.json @@ -1622,6 +1622,15 @@ "mergedPullRequests": 1, "firstContributionAt": "2026-06-23T05:56:48Z", "latestContributionAt": "2026-06-23T17:44:46Z" + }, + { + "login": "TangTangH", + "avatarUrl": "https://avatars.githubusercontent.com/u/31877158?v=4", + "profileUrl": "https://github.com/TangTangH", + "commits": 0, + "mergedPullRequests": 1, + "firstContributionAt": "2026-08-04T04:55:17Z", + "latestContributionAt": "2026-08-04T18:59:34Z" } ] }