fix(dameng): preserve cloned column comments
This commit is contained in:
parent
d5e0cde7e2
commit
0b4b1f956f
|
|
@ -60,7 +60,18 @@ import { supportsSchemaDiagram, supportsTableImport, supportsTableStructureEditi
|
|||
import { codeMirrorSqlDialect, connectionObjectTreeNodeSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { getTableMetadataCapabilities, type TableMetadataCapabilities } from "@/lib/table/tableMetadataCapabilities";
|
||||
import { buildTableSelectSql } from "@/lib/table/tableSelectSql";
|
||||
import { buildDropObjectSql, buildDropTableSql, buildDuplicateTableStructureSql, buildCopyTableDataSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, supportsTruncateTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import {
|
||||
buildDropObjectSql,
|
||||
buildDropTableSql,
|
||||
buildDuplicateTableStructureSql,
|
||||
buildCopyTableDataSql,
|
||||
buildEmptyTableSql,
|
||||
buildTruncateTableSql,
|
||||
collectDuplicateTableColumnComments,
|
||||
supportsDropTableCascade,
|
||||
supportsTruncateTableCascade,
|
||||
type TableAdminSqlOptions,
|
||||
} from "@/lib/database/dbAdminSql";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { buildExecutableObjectSourceStatements, buildRoutineRenameObjectSourceStatements, executeObjectSourceSave, formatObjectSourceSaveError, supportsSourceBackedRoutineRename } from "@/lib/table/objectSourceEditor";
|
||||
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/table/objectRenameSql";
|
||||
|
|
@ -1809,6 +1820,30 @@ function requestDuplicateStructure(row: ObjectBrowserRow) {
|
|||
showDuplicateDialog.value = true;
|
||||
}
|
||||
|
||||
async function buildDuplicateStructurePlan(sourceName: string, targetName: string, schema: string | undefined, sourceColumns?: ColumnInfo[]) {
|
||||
let columns = sourceColumns;
|
||||
if (effectiveDatabaseType.value === "dameng" && !columns) {
|
||||
try {
|
||||
columns = await api.getColumns(props.connection.id, props.database, schema || "", sourceName, props.catalog);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load Dameng column comments for table clone: ${sourceName}`, error);
|
||||
}
|
||||
}
|
||||
const columnComments = effectiveDatabaseType.value === "dameng" ? collectDuplicateTableColumnComments(columns ?? []) : [];
|
||||
const sql = await buildDuplicateTableStructureSql({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
schema,
|
||||
sourceName,
|
||||
targetName,
|
||||
columnComments,
|
||||
});
|
||||
return { sql, sourceColumns: columns, executeAsScript: columnComments.length > 0 };
|
||||
}
|
||||
|
||||
function executeDuplicateStructurePlan(plan: { sql: string; executeAsScript: boolean }, schema: string | undefined) {
|
||||
return plan.executeAsScript ? api.executeScript(props.connection.id, props.database, plan.sql, schema) : api.executeQuery(props.connection.id, props.database, plan.sql, schema);
|
||||
}
|
||||
|
||||
async function confirmDuplicateStructure() {
|
||||
const row = duplicateTarget.value;
|
||||
const newName = duplicateTableName.value.trim();
|
||||
|
|
@ -1816,13 +1851,8 @@ async function confirmDuplicateStructure() {
|
|||
showDuplicateDialog.value = false;
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
const sql = await buildDuplicateTableStructureSql({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
schema,
|
||||
sourceName: row.name,
|
||||
targetName: newName,
|
||||
});
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(sql, () => api.executeQuery(props.connection.id, props.database, sql, schema));
|
||||
const plan = await buildDuplicateStructurePlan(row.name, newName, schema);
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(plan.sql, () => executeDuplicateStructurePlan(plan, schema));
|
||||
if (!executed) return;
|
||||
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }));
|
||||
await reload();
|
||||
|
|
@ -1919,18 +1949,15 @@ async function confirmPasteTable() {
|
|||
const targetName = entry.targetName.trim();
|
||||
const schema = entry.schema || selectedSchema.value;
|
||||
try {
|
||||
let sourceColumns: ColumnInfo[] | undefined;
|
||||
if (mode === "structure-and-data" || mode === "structure-only") {
|
||||
const structureSql = await buildDuplicateTableStructureSql({
|
||||
databaseType: effectiveDatabaseType.value,
|
||||
schema,
|
||||
sourceName: entry.sourceName,
|
||||
targetName,
|
||||
});
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(structureSql, () => api.executeQuery(props.connection.id, props.database, structureSql, schema));
|
||||
const plan = await buildDuplicateStructurePlan(entry.sourceName, targetName, schema, sourceColumns);
|
||||
sourceColumns = plan.sourceColumns;
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(plan.sql, () => executeDuplicateStructurePlan(plan, schema));
|
||||
if (!executed) return;
|
||||
}
|
||||
if (copyData) {
|
||||
const sourceColumns = await api.getColumns(props.connection.id, props.database, schema || "", entry.sourceName, props.catalog);
|
||||
sourceColumns ??= await api.getColumns(props.connection.id, props.database, schema || "", entry.sourceName, props.catalog);
|
||||
const dataCopyColumnOptions = tableDataCopyColumnOptions(effectiveDatabaseType.value, sourceColumns);
|
||||
if (dataCopyColumnOptions.columns.length === 0) {
|
||||
throw new Error("No writable columns available for table data copy.");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { collectDuplicateTableColumnComments } from "@/lib/database/dbAdminSql";
|
||||
|
||||
describe("collectDuplicateTableColumnComments", () => {
|
||||
it("preserves meaningful whitespace and excludes whitespace-only comments", () => {
|
||||
expect(
|
||||
collectDuplicateTableColumnComments([
|
||||
{ name: "LEADING", comment: " leading" },
|
||||
{ name: "TRAILING", comment: "trailing " },
|
||||
{ name: "BOTH", comment: " Owner's; display name " },
|
||||
{ name: "WHITESPACE_ONLY", comment: " \t\n" },
|
||||
{ name: "EMPTY", comment: "" },
|
||||
{ name: "NULL", comment: null },
|
||||
]),
|
||||
).toEqual([
|
||||
{ name: "LEADING", comment: " leading" },
|
||||
{ name: "TRAILING", comment: "trailing " },
|
||||
{ name: "BOTH", comment: " Owner's; display name " },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { DatabaseObjectType, DatabaseType } from "@/types/database";
|
||||
import type { ColumnInfo, DatabaseObjectType, DatabaseType } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
|
||||
export interface DropObjectSqlOptions {
|
||||
|
|
@ -55,6 +55,14 @@ export interface DuplicateTableStructureSqlOptions {
|
|||
schema?: string | null;
|
||||
sourceName: string;
|
||||
targetName: string;
|
||||
columnComments?: Array<{ name: string; comment: string }>;
|
||||
}
|
||||
|
||||
export function collectDuplicateTableColumnComments(columns: readonly Pick<ColumnInfo, "name" | "comment">[]): Array<{ name: string; comment: string }> {
|
||||
return columns.flatMap((column) => {
|
||||
const comment = column.comment;
|
||||
return comment?.trim() ? [{ name: column.name, comment }] : [];
|
||||
});
|
||||
}
|
||||
|
||||
export interface CopyTableDataSqlOptions {
|
||||
|
|
|
|||
|
|
@ -162,6 +162,15 @@ pub struct DuplicateTableStructureSqlOptions {
|
|||
pub schema: Option<String>,
|
||||
pub source_name: String,
|
||||
pub target_name: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub column_comments: Vec<DuplicateTableColumnComment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DuplicateTableColumnComment {
|
||||
pub name: String,
|
||||
pub comment: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -582,22 +591,38 @@ 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);
|
||||
if options.database_type == Some(DatabaseType::Mysql) {
|
||||
return format!("CREATE TABLE {target} LIKE {source};");
|
||||
let structure_sql = if options.database_type == Some(DatabaseType::Mysql) {
|
||||
format!("CREATE TABLE {target} LIKE {source};")
|
||||
} else if options.database_type == Some(DatabaseType::Questdb) {
|
||||
format!("CREATE TABLE {target} (LIKE {source});")
|
||||
} else if options.database_type.is_some_and(is_postgres_like_structure_copy) {
|
||||
format!("CREATE TABLE {target} (LIKE {source} INCLUDING ALL);")
|
||||
} else if options.database_type == Some(DatabaseType::SqlServer) {
|
||||
format!("SELECT TOP 0 * INTO {target} FROM {source};")
|
||||
} else if options.database_type.is_some_and(uses_false_predicate_duplicate_structure) {
|
||||
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 1=0")
|
||||
} else {
|
||||
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 0;")
|
||||
};
|
||||
|
||||
if options.database_type != Some(DatabaseType::Dameng) {
|
||||
return structure_sql;
|
||||
}
|
||||
if options.database_type == Some(DatabaseType::Questdb) {
|
||||
return format!("CREATE TABLE {target} (LIKE {source});");
|
||||
let comment_sql = options
|
||||
.column_comments
|
||||
.iter()
|
||||
.filter_map(|column| {
|
||||
if column.comment.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let column_name = quote_table_identifier(options.database_type, &column.name);
|
||||
Some(format!("COMMENT ON COLUMN {target}.{column_name} IS {}", quote_sql_string(&column.comment)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if comment_sql.is_empty() {
|
||||
return structure_sql;
|
||||
}
|
||||
if options.database_type.is_some_and(is_postgres_like_structure_copy) {
|
||||
return format!("CREATE TABLE {target} (LIKE {source} INCLUDING ALL);");
|
||||
}
|
||||
if options.database_type == Some(DatabaseType::SqlServer) {
|
||||
return format!("SELECT TOP 0 * INTO {target} FROM {source};");
|
||||
}
|
||||
if options.database_type.is_some_and(uses_false_predicate_duplicate_structure) {
|
||||
return format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 1=0");
|
||||
}
|
||||
format!("CREATE TABLE {target} AS SELECT * FROM {source} WHERE 0;")
|
||||
format!("{};\n{};", structure_sql.trim_end_matches(';'), comment_sql.join(";\n"))
|
||||
}
|
||||
|
||||
pub fn build_copy_table_data_sql(options: CopyTableDataSqlOptions) -> String {
|
||||
|
|
@ -1419,6 +1444,7 @@ mod tests {
|
|||
schema: None,
|
||||
source_name: "users".to_string(),
|
||||
target_name: "users_copy".to_string(),
|
||||
column_comments: vec![],
|
||||
}),
|
||||
"CREATE TABLE `users_copy` LIKE `users`;"
|
||||
);
|
||||
|
|
@ -1428,6 +1454,7 @@ mod tests {
|
|||
schema: Some("public".to_string()),
|
||||
source_name: "users".to_string(),
|
||||
target_name: "users_copy".to_string(),
|
||||
column_comments: vec![],
|
||||
}),
|
||||
"CREATE TABLE \"public\".\"users_copy\" (LIKE \"public\".\"users\" INCLUDING ALL);"
|
||||
);
|
||||
|
|
@ -1437,6 +1464,7 @@ mod tests {
|
|||
schema: Some("public".to_string()),
|
||||
source_name: "users".to_string(),
|
||||
target_name: "users_copy".to_string(),
|
||||
column_comments: vec![],
|
||||
}),
|
||||
"CREATE TABLE \"public\".\"users_copy\" (LIKE \"public\".\"users\" INCLUDING ALL);"
|
||||
);
|
||||
|
|
@ -1446,6 +1474,7 @@ mod tests {
|
|||
schema: Some("dbo".to_string()),
|
||||
source_name: "users".to_string(),
|
||||
target_name: "users_copy".to_string(),
|
||||
column_comments: vec![],
|
||||
}),
|
||||
"SELECT TOP 0 * INTO [dbo].[users_copy] FROM [dbo].[users];"
|
||||
);
|
||||
|
|
@ -1455,15 +1484,44 @@ mod tests {
|
|||
schema: Some("HR".to_string()),
|
||||
source_name: "USERS".to_string(),
|
||||
target_name: "USERS_COPY".to_string(),
|
||||
column_comments: vec![],
|
||||
}),
|
||||
"CREATE TABLE \"HR\".\"USERS_COPY\" AS SELECT * FROM \"HR\".\"USERS\" WHERE 1=0"
|
||||
);
|
||||
let dameng_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(),
|
||||
column_comments: vec![
|
||||
DuplicateTableColumnComment {
|
||||
name: "DISPLAY\"NAME".to_string(),
|
||||
comment: " Owner's; display name".to_string(),
|
||||
},
|
||||
DuplicateTableColumnComment { name: "STATUS".to_string(), comment: "active ".to_string() },
|
||||
DuplicateTableColumnComment { name: "EMPTY".to_string(), comment: " \t\n".to_string() },
|
||||
],
|
||||
});
|
||||
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 ';"
|
||||
);
|
||||
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(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Iris),
|
||||
schema: Some("SQLUSER".to_string()),
|
||||
source_name: "tb_a".to_string(),
|
||||
target_name: "tb_a_copy".to_string(),
|
||||
column_comments: vec![],
|
||||
}),
|
||||
"CREATE TABLE \"SQLUSER\".\"tb_a_copy\" AS SELECT * FROM \"SQLUSER\".\"tb_a\" WHERE 1=0"
|
||||
);
|
||||
|
|
@ -1473,6 +1531,7 @@ mod tests {
|
|||
schema: None,
|
||||
source_name: "users".to_string(),
|
||||
target_name: "users_copy".to_string(),
|
||||
column_comments: vec![],
|
||||
}),
|
||||
"CREATE TABLE `users_copy` (LIKE `users`);"
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue