1 parent
278429ab07
commit
4c6c3142c0
18 files changed
+456
-7
No files matched your search
@@ -51,6 +51,7 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { cancelPendingSidebarDataOpen, runSidebarDataOpenImmediately, type SidebarDataOpenRequest } from "@/lib/sidebar/sidebarDataOpenCoordinator";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { codeMirrorSqlDialect } from "@/lib/database/jdbcDialect";
|
||||
import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
|
||||
@@ -1460,6 +1461,13 @@ function updateSidebarDangerDialogOption(event: Event) {
|
||||
void option.onChange?.(option.checked);
|
||||
}
|
||||
|
||||
function updateSidebarDangerDialogTextInput(value: string | number) {
|
||||
const input = sidebarDangerDialogRequest.value?.textInput;
|
||||
if (!input) return;
|
||||
input.value = String(value);
|
||||
void input.onInput?.(input.value);
|
||||
}
|
||||
|
||||
function updateSidebarTreeItemDialogController(controller: Record<string, any> | null) {
|
||||
sidebarTreeItemDialogController.value = controller;
|
||||
}
|
||||
@@ -2287,6 +2295,10 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
<span class="text-xs leading-5 text-muted-foreground">{{ sidebarDangerDialogRequest.option.hint }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label v-if="sidebarDangerDialogRequest.textInput" class="mb-3 grid gap-1.5 rounded-md border bg-muted/20 px-3 py-2 text-sm">
|
||||
<span class="font-medium text-foreground">{{ sidebarDangerDialogRequest.textInput.label }}</span>
|
||||
<Input :model-value="sidebarDangerDialogRequest.textInput.value" :inputmode="sidebarDangerDialogRequest.textInput.inputMode" :placeholder="sidebarDangerDialogRequest.textInput.placeholder" @update:model-value="updateSidebarDangerDialogTextInput" />
|
||||
</label>
|
||||
</template>
|
||||
</SidebarDangerConfirmDialog>
|
||||
<SidebarTreeItemDialogs v-if="sidebarTreeItemDialogController" :key="sidebarTreeItemDialogController.node?.id" :controller="sidebarTreeItemDialogController" @closed="sidebarTreeItemDialogController = null" />
|
||||
|
||||
@@ -190,6 +190,7 @@ import {
|
||||
isLoadingStructurePreview,
|
||||
showEmptyTableConfirm,
|
||||
showTruncateTableConfirm,
|
||||
showMysqlAutoIncrementConfirm,
|
||||
showRenameObjectDialog,
|
||||
renameObjectName,
|
||||
renameObjectError,
|
||||
@@ -200,6 +201,8 @@ import {
|
||||
emptyTablePreviewSql,
|
||||
truncateTablePreviewSql,
|
||||
truncateTableCascade,
|
||||
mysqlAutoIncrementValue,
|
||||
mysqlAutoIncrementPreviewSql,
|
||||
dropObjectPreviewSql,
|
||||
showDropObjectConfirm,
|
||||
dropTableChildObjectPreviewSql,
|
||||
@@ -462,7 +465,7 @@ const {
|
||||
confirmDropAllMongoIndexes,
|
||||
} = useSidebarDatabaseSpecificMutationRuntime({ activeNode, connectionStore });
|
||||
|
||||
const { isTableNotView, supportsTruncate, canDropTableCascade, canTruncateTableCascade, refreshDropTablePreviewSql, refreshTruncateTablePreviewSql, dropTable, refreshTableList, confirmDropTable, emptyTable, confirmEmptyTable, truncateTable, confirmTruncateTable } = useSidebarTableMutationRuntime({
|
||||
const { isTableNotView, supportsTruncate, supportsMysqlAutoIncrement, canDropTableCascade, canTruncateTableCascade, refreshDropTablePreviewSql, refreshTruncateTablePreviewSql, dropTable, refreshTableList, confirmDropTable, emptyTable, confirmEmptyTable, truncateTable, confirmTruncateTable, mysqlAutoIncrement, refreshMysqlAutoIncrementPreviewSql, confirmMysqlAutoIncrement } = useSidebarTableMutationRuntime({
|
||||
activeNode,
|
||||
releaseActiveNodeReference,
|
||||
connectionStore,
|
||||
@@ -3777,6 +3780,13 @@ function dangerRequest(request: Omit<SidebarDangerDialogRequest, "target">): Sid
|
||||
await onChange(checked);
|
||||
};
|
||||
}
|
||||
if (request.textInput?.onInput) {
|
||||
const onInput = request.textInput.onInput;
|
||||
request.textInput.onInput = async (value) => {
|
||||
activateActionTarget(target);
|
||||
await onInput(value);
|
||||
};
|
||||
}
|
||||
routedRequest.target = target;
|
||||
sidebarDangerTarget.value = routedRequest.target;
|
||||
return routedRequest;
|
||||
@@ -3872,6 +3882,30 @@ routeDangerDialog(showTruncateTableConfirm, () =>
|
||||
}),
|
||||
);
|
||||
|
||||
routeDangerDialog(showMysqlAutoIncrementConfirm, () =>
|
||||
dangerRequest({
|
||||
title: t("contextMenu.mysqlAutoIncrementTitle"),
|
||||
message: t("contextMenu.mysqlAutoIncrementMessage", { name: activeNode.value.label }),
|
||||
detailsText: t("contextMenu.mysqlAutoIncrementNonemptyHint"),
|
||||
get sql() {
|
||||
return mysqlAutoIncrementPreviewSql.value;
|
||||
},
|
||||
confirmLabel: t("contextMenu.mysqlAutoIncrement"),
|
||||
textInput: {
|
||||
value: mysqlAutoIncrementValue.value,
|
||||
label: t("contextMenu.mysqlAutoIncrementValue"),
|
||||
placeholder: "1",
|
||||
inputMode: "numeric",
|
||||
async onInput(value) {
|
||||
mysqlAutoIncrementValue.value = value;
|
||||
if (sidebarDangerTarget.value) activateActionTarget(sidebarDangerTarget.value);
|
||||
await refreshMysqlAutoIncrementPreviewSql();
|
||||
},
|
||||
},
|
||||
confirm: confirmMysqlAutoIncrement,
|
||||
}),
|
||||
);
|
||||
|
||||
routeDangerDialog(showDropObjectConfirm, () =>
|
||||
dangerRequest({
|
||||
title: dropObjectConfirmTitle(),
|
||||
@@ -4898,6 +4932,9 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
items.push({ label: t("contextMenu.duplicateStructure"), action: duplicateStructure, icon: CopyPlus });
|
||||
// Keep menu copy aligned with keyboard copy so frozen multi-selection and single-row fallback stay compatible.
|
||||
items.push(...treeTableClipboardMenuItems(node));
|
||||
if (supportsMysqlAutoIncrement.value) {
|
||||
items.push({ label: t("contextMenu.mysqlAutoIncrement"), action: mysqlAutoIncrement, icon: Gauge });
|
||||
}
|
||||
if (supportsTruncate.value) {
|
||||
destructiveActions.push({
|
||||
label: truncateMenuLabel(t("contextMenu.truncateTable")),
|
||||
|
||||
@@ -32,6 +32,7 @@ export const structureDocCopyTitle = ref("");
|
||||
export const isLoadingStructurePreview = ref(false);
|
||||
export const showEmptyTableConfirm = ref(false);
|
||||
export const showTruncateTableConfirm = ref(false);
|
||||
export const showMysqlAutoIncrementConfirm = ref(false);
|
||||
export const showRenameObjectDialog = ref(false);
|
||||
export const renameObjectName = ref("");
|
||||
export const renameObjectError = ref("");
|
||||
@@ -42,6 +43,9 @@ export const batchDropCascade = ref(false);
|
||||
export const emptyTablePreviewSql = ref("");
|
||||
export const truncateTablePreviewSql = ref("");
|
||||
export const truncateTableCascade = ref(false);
|
||||
export const mysqlAutoIncrementValue = ref("1");
|
||||
export const mysqlAutoIncrementPreviewSql = ref("");
|
||||
export const mysqlAutoIncrementPreviewKey = ref("");
|
||||
export const dropObjectPreviewSql = ref("");
|
||||
export const showDropObjectConfirm = ref(false);
|
||||
export const dropTableChildObjectPreviewSql = ref("");
|
||||
@@ -145,6 +149,7 @@ const openFlags = [
|
||||
showStructureDocCopyDialog,
|
||||
showEmptyTableConfirm,
|
||||
showTruncateTableConfirm,
|
||||
showMysqlAutoIncrementConfirm,
|
||||
showDropObjectConfirm,
|
||||
showRenameObjectDialog,
|
||||
showDuplicateDialog,
|
||||
|
||||
Vendored
+137
@@ -0,0 +1,137 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { shallowRef } from "vue";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { mysqlAutoIncrementPreviewKey, mysqlAutoIncrementPreviewSql, mysqlAutoIncrementValue, showMysqlAutoIncrementConfirm, sidebarDangerTarget } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toast: vi.fn(),
|
||||
buildMysqlAutoIncrementSql: vi.fn(),
|
||||
ensureConnected: vi.fn(),
|
||||
executeWithProductionGuard: vi.fn(),
|
||||
refreshMutatedTableDataTabsForNode: 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")>()),
|
||||
buildMysqlAutoIncrementSql: mocks.buildMysqlAutoIncrementSql,
|
||||
}));
|
||||
|
||||
import { useSidebarTableMutationRuntime } from "@/composables/useSidebarTableMutationRuntime";
|
||||
|
||||
function tableNode(): TreeNode {
|
||||
return { id: "mysql-1:sales:events", label: "events", type: "table", connectionId: "mysql-1", database: "sales", isExpanded: false };
|
||||
}
|
||||
|
||||
function runtime(driverProfile: string | undefined = "mysql") {
|
||||
const activeNode = shallowRef(tableNode());
|
||||
const connectionStore = {
|
||||
getConfig: () => ({ id: "mysql-1", db_type: "mysql", driver_profile: driverProfile }),
|
||||
ensureConnected: mocks.ensureConnected,
|
||||
} as any;
|
||||
const feature = useSidebarTableMutationRuntime({
|
||||
activeNode,
|
||||
releaseActiveNodeReference: vi.fn(),
|
||||
connectionStore,
|
||||
currentDatabaseType: () => "mysql",
|
||||
databaseTypeForNode: () => "mysql",
|
||||
executeWithProductionGuard: mocks.executeWithProductionGuard,
|
||||
closeDroppedTableObjectTabsForNode: vi.fn(),
|
||||
refreshMutatedTableDataTabsForNode: mocks.refreshMutatedTableDataTabsForNode,
|
||||
});
|
||||
return { ...feature, activeNode };
|
||||
}
|
||||
|
||||
describe("useSidebarTableMutationRuntime MySQL AUTO_INCREMENT action", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
sidebarDangerTarget.value = null;
|
||||
showMysqlAutoIncrementConfirm.value = false;
|
||||
mysqlAutoIncrementValue.value = "99";
|
||||
mysqlAutoIncrementPreviewSql.value = "";
|
||||
mysqlAutoIncrementPreviewKey.value = "";
|
||||
mocks.ensureConnected.mockResolvedValue(undefined);
|
||||
mocks.buildMysqlAutoIncrementSql.mockResolvedValue("ALTER TABLE `sales`.`events` AUTO_INCREMENT = 1;");
|
||||
mocks.executeWithProductionGuard.mockResolvedValue(undefined);
|
||||
mocks.refreshMutatedTableDataTabsForNode.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("opens with value 1 and builds only a preview", async () => {
|
||||
const feature = runtime();
|
||||
|
||||
feature.mysqlAutoIncrement();
|
||||
await vi.waitFor(() => expect(mocks.buildMysqlAutoIncrementSql).toHaveBeenCalled());
|
||||
|
||||
expect(feature.supportsMysqlAutoIncrement.value).toBe(true);
|
||||
expect(mysqlAutoIncrementValue.value).toBe("1");
|
||||
expect(showMysqlAutoIncrementConfirm.value).toBe(true);
|
||||
expect(mocks.ensureConnected).not.toHaveBeenCalled();
|
||||
expect(mocks.executeWithProductionGuard).not.toHaveBeenCalled();
|
||||
expect(mocks.buildMysqlAutoIncrementSql).toHaveBeenCalledWith(expect.objectContaining({ driverProfile: "mysql", schema: "sales", value: "1" }));
|
||||
});
|
||||
|
||||
it("canceling after open does not connect or execute", async () => {
|
||||
const feature = runtime();
|
||||
|
||||
feature.mysqlAutoIncrement();
|
||||
await vi.waitFor(() => expect(mocks.buildMysqlAutoIncrementSql).toHaveBeenCalledTimes(1));
|
||||
showMysqlAutoIncrementConfirm.value = false;
|
||||
|
||||
expect(mocks.ensureConnected).not.toHaveBeenCalled();
|
||||
expect(mocks.executeWithProductionGuard).not.toHaveBeenCalled();
|
||||
expect(mocks.refreshMutatedTableDataTabsForNode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("executes the validated preview through existing guards and refreshes open data tabs", async () => {
|
||||
const feature = runtime();
|
||||
mysqlAutoIncrementValue.value = "18446744073709551615";
|
||||
mocks.buildMysqlAutoIncrementSql.mockResolvedValueOnce("ALTER TABLE `sales`.`events` AUTO_INCREMENT = 18446744073709551615;");
|
||||
await feature.refreshMysqlAutoIncrementPreviewSql();
|
||||
|
||||
await feature.confirmMysqlAutoIncrement();
|
||||
|
||||
expect(mocks.ensureConnected).toHaveBeenCalledWith("mysql-1");
|
||||
expect(mocks.executeWithProductionGuard).toHaveBeenCalledWith(tableNode(), mysqlAutoIncrementPreviewSql.value, { database: "sales", schema: undefined });
|
||||
expect(mocks.refreshMutatedTableDataTabsForNode).toHaveBeenCalledWith(tableNode());
|
||||
});
|
||||
|
||||
it("excludes MySQL-compatible profiles", () => {
|
||||
expect(runtime("mariadb").supportsMysqlAutoIncrement.value).toBe(false);
|
||||
expect(runtime("tidb").supportsMysqlAutoIncrement.value).toBe(false);
|
||||
expect(runtime("oceanbase").supportsMysqlAutoIncrement.value).toBe(false);
|
||||
expect(runtime("dolt").supportsMysqlAutoIncrement.value).toBe(false);
|
||||
expect(runtime("goldendb").supportsMysqlAutoIncrement.value).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores out-of-order preview responses", async () => {
|
||||
const resolvers: Array<(sql: string) => void> = [];
|
||||
mocks.buildMysqlAutoIncrementSql.mockImplementation(() => new Promise<string>((resolve) => resolvers.push(resolve)));
|
||||
const feature = runtime();
|
||||
|
||||
mysqlAutoIncrementValue.value = "1";
|
||||
const first = feature.refreshMysqlAutoIncrementPreviewSql();
|
||||
mysqlAutoIncrementValue.value = "2";
|
||||
const second = feature.refreshMysqlAutoIncrementPreviewSql();
|
||||
resolvers[1]!("ALTER TABLE `sales`.`events` AUTO_INCREMENT = 2;");
|
||||
await second;
|
||||
resolvers[0]!("ALTER TABLE `sales`.`events` AUTO_INCREMENT = 1;");
|
||||
await first;
|
||||
|
||||
expect(mysqlAutoIncrementPreviewSql.value).toBe("ALTER TABLE `sales`.`events` AUTO_INCREMENT = 2;");
|
||||
});
|
||||
|
||||
it("rebuilds for the frozen confirmation target instead of executing another table's preview", async () => {
|
||||
const feature = runtime();
|
||||
mysqlAutoIncrementValue.value = "7";
|
||||
await feature.refreshMysqlAutoIncrementPreviewSql();
|
||||
const orders = { ...tableNode(), id: "mysql-1:sales:orders", label: "orders" };
|
||||
sidebarDangerTarget.value = orders;
|
||||
mocks.buildMysqlAutoIncrementSql.mockResolvedValueOnce("ALTER TABLE `sales`.`orders` AUTO_INCREMENT = 7;");
|
||||
|
||||
await feature.confirmMysqlAutoIncrement();
|
||||
|
||||
expect(mocks.buildMysqlAutoIncrementSql).toHaveBeenLastCalledWith(expect.objectContaining({ tableName: "orders", value: "7" }));
|
||||
expect(mocks.executeWithProductionGuard).toHaveBeenCalledWith(orders, "ALTER TABLE `sales`.`orders` AUTO_INCREMENT = 7;", { database: "sales", schema: undefined });
|
||||
});
|
||||
});
|
||||
@@ -4,9 +4,9 @@ import { useToast } from "@/composables/useToast";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import type { DatabaseType, TreeNode } from "@/types/database";
|
||||
import { supportsTableTruncate } from "@/lib/database/databaseCapabilities";
|
||||
import { buildDropTableSql, buildEmptyTableSql, buildTruncateTableSql, supportsDropTableCascade, supportsTruncateTableCascade, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import { buildDropTableSql, buildEmptyTableSql, buildMysqlAutoIncrementSql, buildTruncateTableSql, supportsDropTableCascade, supportsNativeMysqlAutoIncrement, supportsTruncateTableCascade, type MysqlAutoIncrementSqlOptions, type TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import { isSqlServerLinkedNode } from "@/lib/database/sqlServerLinkedServers";
|
||||
import { sidebarDangerTarget, showDropTableConfirm, showEmptyTableConfirm, showTruncateTableConfirm, dropTablePreviewSql, dropTableCascade, emptyTablePreviewSql, truncateTablePreviewSql, truncateTableCascade } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
import { sidebarDangerTarget, showDropTableConfirm, showEmptyTableConfirm, showMysqlAutoIncrementConfirm, showTruncateTableConfirm, dropTablePreviewSql, dropTableCascade, emptyTablePreviewSql, mysqlAutoIncrementPreviewKey, mysqlAutoIncrementPreviewSql, mysqlAutoIncrementValue, truncateTablePreviewSql, truncateTableCascade } from "@/components/sidebar/sidebarTreeDialogState";
|
||||
|
||||
interface SidebarTableMutationRuntimeOptions {
|
||||
activeNode: ShallowRef<TreeNode>;
|
||||
@@ -28,6 +28,7 @@ export function useSidebarTableMutationRuntime(options: SidebarTableMutationRunt
|
||||
const supportsTruncate = computed(() => supportsTableTruncate(currentDatabaseType()));
|
||||
const canDropTableCascade = computed(() => activeNode.value.type === "table" && supportsDropTableCascade(currentDatabaseType()));
|
||||
const canTruncateTableCascade = computed(() => activeNode.value.type === "table" && supportsTruncateTableCascade(currentDatabaseType()));
|
||||
const supportsMysqlAutoIncrement = computed(() => activeNode.value.type === "table" && supportsNativeMysqlAutoIncrement(activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined));
|
||||
|
||||
function tableAdminSqlOptions(optionsOverride?: { cascade?: boolean }): TableAdminSqlOptions {
|
||||
const result: TableAdminSqlOptions = {
|
||||
@@ -72,6 +73,59 @@ export function useSidebarTableMutationRuntime(options: SidebarTableMutationRunt
|
||||
truncateTablePreviewSql.value = await buildTruncateTableSql(truncateTableSqlOptions()).catch(() => "");
|
||||
}
|
||||
|
||||
function mysqlAutoIncrementSqlOptionsForNode(node: TreeNode): MysqlAutoIncrementSqlOptions {
|
||||
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
||||
return {
|
||||
databaseType: config?.db_type ?? databaseTypeForNode(node) ?? "mysql",
|
||||
driverProfile: config?.driver_profile,
|
||||
schema: node.database || node.schema,
|
||||
tableName: node.label,
|
||||
value: mysqlAutoIncrementValue.value,
|
||||
};
|
||||
}
|
||||
|
||||
function mysqlAutoIncrementPreviewKeyForNode(node: TreeNode, sqlOptions = mysqlAutoIncrementSqlOptionsForNode(node)): string {
|
||||
return JSON.stringify([node.id, node.connectionId, node.database, sqlOptions.databaseType, sqlOptions.driverProfile?.trim().toLowerCase() || "", sqlOptions.schema || "", sqlOptions.tableName, sqlOptions.value]);
|
||||
}
|
||||
|
||||
async function refreshMysqlAutoIncrementPreviewSql() {
|
||||
const node = activeNode.value;
|
||||
const sqlOptions = mysqlAutoIncrementSqlOptionsForNode(node);
|
||||
const previewKey = mysqlAutoIncrementPreviewKeyForNode(node, sqlOptions);
|
||||
mysqlAutoIncrementPreviewSql.value = "";
|
||||
mysqlAutoIncrementPreviewKey.value = "";
|
||||
const sql = await buildMysqlAutoIncrementSql(sqlOptions).catch(() => "");
|
||||
if (previewKey !== mysqlAutoIncrementPreviewKeyForNode(activeNode.value)) return;
|
||||
mysqlAutoIncrementPreviewSql.value = sql;
|
||||
mysqlAutoIncrementPreviewKey.value = sql ? previewKey : "";
|
||||
}
|
||||
|
||||
function mysqlAutoIncrement() {
|
||||
if (!supportsMysqlAutoIncrement.value) return;
|
||||
mysqlAutoIncrementValue.value = "1";
|
||||
mysqlAutoIncrementPreviewKey.value = "";
|
||||
void refreshMysqlAutoIncrementPreviewSql();
|
||||
showMysqlAutoIncrementConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmMysqlAutoIncrement() {
|
||||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
try {
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (!supportsNativeMysqlAutoIncrement(config)) throw new Error("Setting AUTO_INCREMENT is supported only for native MySQL connections.");
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const sqlOptions = mysqlAutoIncrementSqlOptionsForNode(node);
|
||||
const previewKey = mysqlAutoIncrementPreviewKeyForNode(node, sqlOptions);
|
||||
const sql = mysqlAutoIncrementPreviewKey.value === previewKey && mysqlAutoIncrementPreviewSql.value ? mysqlAutoIncrementPreviewSql.value : await buildMysqlAutoIncrementSql(sqlOptions);
|
||||
await options.executeWithProductionGuard(node, sql, { database: node.database, schema: node.schema });
|
||||
toast(t("contextMenu.mysqlAutoIncrementSuccess", { name: node.label, value: mysqlAutoIncrementValue.value }), 3000);
|
||||
await options.refreshMutatedTableDataTabsForNode(node);
|
||||
} catch (error: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: error?.message || String(error) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function dropTable() {
|
||||
dropTableCascade.value = false;
|
||||
void refreshDropTablePreviewSql();
|
||||
@@ -144,6 +198,7 @@ export function useSidebarTableMutationRuntime(options: SidebarTableMutationRunt
|
||||
supportsTruncate,
|
||||
canDropTableCascade,
|
||||
canTruncateTableCascade,
|
||||
supportsMysqlAutoIncrement,
|
||||
refreshDropTablePreviewSql,
|
||||
refreshTruncateTablePreviewSql,
|
||||
dropTable,
|
||||
@@ -153,5 +208,8 @@ export function useSidebarTableMutationRuntime(options: SidebarTableMutationRunt
|
||||
confirmEmptyTable,
|
||||
truncateTable,
|
||||
confirmTruncateTable,
|
||||
mysqlAutoIncrement,
|
||||
refreshMysqlAutoIncrementPreviewSql,
|
||||
confirmMysqlAutoIncrement,
|
||||
};
|
||||
}
|
||||
@@ -2680,6 +2680,11 @@ export default {
|
||||
dropTable: "Drop Table",
|
||||
emptyTable: "Empty Table",
|
||||
truncateTable: "Truncate Table",
|
||||
mysqlAutoIncrement: "Set AUTO_INCREMENT",
|
||||
mysqlAutoIncrementTitle: "Set MySQL AUTO_INCREMENT",
|
||||
mysqlAutoIncrementMessage: 'Set the next AUTO_INCREMENT value requested for "{name}"?',
|
||||
mysqlAutoIncrementNonemptyHint: "For a nonempty table, MySQL does not reuse occupied IDs. The effective next value remains at least MAX(id) + 1.",
|
||||
mysqlAutoIncrementValue: "Requested next value",
|
||||
duplicateStructure: "Clone as New Table",
|
||||
renameObject: "Rename",
|
||||
renameObjectTitle: "Rename Object",
|
||||
@@ -2695,6 +2700,7 @@ export default {
|
||||
confirmEmptyTableMessage: 'Are you sure you want to delete all data from "{name}"?',
|
||||
confirmTruncateTableTitle: "Truncate Table",
|
||||
confirmTruncateTableMessage: 'Are you sure you want to truncate "{name}"? This will remove all rows.',
|
||||
mysqlAutoIncrementSuccess: 'Requested AUTO_INCREMENT {value} for table "{name}"',
|
||||
dropTableSuccess: 'Table "{name}" dropped',
|
||||
editView: "Edit View",
|
||||
viewSource: "View Source",
|
||||
|
||||
@@ -2604,6 +2604,11 @@ export default withEnglishFallback({
|
||||
dropTable: "删除表",
|
||||
emptyTable: "清空数据",
|
||||
truncateTable: "截断表",
|
||||
mysqlAutoIncrement: "设置自增起始值",
|
||||
mysqlAutoIncrementTitle: "设置 MySQL 自增起始值",
|
||||
mysqlAutoIncrementMessage: "确定要设置表「{name}」请求的下一个自增值吗?",
|
||||
mysqlAutoIncrementNonemptyHint: "对于非空表,MySQL 不会复用已占用的 ID;实际下一个值不会低于 MAX(id) + 1。",
|
||||
mysqlAutoIncrementValue: "请求的下一个值",
|
||||
duplicateStructure: "克隆为新表",
|
||||
renameObject: "重命名",
|
||||
renameObjectTitle: "重命名对象",
|
||||
@@ -2619,6 +2624,7 @@ export default withEnglishFallback({
|
||||
confirmEmptyTableMessage: "确定要删除「{name}」中的所有数据吗?",
|
||||
confirmTruncateTableTitle: "截断表",
|
||||
confirmTruncateTableMessage: "确定要截断「{name}」吗?这将删除所有行。",
|
||||
mysqlAutoIncrementSuccess: "已为表「{name}」请求自增值 {value}",
|
||||
dropTableSuccess: "表「{name}」已删除",
|
||||
editView: "编辑视图",
|
||||
viewSource: "查看源码",
|
||||
|
||||
+40
-1
@@ -8,16 +8,55 @@ const apiMock = vi.hoisted(() => ({
|
||||
listTriggers: vi.fn(),
|
||||
buildCreateTableSql: vi.fn(),
|
||||
buildDuplicateTableStructureSql: vi.fn(),
|
||||
buildMysqlAutoIncrementSql: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => apiMock);
|
||||
|
||||
import { buildDuplicateTableStructurePlan, collectDuplicateTableColumnComments, damengDropSchemaExecutionSchema, damengDuplicateTableCreateOptions, duplicateTableStructureRequiresScript, oracleDuplicateTableCreateOptions } from "@/lib/database/dbAdminSql";
|
||||
import { buildDuplicateTableStructurePlan, buildMysqlAutoIncrementSql, collectDuplicateTableColumnComments, damengDropSchemaExecutionSchema, damengDuplicateTableCreateOptions, duplicateTableStructureRequiresScript, oracleDuplicateTableCreateOptions, supportsNativeMysqlAutoIncrement } from "@/lib/database/dbAdminSql";
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("MySQL AUTO_INCREMENT administration", () => {
|
||||
it("is available only for explicit native MySQL connections", () => {
|
||||
expect(supportsNativeMysqlAutoIncrement({ db_type: "mysql", driver_profile: undefined })).toBe(true);
|
||||
expect(supportsNativeMysqlAutoIncrement({ db_type: "mysql", driver_profile: "" })).toBe(true);
|
||||
expect(supportsNativeMysqlAutoIncrement({ db_type: "mysql", driver_profile: " MySQL " })).toBe(true);
|
||||
|
||||
for (const connection of [
|
||||
{ db_type: "jdbc", driver_profile: "mysql" },
|
||||
{ db_type: "mysql", driver_profile: "mariadb" },
|
||||
{ db_type: "mysql", driver_profile: "tidb" },
|
||||
{ db_type: "mysql", driver_profile: "oceanbase" },
|
||||
{ db_type: "mysql", driver_profile: "dolt" },
|
||||
{ db_type: "goldendb", driver_profile: "goldendb" },
|
||||
{ db_type: "mysql", driver_profile: "goldendb" },
|
||||
{ db_type: "doris", driver_profile: "doris" },
|
||||
{ db_type: "doris", driver_profile: "selectdb" },
|
||||
{ db_type: "starrocks", driver_profile: "starrocks" },
|
||||
] as const) {
|
||||
expect(supportsNativeMysqlAutoIncrement(connection as any)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the counter as a decimal string across the frontend boundary", async () => {
|
||||
apiMock.buildMysqlAutoIncrementSql.mockResolvedValue("ALTER TABLE `sales`.`events` AUTO_INCREMENT = 18446744073709551615;");
|
||||
|
||||
await expect(
|
||||
buildMysqlAutoIncrementSql({
|
||||
databaseType: "mysql",
|
||||
driverProfile: "mysql",
|
||||
schema: "sales",
|
||||
tableName: "events",
|
||||
value: "18446744073709551615",
|
||||
}),
|
||||
).resolves.toContain("18446744073709551615");
|
||||
expect(apiMock.buildMysqlAutoIncrementSql).toHaveBeenCalledWith(expect.objectContaining({ value: "18446744073709551615" }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectDuplicateTableColumnComments", () => {
|
||||
it("preserves meaningful whitespace and excludes whitespace-only comments", () => {
|
||||
expect(
|
||||
|
||||
@@ -223,6 +223,7 @@ export const buildDropTableSql = forward("buildDropTableSql");
|
||||
export const buildDropTableChildObjectSql = forward("buildDropTableChildObjectSql");
|
||||
export const buildEmptyTableSql = forward("buildEmptyTableSql");
|
||||
export const buildTruncateTableSql = forward("buildTruncateTableSql");
|
||||
export const buildMysqlAutoIncrementSql = forward("buildMysqlAutoIncrementSql");
|
||||
export const buildDropDatabaseSql = forward("buildDropDatabaseSql");
|
||||
export const buildCreateSchemaSql = forward("buildCreateSchemaSql");
|
||||
export const buildUpdateDatabasePropertiesSql = forward("buildUpdateDatabasePropertiesSql");
|
||||
|
||||
@@ -159,7 +159,7 @@ import type { BuildEditableObjectSourceSqlInput, BuildRoutineRenameObjectSourceI
|
||||
import type { BuildViewDdlInput } from "@/lib/table/viewDdl";
|
||||
import type { BuildRenameObjectSqlOptions } from "@/lib/table/objectRenameSql";
|
||||
import type { CreateDatabaseSqlOptions } from "@/lib/database/createDatabaseSql";
|
||||
import type { DatabaseNameSqlOptions, DatabasePropertyEditSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, CopyTableDataSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import type { DatabaseNameSqlOptions, DatabasePropertyEditSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, CopyTableDataSqlOptions, MysqlAutoIncrementSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/export/databaseExport";
|
||||
import { loadBrowserAppState, saveBrowserAppState } from "@/lib/backend/browserAppStateStorage";
|
||||
import type { DataCompareFromTablesOptions, DataCompareFromTablesPreparation, DataCompareSyncPlan, DataCompareSyncPlanOptions, DataComparePreparation, DataComparePreparationOptions } from "@/lib/dataGrid/dataCompare";
|
||||
@@ -1238,6 +1238,10 @@ export async function buildTruncateTableSql(options: TableAdminSqlOptions): Prom
|
||||
return post("/api/query/build-truncate-table-sql", { options });
|
||||
}
|
||||
|
||||
export async function buildMysqlAutoIncrementSql(options: MysqlAutoIncrementSqlOptions): Promise<string> {
|
||||
return post("/api/query/build-mysql-auto-increment-sql", { options });
|
||||
}
|
||||
|
||||
export async function buildDropDatabaseSql(options: DatabaseNameSqlOptions): Promise<string> {
|
||||
return post("/api/query/build-drop-database-sql", { options });
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ import type { BuildEditableObjectSourceSqlInput, BuildRoutineRenameObjectSourceI
|
||||
import type { BuildViewDdlInput } from "@/lib/table/viewDdl";
|
||||
import type { BuildRenameObjectSqlOptions } from "@/lib/table/objectRenameSql";
|
||||
import type { CreateDatabaseSqlOptions } from "@/lib/database/createDatabaseSql";
|
||||
import type { DatabaseNameSqlOptions, DatabasePropertyEditSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, CopyTableDataSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import type { DatabaseNameSqlOptions, DatabasePropertyEditSqlOptions, DropTableChildObjectSqlOptions, DropObjectSqlOptions, DuplicateTableStructureSqlOptions, CopyTableDataSqlOptions, MysqlAutoIncrementSqlOptions, SchemaNameSqlOptions, TableAdminSqlOptions } from "@/lib/database/dbAdminSql";
|
||||
import type { BuildDatabaseSqlExportOptions, BuildExportInsertStatementsOptions } from "@/lib/export/databaseExport";
|
||||
|
||||
export interface SshPromptResolution {
|
||||
@@ -1418,6 +1418,10 @@ export async function buildTruncateTableSql(options: TableAdminSqlOptions): Prom
|
||||
return invoke("build_truncate_table_sql", { options });
|
||||
}
|
||||
|
||||
export async function buildMysqlAutoIncrementSql(options: MysqlAutoIncrementSqlOptions): Promise<string> {
|
||||
return invoke("build_mysql_auto_increment_sql", { options });
|
||||
}
|
||||
|
||||
export async function buildDropDatabaseSql(options: DatabaseNameSqlOptions): Promise<string> {
|
||||
return invoke("build_drop_database_sql", { options });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ColumnInfo, DatabaseObjectType, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database";
|
||||
import type { ColumnInfo, ConnectionConfig, DatabaseObjectType, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { createColumnDrafts, createForeignKeyDrafts, createIndexDrafts, createTriggerDrafts } from "@/lib/table/tableStructureEditorState";
|
||||
import type { BuildTableStructureChangeSqlOptions } from "@/lib/table/tableStructureEditorSql";
|
||||
@@ -18,6 +18,14 @@ export interface TableAdminSqlOptions {
|
||||
cascade?: boolean;
|
||||
}
|
||||
|
||||
export interface MysqlAutoIncrementSqlOptions {
|
||||
databaseType: DatabaseType;
|
||||
driverProfile?: string | null;
|
||||
schema?: string | null;
|
||||
tableName: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type TableChildObjectType = "COLUMN" | "INDEX" | "FOREIGN_KEY" | "TRIGGER";
|
||||
|
||||
export interface DropTableChildObjectSqlOptions {
|
||||
@@ -278,6 +286,16 @@ export function buildTruncateTableSql(options: TableAdminSqlOptions): Promise<st
|
||||
return api.buildTruncateTableSql(options);
|
||||
}
|
||||
|
||||
export function buildMysqlAutoIncrementSql(options: MysqlAutoIncrementSqlOptions): Promise<string> {
|
||||
return api.buildMysqlAutoIncrementSql(options);
|
||||
}
|
||||
|
||||
export function supportsNativeMysqlAutoIncrement(connection: Pick<ConnectionConfig, "db_type" | "driver_profile"> | undefined): boolean {
|
||||
if (connection?.db_type !== "mysql") return false;
|
||||
const profile = connection.driver_profile?.trim().toLowerCase();
|
||||
return !profile || profile === "mysql";
|
||||
}
|
||||
|
||||
const DROP_TABLE_CASCADE_DATABASE_TYPES: readonly DatabaseType[] = ["postgres", "redshift", "gaussdb", "kwdb", "kingbase", "highgo", "uxdb", "vastbase", "opengauss"];
|
||||
const TRUNCATE_TABLE_CASCADE_DATABASE_TYPES: readonly DatabaseType[] = ["postgres", "gaussdb", "kwdb", "kingbase", "highgo", "uxdb", "vastbase", "opengauss"];
|
||||
|
||||
|
||||
@@ -12,6 +12,14 @@ export interface SidebarDangerDialogProgress {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SidebarDangerDialogTextInput {
|
||||
value: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
inputMode?: "text" | "numeric";
|
||||
onInput?: (value: string) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface SidebarDangerDialogRequest {
|
||||
target: SidebarActionTarget;
|
||||
title: string;
|
||||
@@ -24,5 +32,6 @@ export interface SidebarDangerDialogRequest {
|
||||
closeOnConfirm?: boolean;
|
||||
progress?: SidebarDangerDialogProgress;
|
||||
option?: SidebarDangerDialogOption;
|
||||
textInput?: SidebarDangerDialogTextInput;
|
||||
confirm: () => void | Promise<void>;
|
||||
}
|
||||
Reference in new issue
Block a user