mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-22 19:58:51 +00:00
fix(sidebar): keep multi-object DDL in one connection context
This commit is contained in:
@@ -143,6 +143,8 @@ import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import { batchTableEmptyFeedback, runBatchTableEmpty } from "@/lib/sidebar/batchTableEmpty";
|
||||
import { runBatchTableTruncate } from "@/lib/table/batchTableTruncate";
|
||||
import { runBatchTableDrop } from "@/lib/table/batchTableDrop";
|
||||
import { buildSidebarDdlTemplateSql, sidebarDdlTargetsForExecutionContext } from "@/lib/sidebar/sidebarDdlTemplate";
|
||||
import { sidebarStructureExportTargets } from "@/lib/sidebar/sidebarExportRuntime";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { rankSavedSqlHistory, type SavedSqlHistoryScope } from "@/lib/savedSql/savedSqlHistory";
|
||||
@@ -1373,34 +1375,50 @@ async function newDeleteTemplate() {
|
||||
}
|
||||
|
||||
async function generateDdlTemplate() {
|
||||
const node = activeNode.value;
|
||||
if (!node.connectionId || !hasTreeNodeDatabaseContext(node)) return;
|
||||
if (node.type !== "table" && node.type !== "view" && node.type !== "materialized_view") return;
|
||||
const targets = selectedDdlTargets();
|
||||
if (!targets.length) return;
|
||||
const tabTarget = targets.find((target) => target.id === activeNode.value.id) ?? targets[0]!;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
const schema = node.schema || node.database;
|
||||
let ddl: string;
|
||||
if (node.type === "table") {
|
||||
ddl = await api.getTableDisplayDdl(node.connectionId, node.database, schema, node.label, undefined, node.catalog);
|
||||
} else if (node.type === "materialized_view") {
|
||||
ddl = await api.getTableDisplayDdl(node.connectionId, node.database, schema, node.label, "MATERIALIZED_VIEW", node.catalog);
|
||||
} else {
|
||||
const result = await api.getObjectSource(node.connectionId, node.database, schema, node.label, "VIEW");
|
||||
ddl = await buildViewDdl({
|
||||
databaseType: currentDatabaseType(),
|
||||
schema,
|
||||
name: node.label,
|
||||
source: result.source,
|
||||
});
|
||||
}
|
||||
const formatted = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(currentDatabaseType()), settingsStore.editorSettings.sqlFormatter);
|
||||
openSqlTemplateTab(node.connectionId, node.database, node.schema, node.catalog, formatted, `DDL - ${node.label}`);
|
||||
const sql = await buildSidebarDdlTemplateSql(
|
||||
targets,
|
||||
async (target) => {
|
||||
await connectionStore.ensureConnected(target.connectionId);
|
||||
const schema = target.schema || target.database;
|
||||
if (target.type === "table") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, undefined, target.catalog);
|
||||
if (target.type === "materialized_view") return api.getTableDisplayDdl(target.connectionId, target.database, schema, target.label, "MATERIALIZED_VIEW", target.catalog);
|
||||
const result = await api.getObjectSource(target.connectionId, target.database, schema, target.label, "VIEW");
|
||||
return buildViewDdl({
|
||||
databaseType: databaseTypeForNode(target),
|
||||
schema,
|
||||
name: target.label,
|
||||
source: result.source,
|
||||
});
|
||||
},
|
||||
(ddl, target) => formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseTypeForNode(target)), settingsStore.editorSettings.sqlFormatter),
|
||||
);
|
||||
connectionStore.activeConnectionId = tabTarget.connectionId;
|
||||
const title = `DDL - ${targets.map((target) => target.label).join(", ")}`;
|
||||
openSqlTemplateTab(tabTarget.connectionId, tabTarget.database, tabTarget.schema, tabTarget.catalog, sql, title);
|
||||
} catch (e: any) {
|
||||
toast(e?.message || String(e), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function selectedDdlTargets() {
|
||||
const targets = sidebarStructureExportTargets(activeNode.value, connectionStore.treeNodes, acceptedSelectionIds ?? connectionStore.selectedTreeNodeIds);
|
||||
return sidebarDdlTargetsForExecutionContext(activeNode.value as TreeNode & { connectionId: string; database: string }, targets);
|
||||
}
|
||||
|
||||
async function openDdl() {
|
||||
const targets = selectedDdlTargets();
|
||||
if (!targets.length) return;
|
||||
if (targets.length > 1) {
|
||||
await exportStructure();
|
||||
return;
|
||||
}
|
||||
emit("open-ddl", targets[0]!);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const node = activeNode.value;
|
||||
try {
|
||||
@@ -4328,7 +4346,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
if (node.type === "table") {
|
||||
items.push({
|
||||
label: t("contextMenu.viewDdl"),
|
||||
action: () => emit("open-ddl", node),
|
||||
action: openDdl,
|
||||
icon: FileCode,
|
||||
});
|
||||
}
|
||||
@@ -4337,7 +4355,7 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
||||
items.push({
|
||||
label: t("contextMenu.viewDdl"),
|
||||
action: () => emit("open-ddl", node),
|
||||
action: openDdl,
|
||||
icon: FileCode,
|
||||
});
|
||||
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
||||
|
||||
@@ -7,6 +7,7 @@ const apiMock = vi.hoisted(() => ({
|
||||
buildTableSelectSql: vi.fn(async () => 'SELECT * FROM "main"."users" LIMIT 10000'),
|
||||
executeQuery: vi.fn(),
|
||||
exportQueryResultJson: vi.fn(),
|
||||
getTableDdl: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => apiMock);
|
||||
@@ -24,10 +25,14 @@ vi.mock("vue-i18n", () => ({
|
||||
}));
|
||||
|
||||
import { useSidebarTreeExportRuntime } from "@/composables/useSidebarTreeExportRuntime";
|
||||
import { showStructurePreviewDialog, structurePreviewSql, structurePreviewTitle } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
|
||||
describe("useSidebarTreeExportRuntime", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
showStructurePreviewDialog.value = false;
|
||||
structurePreviewSql.value = "";
|
||||
structurePreviewTitle.value = "";
|
||||
});
|
||||
|
||||
it("translates direct executeQuery errors for sidebar JSON export", async () => {
|
||||
@@ -52,4 +57,33 @@ describe("useSidebarTreeExportRuntime", () => {
|
||||
expect(apiMock.executeQuery).toHaveBeenCalledOnce();
|
||||
expect(toastMock).toHaveBeenCalledWith("导出失败:上一个 DuckDB 查询仍在停止中,请稍后重试。", 5000);
|
||||
});
|
||||
|
||||
it("loads and joins every selected DDL in tree order", async () => {
|
||||
apiMock.getTableDdl.mockResolvedValueOnce("CREATE TABLE one (id INT)").mockResolvedValueOnce("CREATE VIEW two AS SELECT 1;");
|
||||
const first = { id: "table-1", type: "table", label: "one", connectionId: "conn-1", database: "db", schema: "main" } as TreeNode;
|
||||
const second = { id: "view-1", type: "view", label: "two", connectionId: "conn-1", database: "db", schema: "main" } as TreeNode;
|
||||
const group = { id: "tables", type: "group-tables", label: "Tables", children: [first, second] } as TreeNode;
|
||||
const activeNode = shallowRef(first);
|
||||
const connectionStore = {
|
||||
ensureConnected: vi.fn(),
|
||||
treeNodes: [group],
|
||||
selectedTreeNodeIds: [second.id, first.id],
|
||||
};
|
||||
const runtime = useSidebarTreeExportRuntime({
|
||||
activeNode,
|
||||
connectionStore: connectionStore as never,
|
||||
settingsStore: {} as never,
|
||||
acceptedSelectionIds: () => null,
|
||||
});
|
||||
|
||||
await runtime.exportStructure();
|
||||
|
||||
expect(connectionStore.ensureConnected).toHaveBeenNthCalledWith(1, first.connectionId);
|
||||
expect(connectionStore.ensureConnected).toHaveBeenNthCalledWith(2, second.connectionId);
|
||||
expect(apiMock.getTableDdl).toHaveBeenNthCalledWith(1, first.connectionId, first.database, first.schema, first.label, undefined, undefined);
|
||||
expect(apiMock.getTableDdl).toHaveBeenNthCalledWith(2, second.connectionId, second.database, second.schema, second.label, "VIEW", undefined);
|
||||
expect(structurePreviewSql.value).toBe("CREATE TABLE one (id INT);\n\nCREATE VIEW two AS SELECT 1;\n");
|
||||
expect(structurePreviewTitle.value).toBe("contextMenu.exportStructurePreviewTitleMultiple");
|
||||
expect(showStructurePreviewDialog.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildSidebarDdlTemplateSql, sidebarDdlTargetsForExecutionContext } from "@/lib/sidebar/sidebarDdlTemplate";
|
||||
|
||||
describe("sidebar DDL template", () => {
|
||||
it("preserves the single-target SQL", async () => {
|
||||
const sql = await buildSidebarDdlTemplateSql(
|
||||
["one"],
|
||||
async () => "CREATE TABLE one (id INT)",
|
||||
async (ddl) => ddl,
|
||||
);
|
||||
|
||||
expect(sql).toBe("CREATE TABLE one (id INT)");
|
||||
});
|
||||
|
||||
it("loads, formats, and joins multiple targets in order", async () => {
|
||||
const loadDdl = vi.fn(async (target: string) => `CREATE TABLE ${target} (id INT)`);
|
||||
const formatDdl = vi.fn(async (ddl: string, target: string) => `${ddl} /* ${target} */`);
|
||||
|
||||
const sql = await buildSidebarDdlTemplateSql(["one", "two"], loadDdl, formatDdl);
|
||||
|
||||
expect(loadDdl.mock.calls).toEqual([["one"], ["two"]]);
|
||||
expect(formatDdl.mock.calls).toEqual([
|
||||
["CREATE TABLE one (id INT)", "one"],
|
||||
["CREATE TABLE two (id INT)", "two"],
|
||||
]);
|
||||
expect(sql).toBe("CREATE TABLE one (id INT) /* one */;\n\nCREATE TABLE two (id INT) /* two */;\n");
|
||||
});
|
||||
|
||||
it("keeps DDL targets in the active SQL execution context", () => {
|
||||
const active = { id: "active", connectionId: "c1", database: "db1", catalog: "catalog1" };
|
||||
const sameContext = { id: "same", connectionId: "c1", database: "db1", catalog: "catalog1" };
|
||||
const otherDatabase = { id: "database", connectionId: "c1", database: "db2", catalog: "catalog1" };
|
||||
const otherConnection = { id: "connection", connectionId: "c2", database: "db1", catalog: "catalog1" };
|
||||
const otherCatalog = { id: "catalog", connectionId: "c1", database: "db1", catalog: "catalog2" };
|
||||
|
||||
expect(sidebarDdlTargetsForExecutionContext(active, [active, otherDatabase, sameContext, otherConnection, otherCatalog])).toEqual([active, sameContext]);
|
||||
});
|
||||
});
|
||||
@@ -21,9 +21,10 @@ describe("sidebar export runtime", () => {
|
||||
it("freezes the accepted structure selection before export work starts", () => {
|
||||
const first: TreeNode = { id: "t1", label: "one", type: "table", connectionId: "c1", database: "db" };
|
||||
const second: TreeNode = { id: "t2", label: "two", type: "view", connectionId: "c1", database: "db" };
|
||||
const group: TreeNode = { id: "group", label: "Tables", type: "group-tables", children: [first, second] };
|
||||
const third: TreeNode = { id: "t3", label: "three", type: "materialized_view", connectionId: "c1", database: "db" };
|
||||
const group: TreeNode = { id: "group", label: "Tables", type: "group-tables", children: [first, second, third] };
|
||||
|
||||
expect(sidebarStructureExportTargets(first, [group], [first.id, second.id])).toEqual([first, second]);
|
||||
expect(sidebarStructureExportTargets(first, [group], [third.id, first.id, second.id])).toEqual([first, second, third]);
|
||||
expect(sidebarStructureExportTargets(first, [group], [second.id])).toEqual([first]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { joinExportedDdls } from "@/lib/export/ddlExport";
|
||||
|
||||
interface SidebarDdlExecutionTarget {
|
||||
connectionId: string;
|
||||
database: string;
|
||||
catalog?: string;
|
||||
}
|
||||
|
||||
export function sidebarDdlTargetsForExecutionContext<T extends SidebarDdlExecutionTarget>(activeTarget: SidebarDdlExecutionTarget, targets: readonly T[]): T[] {
|
||||
return targets.filter((target) => target.connectionId === activeTarget.connectionId && target.database === activeTarget.database && (target.catalog ?? "") === (activeTarget.catalog ?? ""));
|
||||
}
|
||||
|
||||
export async function buildSidebarDdlTemplateSql<T>(targets: readonly T[], loadDdl: (target: T) => Promise<string>, formatDdl: (ddl: string, target: T) => Promise<string>): Promise<string> {
|
||||
const parts: string[] = [];
|
||||
for (const target of targets) {
|
||||
parts.push(await formatDdl(await loadDdl(target), target));
|
||||
}
|
||||
return parts.length === 1 ? parts[0]! : joinExportedDdls(parts);
|
||||
}
|
||||
Reference in New Issue
Block a user