fix(saphana): restore schema-scoped table actions

Closes #6457
This commit is contained in:
t8y2 committed 2026-08-17 07:47:51 +00:00
1 parent 088cca545c
commit e872586b70
2 files changed
+164 -3

No files matched your search

@@ -0,0 +1,161 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { shallowRef } from "vue";
import type { TreeNode } from "@/types/database";
import { dropTableCascade, dropTablePreviewSql, emptyTablePreviewSql, sidebarDangerTarget, truncateTableCascade, truncateTablePreviewSql } from "@/components/sidebar/sidebarTreeDialogState";
const mocks = vi.hoisted(() => ({
toast: vi.fn(),
buildDropTableSql: vi.fn(),
buildEmptyTableSql: vi.fn(),
buildTruncateTableSql: vi.fn(),
ensureConnected: vi.fn(),
executeWithProductionGuard: vi.fn(),
closeDroppedTableObjectTabsForNode: vi.fn(),
refreshMutatedTableDataTabsForNode: vi.fn(),
removeTreeNode: vi.fn(),
releaseActiveNodeReference: vi.fn(),
}));
vi.mock("vue-i18n", () => ({ useI18n: () => ({ t: (key: string) => key }) }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
vi.mock("@/lib/database/dbAdminSql", async (importOriginal) => ({
...(await importOriginal<typeof import("@/lib/database/dbAdminSql")>()),
buildDropTableSql: mocks.buildDropTableSql,
buildEmptyTableSql: mocks.buildEmptyTableSql,
buildTruncateTableSql: mocks.buildTruncateTableSql,
}));
import { useSidebarTableMutationRuntime } from "@/composables/useSidebarTableMutationRuntime";
type ConfirmAction = "confirmDropTable" | "confirmEmptyTable" | "confirmTruncateTable";
const actions: Array<{ action: ConfirmAction; builder: typeof mocks.buildDropTableSql; sql: string }> = [
{ action: "confirmDropTable", builder: mocks.buildDropTableSql, sql: 'DROP TABLE "APP"."ORDERS";' },
{ action: "confirmEmptyTable", builder: mocks.buildEmptyTableSql, sql: 'DELETE FROM "APP"."ORDERS";' },
{ action: "confirmTruncateTable", builder: mocks.buildTruncateTableSql, sql: 'TRUNCATE TABLE "APP"."ORDERS";' },
];
function tableNode(database: string | null | undefined): TreeNode {
return {
id: "hana-1::APP:ORDERS",
label: "ORDERS",
type: "table",
connectionId: "hana-1",
database,
schema: "APP",
isExpanded: false,
} as TreeNode;
}
function runtime(database: string | null | undefined) {
const node = tableNode(database);
const activeNode = shallowRef(node);
const connectionStore = {
ensureConnected: mocks.ensureConnected,
removeTreeNode: mocks.removeTreeNode,
} as any;
const feature = useSidebarTableMutationRuntime({
activeNode,
releaseActiveNodeReference: mocks.releaseActiveNodeReference,
connectionStore,
currentDatabaseType: () => "saphana",
databaseTypeForNode: () => "saphana",
executeWithProductionGuard: mocks.executeWithProductionGuard,
closeDroppedTableObjectTabsForNode: mocks.closeDroppedTableObjectTabsForNode,
refreshMutatedTableDataTabsForNode: mocks.refreshMutatedTableDataTabsForNode,
});
return { feature, node };
}
describe("useSidebarTableMutationRuntime SAP HANA schema-scoped actions", () => {
beforeEach(() => {
vi.clearAllMocks();
sidebarDangerTarget.value = null;
dropTablePreviewSql.value = "";
emptyTablePreviewSql.value = "";
truncateTablePreviewSql.value = "";
dropTableCascade.value = false;
truncateTableCascade.value = false;
mocks.ensureConnected.mockResolvedValue(undefined);
mocks.executeWithProductionGuard.mockResolvedValue(undefined);
mocks.refreshMutatedTableDataTabsForNode.mockResolvedValue(undefined);
mocks.buildDropTableSql.mockResolvedValue('DROP TABLE "APP"."ORDERS";');
mocks.buildEmptyTableSql.mockResolvedValue('DELETE FROM "APP"."ORDERS";');
mocks.buildTruncateTableSql.mockResolvedValue('TRUNCATE TABLE "APP"."ORDERS";');
});
it.each(actions)("executes $action with an empty database context", async ({ action, builder, sql }) => {
const { feature, node } = runtime("");
await feature[action]();
expect(mocks.ensureConnected).toHaveBeenCalledOnce();
expect(mocks.ensureConnected).toHaveBeenCalledWith("hana-1");
expect(builder).toHaveBeenCalledOnce();
expect(builder).toHaveBeenCalledWith({ databaseType: "saphana", schema: "APP", tableName: "ORDERS" });
expect(mocks.executeWithProductionGuard).toHaveBeenCalledOnce();
expect(mocks.executeWithProductionGuard).toHaveBeenCalledWith(node, sql, { database: "", schema: "APP" });
});
it("removes the dropped table and releases its active reference", async () => {
const { feature, node } = runtime("");
await feature.confirmDropTable();
expect(mocks.closeDroppedTableObjectTabsForNode).toHaveBeenCalledWith(node);
expect(mocks.removeTreeNode).toHaveBeenCalledWith(node.id);
expect(mocks.releaseActiveNodeReference).toHaveBeenCalledWith([node.id]);
});
it.each(["confirmEmptyTable", "confirmTruncateTable"] as const)("refreshes data tabs after %s", async (action) => {
const { feature, node } = runtime("");
await feature[action]();
expect(mocks.refreshMutatedTableDataTabsForNode).toHaveBeenCalledWith(node);
});
it.each(actions)("preserves $action for a non-empty database context", async ({ action, sql }) => {
const { feature, node } = runtime("TENANT");
await feature[action]();
expect(mocks.executeWithProductionGuard).toHaveBeenCalledWith(node, sql, { database: "TENANT", schema: "APP" });
});
it.each([null, undefined])("rejects a %s database context", async (database) => {
for (const { action } of actions) {
const { feature } = runtime(database);
await feature[action]();
}
expect(mocks.ensureConnected).not.toHaveBeenCalled();
expect(mocks.buildDropTableSql).not.toHaveBeenCalled();
expect(mocks.buildEmptyTableSql).not.toHaveBeenCalled();
expect(mocks.buildTruncateTableSql).not.toHaveBeenCalled();
expect(mocks.executeWithProductionGuard).not.toHaveBeenCalled();
});
it("reports SQL generation failures without dropping local state", async () => {
const { feature } = runtime("");
mocks.buildDropTableSql.mockRejectedValueOnce(new Error("generation failed"));
await feature.confirmDropTable();
expect(mocks.executeWithProductionGuard).not.toHaveBeenCalled();
expect(mocks.closeDroppedTableObjectTabsForNode).not.toHaveBeenCalled();
expect(mocks.removeTreeNode).not.toHaveBeenCalled();
expect(mocks.releaseActiveNodeReference).not.toHaveBeenCalled();
expect(mocks.toast).toHaveBeenCalledWith("contextMenu.tableOperationFailed", 5000);
});
it("reports execution failures without refreshing data tabs", async () => {
const { feature } = runtime("");
mocks.executeWithProductionGuard.mockRejectedValueOnce(new Error("execution failed"));
await feature.confirmTruncateTable();
expect(mocks.refreshMutatedTableDataTabsForNode).not.toHaveBeenCalled();
expect(mocks.toast).toHaveBeenCalledWith("contextMenu.tableOperationFailed", 5000);
});
});
@@ -153,7 +153,7 @@ export function useSidebarTableMutationRuntime(options: SidebarTableMutationRunt
async function confirmDropTable() {
const node = sidebarDangerTarget.value ?? activeNode.value;
if (!node.connectionId || !node.database) return;
if (!node.connectionId || node.database == null) return;
try {
await connectionStore.ensureConnected(node.connectionId);
const sql = dropTablePreviewSql.value || (await buildDropTableSql(tableAdminSqlOptionsForNode(node, { cascade: dropTableCascade.value && supportsDropTableCascade(databaseTypeForNode(node)) })));
@@ -174,7 +174,7 @@ export function useSidebarTableMutationRuntime(options: SidebarTableMutationRunt
async function confirmEmptyTable() {
const node = sidebarDangerTarget.value ?? activeNode.value;
if (!node.connectionId || !node.database) return;
if (!node.connectionId || node.database == null) return;
try {
await connectionStore.ensureConnected(node.connectionId);
const sql = emptyTablePreviewSql.value || (await buildEmptyTableSql(tableAdminSqlOptionsForNode(node)));
@@ -195,7 +195,7 @@ export function useSidebarTableMutationRuntime(options: SidebarTableMutationRunt
async function confirmTruncateTable() {
const node = sidebarDangerTarget.value ?? activeNode.value;
if (!node.connectionId || !node.database) return;
if (!node.connectionId || node.database == null) return;
try {
await connectionStore.ensureConnected(node.connectionId);
const sql = truncateTablePreviewSql.value || (await buildTruncateTableSql(tableAdminSqlOptionsForNode(node, { cascade: truncateTableCascade.value && supportsTruncateTableCascade(databaseTypeForNode(node)) })));