feat(mysql): add auto-increment counter action

Closes #3019
This commit is contained in:
t8y2 committed 2026-08-14 01:59:04 +00:00
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,
@@ -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,
};
}
+6
View File
@@ -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",
+6
View File
@@ -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
View File
@@ -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(
+1
View File
@@ -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");
+5 -1
View File
@@ -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 });
}
+5 -1
View File
@@ -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 });
}
+19 -1
View File
@@ -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>;
}