fix(postgres): preserve copied table comments safely
This commit is contained in:
10 files changed
+166
-19
No files matched your search
@@ -71,6 +71,7 @@ import {
|
||||
buildEmptyTableSql,
|
||||
buildTruncateTableSql,
|
||||
collectDuplicateTableColumnComments,
|
||||
duplicateTableStructureRequiresScript,
|
||||
supportsDropTableCascade,
|
||||
supportsTruncateTableCascade,
|
||||
type TableAdminSqlOptions,
|
||||
@@ -241,7 +242,7 @@ const batchEmptyPlan = ref<BatchTableEmptyPlanItem<ObjectBrowserRow>[]>([]);
|
||||
// Paste table dialog state
|
||||
const showPasteDialog = ref(false);
|
||||
const pasteTableMode = ref<PasteTableMode>("structure-and-data");
|
||||
const pasteTableEntries = ref<{ sourceName: string; targetName: string; schema?: string }[]>([]);
|
||||
const pasteTableEntries = ref<{ sourceName: string; targetName: string; schema?: string; tableComment?: string | null }[]>([]);
|
||||
const pasteTableDataCopySupported = computed(() => supportsWholeRowTableDataCopy(effectiveDatabaseType.value));
|
||||
const objectColumnWidths = ref<Record<ObjectBrowserColumnKey, number>>({
|
||||
select: 34,
|
||||
@@ -1910,7 +1911,7 @@ function requestDuplicateStructure(row: ObjectBrowserRow) {
|
||||
showDuplicateDialog.value = true;
|
||||
}
|
||||
|
||||
async function buildDuplicateStructurePlan(sourceName: string, targetName: string, schema: string | undefined, sourceColumns?: ColumnInfo[]) {
|
||||
async function buildDuplicateStructurePlan(sourceName: string, targetName: string, schema: string | undefined, tableComment?: string | null, sourceColumns?: ColumnInfo[]) {
|
||||
let columns = sourceColumns;
|
||||
if (effectiveDatabaseType.value === "dameng" && !columns) {
|
||||
try {
|
||||
@@ -1925,9 +1926,10 @@ async function buildDuplicateStructurePlan(sourceName: string, targetName: strin
|
||||
schema,
|
||||
sourceName,
|
||||
targetName,
|
||||
tableComment,
|
||||
columnComments,
|
||||
});
|
||||
return { sql, sourceColumns: columns, executeAsScript: columnComments.length > 0 };
|
||||
return { sql, sourceColumns: columns, executeAsScript: duplicateTableStructureRequiresScript(sql) };
|
||||
}
|
||||
|
||||
function executeDuplicateStructurePlan(plan: { sql: string; executeAsScript: boolean }, schema: string | undefined) {
|
||||
@@ -1941,7 +1943,7 @@ async function confirmDuplicateStructure() {
|
||||
showDuplicateDialog.value = false;
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
const plan = await buildDuplicateStructurePlan(row.name, newName, schema);
|
||||
const plan = await buildDuplicateStructurePlan(row.name, newName, schema, row.comment);
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(plan.sql, () => executeDuplicateStructurePlan(plan, schema));
|
||||
if (!executed) return;
|
||||
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }));
|
||||
@@ -1962,6 +1964,7 @@ function copySelectedTablesToClipboard() {
|
||||
database: props.database,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(row.schema || selectedSchema.value),
|
||||
tableName: row.name,
|
||||
tableComment: row.comment,
|
||||
})),
|
||||
};
|
||||
toast(t("contextMenu.pasteTableClipboardUpdated"), 2000);
|
||||
@@ -2031,6 +2034,7 @@ function copySingleTableToClipboard(row: ObjectBrowserRow) {
|
||||
database: props.database,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(row.schema || selectedSchema.value),
|
||||
tableName: row.name,
|
||||
tableComment: row.comment,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -2052,6 +2056,7 @@ function openPasteTableDialog() {
|
||||
sourceName: entry.tableName,
|
||||
targetName: `${entry.tableName}_copy`,
|
||||
schema: normalizeObjectBrowserTableClipboardSchema(entry.schema, entry.database, entry.connectionId),
|
||||
tableComment: entry.tableComment,
|
||||
}));
|
||||
showPasteDialog.value = true;
|
||||
}
|
||||
@@ -2090,7 +2095,7 @@ async function confirmPasteTable() {
|
||||
try {
|
||||
let sourceColumns: ColumnInfo[] | undefined;
|
||||
if (mode === "structure-and-data" || mode === "structure-only") {
|
||||
const plan = await buildDuplicateStructurePlan(entry.sourceName, targetName, schema, sourceColumns);
|
||||
const plan = await buildDuplicateStructurePlan(entry.sourceName, targetName, schema, entry.tableComment, sourceColumns);
|
||||
sourceColumns = plan.sourceColumns;
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(plan.sql, () => executeDuplicateStructurePlan(plan, schema));
|
||||
if (!executed) {
|
||||
|
||||
@@ -30,4 +30,10 @@ describe("ObjectBrowser table clipboard context menu", () => {
|
||||
expect(objectBrowserSource).toMatch(/if \(!executed\) \{[\s\S]*?pasteCancelled = true;[\s\S]*?break;/);
|
||||
expect(objectBrowserSource).toMatch(/if \(pasteCancelled\) \{[\s\S]*?if \(hasMutatedTable\)[\s\S]*?await reload\(\)[\s\S]*?refreshObjectListTreeNode[\s\S]*?pasteTableCancelledAfterPartial[\s\S]*?return;/);
|
||||
});
|
||||
|
||||
it("carries table comments through local copy and paste", () => {
|
||||
expect(objectBrowserSource).toMatch(/tableName: row\.name,\s*tableComment: row\.comment/);
|
||||
expect(objectBrowserSource).toMatch(/targetName: `\$\{entry\.tableName\}_copy`,[\s\S]*?tableComment: entry\.tableComment/);
|
||||
expect(objectBrowserSource).toMatch(/buildDuplicateStructurePlan\(entry\.sourceName, targetName, schema, entry\.tableComment/);
|
||||
});
|
||||
});
|
||||
@@ -1597,6 +1597,7 @@ function copySelectedSidebarNames(): boolean {
|
||||
database: node.database!,
|
||||
schema: connectionObjectTreeNodeSchema(store.getConfig(node.connectionId!), node.database!, node.schema),
|
||||
tableName: node.label,
|
||||
tableComment: node.comment,
|
||||
})),
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -113,6 +113,7 @@ import {
|
||||
buildCopyTableDataSql,
|
||||
buildEmptyTableSql,
|
||||
buildTruncateTableSql,
|
||||
duplicateTableStructureRequiresScript,
|
||||
supportsDropTableCascade,
|
||||
supportsTruncateTableCascade,
|
||||
supportsSchemaComment,
|
||||
@@ -921,6 +922,7 @@ function requestPasteTreeClipboard(): boolean {
|
||||
connectionId: entry.connectionId,
|
||||
database: entry.database,
|
||||
schema: normalizeTreeClipboardSchema(entry.connectionId, entry.database, entry.schema),
|
||||
tableComment: entry.tableComment,
|
||||
}));
|
||||
showPasteDialog.value = true;
|
||||
return true;
|
||||
@@ -1421,6 +1423,7 @@ function updateTreeClipboardForNodes(nodes: TreeNode[]) {
|
||||
database: node.database,
|
||||
schema: normalizeTreeClipboardSchema(node.connectionId, node.database, node.schema),
|
||||
tableName: node.label,
|
||||
tableComment: node.comment,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -1923,7 +1926,7 @@ function openRenameObjectDialog() {
|
||||
showRenameObjectDialog.value = true;
|
||||
}
|
||||
|
||||
async function executeTreeNodeSqlWithProductionGuard(node: Pick<TreeNode, "connectionId" | "database" | "schema">, sql: string, options: { database?: string; schema?: string } = {}) {
|
||||
async function executeTreeNodeSqlWithProductionGuard(node: Pick<TreeNode, "connectionId" | "database" | "schema">, sql: string, options: { database?: string; schema?: string; executeAsScript?: boolean } = {}) {
|
||||
if (!node.connectionId) return undefined;
|
||||
const database = options.database ?? node.database ?? "";
|
||||
return executeWithProductionSqlGuard({
|
||||
@@ -1931,7 +1934,7 @@ async function executeTreeNodeSqlWithProductionGuard(node: Pick<TreeNode, "conne
|
||||
database,
|
||||
sql,
|
||||
source: t("production.sourceSidebar"),
|
||||
execute: () => api.executeQuery(node.connectionId!, database, sql, options.schema ?? node.schema),
|
||||
execute: () => (options.executeAsScript ? api.executeScript(node.connectionId!, database, sql, options.schema ?? node.schema) : api.executeQuery(node.connectionId!, database, sql, options.schema ?? node.schema)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2989,8 +2992,13 @@ async function confirmDuplicateStructure() {
|
||||
schema: node.schema,
|
||||
sourceName: node.label,
|
||||
targetName: newName,
|
||||
tableComment: node.comment,
|
||||
});
|
||||
await executeTreeNodeSqlWithProductionGuard(node, sql, {
|
||||
database: node.database,
|
||||
schema: node.schema,
|
||||
executeAsScript: duplicateTableStructureRequiresScript(sql),
|
||||
});
|
||||
await executeTreeNodeSqlWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
||||
toast(t("contextMenu.duplicateStructureSuccess", { name: newName }), 3000);
|
||||
await refreshTableList(node);
|
||||
} catch (e: any) {
|
||||
@@ -3031,8 +3039,13 @@ async function confirmPasteTable() {
|
||||
schema: entry.schema,
|
||||
sourceName: entry.sourceName,
|
||||
targetName,
|
||||
tableComment: entry.tableComment,
|
||||
});
|
||||
const structureExecuted = await executeTreeNodeSqlWithProductionGuard(entry, structureSql, {
|
||||
database: entry.database,
|
||||
schema: entry.schema,
|
||||
executeAsScript: duplicateTableStructureRequiresScript(structureSql),
|
||||
});
|
||||
const structureExecuted = await executeTreeNodeSqlWithProductionGuard(entry, structureSql, { database: entry.database, schema: entry.schema });
|
||||
if (!structureExecuted) {
|
||||
pasteCancelled = true;
|
||||
break;
|
||||
@@ -3121,6 +3134,7 @@ function openPasteTableDialog() {
|
||||
connectionId: entry.connectionId,
|
||||
database: entry.database,
|
||||
schema: normalizeTreeClipboardSchema(entry.connectionId, entry.database, entry.schema),
|
||||
tableComment: entry.tableComment,
|
||||
}));
|
||||
showPasteDialog.value = true;
|
||||
}
|
||||
|
||||
@@ -17,4 +17,11 @@ describe("cross-database table paste", () => {
|
||||
expect(runtimeSource).toContain("if (canTransferTreeClipboardToCurrentNode()) return openTransferFromTreeClipboard();");
|
||||
expect(runtimeSource).toContain("pasteTableMode.value = defaultPasteTableMode(currentDatabaseType());");
|
||||
});
|
||||
|
||||
it("carries table comments through the local sidebar paste path", () => {
|
||||
expect(runtimeSource).toMatch(/tableName: node\.label,\s*tableComment: node\.comment/);
|
||||
expect(runtimeSource).toMatch(/targetName: `\$\{entry\.tableName\}_copy`,[\s\S]*?tableComment: entry\.tableComment/);
|
||||
expect(runtimeSource).toMatch(/targetName,\s*tableComment: entry\.tableComment/);
|
||||
expect(runtimeSource).toContain("executeAsScript: duplicateTableStructureRequiresScript(structureSql)");
|
||||
});
|
||||
});
|
||||
@@ -58,7 +58,7 @@ export const duplicateTableName = ref("");
|
||||
export const duplicateStructureSource = ref<DuplicateStructureSource | null>(null);
|
||||
export const showPasteDialog = ref(false);
|
||||
export const pasteTableMode = ref<PasteTableMode>("structure-and-data");
|
||||
export const pasteTableEntries = ref<Array<{ sourceName: string; targetName: string; connectionId: string; database: string; schema?: string }>>([]);
|
||||
export const pasteTableEntries = ref<Array<{ sourceName: string; targetName: string; connectionId: string; database: string; schema?: string; tableComment?: string | null }>>([]);
|
||||
export const showCreateDatabaseDialog = ref(false);
|
||||
export const createDatabaseName = ref("");
|
||||
export const createDatabaseCharset = ref("utf8mb4");
|
||||
|
||||
Reference in new issue
Block a user