feat(mysql): 支持查看和修改表存储引擎
This commit is contained in:
21 files changed
+485
-5
No files matched your search
@@ -33,6 +33,7 @@ import { invalidateObjectMetadataCache, loadObjectMetadataFacet, type ObjectMeta
|
||||
import { invalidateTableMetadataCache } from "@/lib/metadata/tableMetadataCache";
|
||||
import { type BuildTableStructureChangeSqlOptions, type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql";
|
||||
import { buildMysqlAutoIncrementCounterStatement, canEditMysqlAutoIncrementCounter, refreshMysqlAutoIncrementCounterDraft } from "@/lib/table/mysqlAutoIncrementCounter";
|
||||
import { MYSQL_STORAGE_ENGINES_SQL, mysqlTableEngineSql, mysqlTableEngineSqlOption, parseMysqlTableEngineMetadata, refreshMysqlTableEngineDraft, supportsMysqlTableEngine } from "@/lib/table/mysqlTableEngine";
|
||||
import { PRESET_FIELDS_TEMPLATE_ID, createTableColumnTemplateDrafts } from "@/lib/table/tableColumnTemplates";
|
||||
import { getMysqlDataTypeHelp } from "@/lib/table/mysqlDataTypeHelp";
|
||||
import { getPostgresDataTypeHelp, gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp";
|
||||
@@ -917,6 +918,11 @@ const mysqlAutoIncrementValue = ref<string>();
|
||||
const originalMysqlAutoIncrementValue = ref<string>();
|
||||
const mysqlAutoIncrementLoading = ref(false);
|
||||
const mysqlAutoIncrementLoadError = ref("");
|
||||
const mysqlTableEngine = ref("");
|
||||
const originalMysqlTableEngine = ref("");
|
||||
const mysqlTableEngineOptions = ref<string[]>([]);
|
||||
const mysqlTableEngineLoading = ref(false);
|
||||
const mysqlTableEngineLoadError = ref("");
|
||||
const tableOwner = ref("");
|
||||
const originalTableOwner = ref("");
|
||||
const tableOwnerLoading = ref(false);
|
||||
@@ -927,6 +933,7 @@ const tableOwnerRolesLoadError = ref("");
|
||||
const supportsTableOwner = computed(() => !isCreateMode.value && databaseType.value === "postgres");
|
||||
const canEditMysqlAutoIncrement = computed(() => canEditMysqlAutoIncrementCounter(connection.value, isCreateMode.value, columns.value));
|
||||
const canBuildMysqlAutoIncrement = computed(() => canEditMysqlAutoIncrement.value && !mysqlAutoIncrementLoading.value && !mysqlAutoIncrementLoadError.value && originalMysqlAutoIncrementValue.value !== undefined);
|
||||
const supportsMysqlEngine = computed(() => supportsMysqlTableEngine(connection.value));
|
||||
const tableOwnerOptions = computed(() => {
|
||||
const owner = tableOwner.value;
|
||||
if (!owner || tableOwnerRoles.value.includes(owner)) return tableOwnerRoles.value;
|
||||
@@ -950,6 +957,7 @@ let structureLoadRequestId = 0;
|
||||
let tableOwnerLoadRequestId = 0;
|
||||
let tableOwnerRolesLoadRequestId = 0;
|
||||
let mysqlAutoIncrementLoadRequestId = 0;
|
||||
let mysqlTableEngineLoadRequestId = 0;
|
||||
let dataTypeOptionsRequestId = 0;
|
||||
let sqlPreviewDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let deferredSqlPreviewRefresh = false;
|
||||
@@ -1137,6 +1145,8 @@ function createCurrentDraft(initialized = true): TableStructureEditorDraft {
|
||||
originalTableComment: originalTableComment.value,
|
||||
mysqlAutoIncrementValue: mysqlAutoIncrementValue.value,
|
||||
originalMysqlAutoIncrementValue: originalMysqlAutoIncrementValue.value,
|
||||
mysqlTableEngine: mysqlTableEngine.value,
|
||||
originalMysqlTableEngine: originalMysqlTableEngine.value,
|
||||
tableOwner: tableOwner.value,
|
||||
originalTableOwner: originalTableOwner.value,
|
||||
columns: cloneDraftValue(columns.value),
|
||||
@@ -1169,6 +1179,8 @@ function restoreDraft(draft: TableStructureEditorDraft) {
|
||||
originalTableComment.value = draft.originalTableComment || "";
|
||||
mysqlAutoIncrementValue.value = draft.mysqlAutoIncrementValue;
|
||||
originalMysqlAutoIncrementValue.value = draft.originalMysqlAutoIncrementValue;
|
||||
mysqlTableEngine.value = draft.mysqlTableEngine || "";
|
||||
originalMysqlTableEngine.value = draft.originalMysqlTableEngine || "";
|
||||
tableOwner.value = draft.tableOwner || "";
|
||||
originalTableOwner.value = draft.originalTableOwner || "";
|
||||
columns.value = cloneDraftValue(draft.columns || []);
|
||||
@@ -1252,7 +1264,7 @@ function markDraftHydratedAndSync() {
|
||||
|
||||
function hasPendingStructureChanges(): boolean {
|
||||
if (isCreateMode.value) {
|
||||
return !!newTableName.value.trim() || !!tableComment.value.trim() || columns.value.length > 0 || indexes.value.length > 0 || foreignKeys.value.length > 0 || triggers.value.length > 0;
|
||||
return !!newTableName.value.trim() || !!tableComment.value.trim() || mysqlTableEngine.value !== originalMysqlTableEngine.value || columns.value.length > 0 || indexes.value.length > 0 || foreignKeys.value.length > 0 || triggers.value.length > 0;
|
||||
}
|
||||
const scope = captureStructureRefreshScope();
|
||||
return (
|
||||
@@ -1261,6 +1273,7 @@ function hasPendingStructureChanges(): boolean {
|
||||
scope.foreignKeys ||
|
||||
scope.triggers ||
|
||||
scope.tableComment ||
|
||||
mysqlTableEngine.value.toLowerCase() !== originalMysqlTableEngine.value.toLowerCase() ||
|
||||
(canBuildMysqlAutoIncrement.value && mysqlAutoIncrementValue.value !== originalMysqlAutoIncrementValue.value) ||
|
||||
(supportsTableOwner.value && tableOwner.value.trim() !== originalTableOwner.value.trim())
|
||||
);
|
||||
@@ -1405,6 +1418,7 @@ function structureChangeOptions(): BuildTableStructureChangeSqlOptions {
|
||||
triggers: triggers.value,
|
||||
tableComment: tableComment.value,
|
||||
originalTableComment: isCreateMode.value ? undefined : originalTableComment.value,
|
||||
mysqlEngine: mysqlTableEngineSqlOption({ value: mysqlTableEngine.value, originalValue: originalMysqlTableEngine.value }, isCreateMode.value, supportsMysqlEngine.value && !mysqlTableEngineLoading.value && !mysqlTableEngineLoadError.value),
|
||||
partitioned: isPartitionedParent.value,
|
||||
isGaussdbMMode: connection.value?.driver_profile?.toLowerCase() === "gaussdb-m",
|
||||
};
|
||||
@@ -1491,6 +1505,7 @@ const canApply = computed(
|
||||
!saving.value &&
|
||||
!postSaveRefreshing.value &&
|
||||
!secondaryMetadataLoading.value &&
|
||||
!mysqlTableEngineLoading.value &&
|
||||
!sqlPreviewLoading.value &&
|
||||
!sqlPreviewPending.value &&
|
||||
pendingStatements.value.length > 0 &&
|
||||
@@ -1542,6 +1557,12 @@ function resetState() {
|
||||
mysqlAutoIncrementLoadRequestId += 1;
|
||||
mysqlAutoIncrementLoading.value = false;
|
||||
mysqlAutoIncrementLoadError.value = "";
|
||||
mysqlTableEngine.value = "";
|
||||
originalMysqlTableEngine.value = "";
|
||||
mysqlTableEngineOptions.value = [];
|
||||
mysqlTableEngineLoadRequestId += 1;
|
||||
mysqlTableEngineLoading.value = false;
|
||||
mysqlTableEngineLoadError.value = "";
|
||||
tableOwner.value = "";
|
||||
originalTableOwner.value = "";
|
||||
tableOwnerLoadRequestId += 1;
|
||||
@@ -1578,9 +1599,9 @@ async function reloadStructureFromDatabase() {
|
||||
loadedMetadataFacets.clear();
|
||||
if (refreshDdl) {
|
||||
ddlFetched.value = false;
|
||||
await Promise.all([fetchDdl(true), loadTableOwner(true), loadTableOwnerRoles()]);
|
||||
await Promise.all([fetchDdl(true), loadTableOwner(true), loadTableOwnerRoles(), loadMysqlTableEngine(true)]);
|
||||
} else {
|
||||
await Promise.all([loadStructure(false, visibleTableStructureRefreshScope(activeTab.value), true, { blockSecondaryMetadata: true, forceDdl: true, forceMetadata: true }), loadTableOwner(true), loadTableOwnerRoles()]);
|
||||
await Promise.all([loadStructure(false, visibleTableStructureRefreshScope(activeTab.value), true, { blockSecondaryMetadata: true, forceDdl: true, forceMetadata: true }), loadTableOwner(true), loadTableOwnerRoles(), loadMysqlTableEngine(true)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1641,6 +1662,43 @@ async function loadMysqlAutoIncrementCounter(preserveDraft = false) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMysqlTableEngine(preserveDraft = false) {
|
||||
const requestId = ++mysqlTableEngineLoadRequestId;
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
if (!supportsMysqlEngine.value || !connectionId || !database) {
|
||||
mysqlTableEngine.value = "";
|
||||
originalMysqlTableEngine.value = "";
|
||||
mysqlTableEngineOptions.value = [];
|
||||
mysqlTableEngineLoading.value = false;
|
||||
mysqlTableEngineLoadError.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
mysqlTableEngineLoading.value = true;
|
||||
mysqlTableEngineLoadError.value = "";
|
||||
try {
|
||||
await store.ensureConnected(connectionId);
|
||||
const [enginesResult, tableResult] = await Promise.all([
|
||||
api.executeQuery(connectionId, database, MYSQL_STORAGE_ENGINES_SQL, undefined, undefined, { maxRows: 100 }),
|
||||
isCreateMode.value || !props.tableName ? Promise.resolve(undefined) : api.executeQuery(connectionId, database, mysqlTableEngineSql(database, props.tableName), undefined, undefined, { maxRows: 1 }),
|
||||
]);
|
||||
if (requestId !== mysqlTableEngineLoadRequestId) return;
|
||||
const metadata = parseMysqlTableEngineMetadata(enginesResult, tableResult);
|
||||
const draft = refreshMysqlTableEngineDraft(metadata, { value: mysqlTableEngine.value, originalValue: originalMysqlTableEngine.value }, isCreateMode.value, preserveDraft);
|
||||
const options = [...metadata.engines];
|
||||
if (draft.value && !options.some((option) => option.toLowerCase() === draft.value.toLowerCase())) options.unshift(draft.value);
|
||||
mysqlTableEngineOptions.value = options;
|
||||
mysqlTableEngine.value = draft.value;
|
||||
originalMysqlTableEngine.value = draft.originalValue;
|
||||
} catch (error: any) {
|
||||
if (requestId !== mysqlTableEngineLoadRequestId) return;
|
||||
mysqlTableEngineLoadError.value = error?.message || String(error);
|
||||
} finally {
|
||||
if (requestId === mysqlTableEngineLoadRequestId) mysqlTableEngineLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTableOwner(force = false, preserveDraft = false) {
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
@@ -1852,7 +1910,7 @@ async function loadStructure(
|
||||
|
||||
async function refreshStructureAfterSave(scope: TableStructureRefreshScope, characterLengthUnitsAfterSave: ReadonlyMap<string, string>) {
|
||||
try {
|
||||
await Promise.all([loadStructure(true, scope, false, { blockSecondaryMetadata: true, characterLengthUnitsAfterSave }), loadTableOwner(true)]);
|
||||
await Promise.all([loadStructure(true, scope, false, { blockSecondaryMetadata: true, characterLengthUnitsAfterSave }), loadTableOwner(true), loadMysqlTableEngine(false)]);
|
||||
} catch (e) {
|
||||
console.warn("[DBX][structure-editor:post-save-refresh-failed]", e);
|
||||
} finally {
|
||||
@@ -3354,6 +3412,7 @@ onMounted(() => {
|
||||
observeStructureHorizontalScroller();
|
||||
void loadTableOwner(false, props.draft?.tableOwner !== undefined);
|
||||
void loadTableOwnerRoles();
|
||||
void loadMysqlTableEngine(props.draft?.mysqlTableEngine !== undefined);
|
||||
if (props.draft?.initialized) {
|
||||
void hydrateRestoredDraftFromDatabase().then(() => {
|
||||
applyInitialStructureTarget();
|
||||
@@ -3375,6 +3434,9 @@ onActivated(() => {
|
||||
void loadDynamicDataTypeOptions();
|
||||
if (supportsTableOwner.value && !loadedMetadataFacets.has("owner")) void loadTableOwner(false, props.draft?.tableOwner !== undefined);
|
||||
if (supportsTableOwner.value && !tableOwnerRolesLoading.value && tableOwnerRoles.value.length === 0 && !tableOwnerRolesLoadError.value) void loadTableOwnerRoles();
|
||||
if (supportsMysqlEngine.value && !mysqlTableEngineLoading.value && mysqlTableEngineOptions.value.length === 0 && !mysqlTableEngineLoadError.value) {
|
||||
void loadMysqlTableEngine(props.draft?.mysqlTableEngine !== undefined);
|
||||
}
|
||||
if (props.draft?.initialized && !draftHydrated) {
|
||||
restoreDraft(props.draft);
|
||||
applyInitialStructureTarget();
|
||||
@@ -3503,6 +3565,10 @@ watch(
|
||||
originalMysqlAutoIncrementValue,
|
||||
mysqlAutoIncrementLoading,
|
||||
mysqlAutoIncrementLoadError,
|
||||
mysqlTableEngine,
|
||||
originalMysqlTableEngine,
|
||||
mysqlTableEngineLoading,
|
||||
mysqlTableEngineLoadError,
|
||||
tableOwner,
|
||||
columns,
|
||||
indexes,
|
||||
@@ -3625,6 +3691,29 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="supportsMysqlEngine" class="flex shrink-0 items-center gap-2">
|
||||
<label class="shrink-0 font-medium text-muted-foreground">{{ t("structureEditor.mysqlTableEngine") }}</label>
|
||||
<SearchableSelect
|
||||
v-model="mysqlTableEngine"
|
||||
:options="mysqlTableEngineOptions"
|
||||
:placeholder="t('structureEditor.mysqlTableEnginePlaceholder')"
|
||||
:search-placeholder="t('structureEditor.mysqlTableEngineSearchPlaceholder')"
|
||||
:empty-text="t('structureEditor.mysqlTableEngineEmpty')"
|
||||
:loading-text="t('common.loading')"
|
||||
:loading="mysqlTableEngineLoading"
|
||||
:disabled="mysqlTableEngineLoading || !!mysqlTableEngineLoadError || saving"
|
||||
:trigger-class="[structureMonoControlClass, 'w-[220px] max-w-[220px]']"
|
||||
data-mysql-table-engine-select
|
||||
/>
|
||||
<Loader2 v-if="mysqlTableEngineLoading" :class="[structureIconClass, 'animate-spin text-muted-foreground']" />
|
||||
<Tooltip v-else-if="mysqlTableEngineLoadError">
|
||||
<TooltipTrigger as-child>
|
||||
<AlertTriangle :class="[structureIconClass, 'shrink-0 text-destructive']" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("structureEditor.mysqlTableEngineLoadFailed", { message: mysqlTableEngineLoadError }) }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="canEditMysqlAutoIncrement" class="flex shrink-0 items-center gap-2">
|
||||
<label class="shrink-0 font-medium text-muted-foreground">AUTO_INCREMENT</label>
|
||||
<Input
|
||||
|
||||
@@ -3869,6 +3869,11 @@ export default {
|
||||
editComment: "Edit comment",
|
||||
commentPlaceholder: "Enter column comment...",
|
||||
tableCommentPlaceholder: "Enter table comment...",
|
||||
mysqlTableEngine: "Storage engine",
|
||||
mysqlTableEnginePlaceholder: "Select a storage engine",
|
||||
mysqlTableEngineSearchPlaceholder: "Search storage engines",
|
||||
mysqlTableEngineEmpty: "No storage engines available",
|
||||
mysqlTableEngineLoadFailed: "Failed to load storage engines: {message}",
|
||||
owner: "Owner",
|
||||
ownerPlaceholder: "Enter role name...",
|
||||
ownerSearchPlaceholder: "Search or enter a role name...",
|
||||
|
||||
@@ -3684,6 +3684,11 @@ export default withEnglishFallback({
|
||||
editComment: "Editar comentario",
|
||||
commentPlaceholder: "Ingresar comentario de columna...",
|
||||
tableCommentPlaceholder: "Ingresar comentario de tabla...",
|
||||
mysqlTableEngine: "Motor de almacenamiento",
|
||||
mysqlTableEnginePlaceholder: "Seleccionar un motor de almacenamiento",
|
||||
mysqlTableEngineSearchPlaceholder: "Buscar motores de almacenamiento",
|
||||
mysqlTableEngineEmpty: "No hay motores de almacenamiento disponibles",
|
||||
mysqlTableEngineLoadFailed: "Error al cargar los motores de almacenamiento: {message}",
|
||||
owner: "Propietario",
|
||||
ownerPlaceholder: "Ingrese el nombre del rol...",
|
||||
ownerSearchPlaceholder: "Buscar o ingresar un rol...",
|
||||
|
||||
@@ -3682,6 +3682,11 @@ export default withEnglishFallback({
|
||||
editComment: "Modifica commento",
|
||||
commentPlaceholder: "Inserisci commento colonna...",
|
||||
tableCommentPlaceholder: "Inserisci commento tabella...",
|
||||
mysqlTableEngine: "Motore di archiviazione",
|
||||
mysqlTableEnginePlaceholder: "Seleziona un motore di archiviazione",
|
||||
mysqlTableEngineSearchPlaceholder: "Cerca motori di archiviazione",
|
||||
mysqlTableEngineEmpty: "Nessun motore di archiviazione disponibile",
|
||||
mysqlTableEngineLoadFailed: "Impossibile caricare i motori di archiviazione: {message}",
|
||||
owner: "Proprietario",
|
||||
ownerPlaceholder: "Inserisci il nome del ruolo...",
|
||||
ownerSearchPlaceholder: "Cerca o inserisci un ruolo...",
|
||||
|
||||
@@ -3744,6 +3744,11 @@ export default withEnglishFallback({
|
||||
editComment: "コメントを編集",
|
||||
commentPlaceholder: "列コメントを入力...",
|
||||
tableCommentPlaceholder: "テーブルコメントを入力...",
|
||||
mysqlTableEngine: "ストレージエンジン",
|
||||
mysqlTableEnginePlaceholder: "ストレージエンジンを選択",
|
||||
mysqlTableEngineSearchPlaceholder: "ストレージエンジンを検索",
|
||||
mysqlTableEngineEmpty: "利用可能なストレージエンジンがありません",
|
||||
mysqlTableEngineLoadFailed: "ストレージエンジンの読み込みに失敗しました: {message}",
|
||||
owner: "所有者",
|
||||
ownerPlaceholder: "ロール名を入力...",
|
||||
ownerSearchPlaceholder: "ロール名を検索または入力...",
|
||||
|
||||
@@ -3684,6 +3684,11 @@ export default withEnglishFallback({
|
||||
editComment: "Editar comentário",
|
||||
commentPlaceholder: "Insira o comentário da coluna...",
|
||||
tableCommentPlaceholder: "Insira o comentário da tabela...",
|
||||
mysqlTableEngine: "Mecanismo de armazenamento",
|
||||
mysqlTableEnginePlaceholder: "Selecionar um mecanismo de armazenamento",
|
||||
mysqlTableEngineSearchPlaceholder: "Pesquisar mecanismos de armazenamento",
|
||||
mysqlTableEngineEmpty: "Nenhum mecanismo de armazenamento disponível",
|
||||
mysqlTableEngineLoadFailed: "Falha ao carregar os mecanismos de armazenamento: {message}",
|
||||
owner: "Proprietário",
|
||||
ownerPlaceholder: "Insira o nome da função...",
|
||||
ownerSearchPlaceholder: "Pesquise ou insira uma função...",
|
||||
|
||||
@@ -3855,6 +3855,11 @@ export default withEnglishFallback({
|
||||
editComment: "编辑注释",
|
||||
commentPlaceholder: "输入字段注释...",
|
||||
tableCommentPlaceholder: "输入表注释...",
|
||||
mysqlTableEngine: "存储引擎",
|
||||
mysqlTableEnginePlaceholder: "选择存储引擎",
|
||||
mysqlTableEngineSearchPlaceholder: "搜索存储引擎",
|
||||
mysqlTableEngineEmpty: "没有可用的存储引擎",
|
||||
mysqlTableEngineLoadFailed: "加载存储引擎失败:{message}",
|
||||
owner: "所有者",
|
||||
ownerPlaceholder: "输入角色名...",
|
||||
ownerSearchPlaceholder: "搜索或输入角色名...",
|
||||
|
||||
@@ -3425,6 +3425,11 @@ export default withEnglishFallback({
|
||||
editComment: "編輯註解",
|
||||
commentPlaceholder: "輸入欄位註解……",
|
||||
tableCommentPlaceholder: "輸入資料表註解……",
|
||||
mysqlTableEngine: "儲存引擎",
|
||||
mysqlTableEnginePlaceholder: "選擇儲存引擎",
|
||||
mysqlTableEngineSearchPlaceholder: "搜尋儲存引擎",
|
||||
mysqlTableEngineEmpty: "沒有可用的儲存引擎",
|
||||
mysqlTableEngineLoadFailed: "載入儲存引擎失敗:{message}",
|
||||
owner: "擁有者",
|
||||
ownerPlaceholder: "輸入角色名稱……",
|
||||
ownerSearchPlaceholder: "搜尋或輸入角色名稱……",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import { mysqlTableEngineDraft, mysqlTableEngineSqlOption, mysqlTableEngineSql, parseMysqlTableEngineMetadata, refreshMysqlTableEngineDraft, supportsMysqlTableEngine } from "@/lib/table/mysqlTableEngine";
|
||||
|
||||
function result(columns: string[], rows: QueryResult["rows"]): QueryResult {
|
||||
return { columns, rows, affected_rows: 0, execution_time_ms: 1 };
|
||||
}
|
||||
|
||||
describe("mysqlTableEngine", () => {
|
||||
it("builds mode-independent metadata SQL for quoted and Unicode names", () => {
|
||||
const sql = mysqlTableEngineSql("db'x", "订单\\archive");
|
||||
expect(sql).toContain("TABLE_SCHEMA = CONVERT(X'64622778' USING utf8mb4)");
|
||||
expect(sql).toContain("TABLE_NAME = CONVERT(X'e8aea2e58d955c61726368697665' USING utf8mb4)");
|
||||
expect(sql).not.toContain("db'x");
|
||||
});
|
||||
|
||||
it("keeps supported engines, the server default, and the current table engine", () => {
|
||||
const engines = result(
|
||||
["Engine", "Support", "Comment"],
|
||||
[
|
||||
["InnoDB", "DEFAULT", ""],
|
||||
["MyISAM", "YES", ""],
|
||||
["FEDERATED", "NO", ""],
|
||||
],
|
||||
);
|
||||
const table = result(["engine"], [["FEDERATED"]]);
|
||||
|
||||
expect(parseMysqlTableEngineMetadata(engines, table)).toEqual({
|
||||
currentEngine: "FEDERATED",
|
||||
defaultEngine: "InnoDB",
|
||||
engines: ["FEDERATED", "InnoDB", "MyISAM"],
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default for a new table and preserves a dirty edit across refresh", () => {
|
||||
const initial = { currentEngine: "MyISAM", defaultEngine: "InnoDB", engines: ["InnoDB", "MyISAM"] };
|
||||
expect(mysqlTableEngineDraft(initial, true)).toEqual({ value: "InnoDB", originalValue: "InnoDB" });
|
||||
expect(mysqlTableEngineDraft(initial, false)).toEqual({ value: "MyISAM", originalValue: "MyISAM" });
|
||||
|
||||
expect(refreshMysqlTableEngineDraft({ ...initial, currentEngine: "InnoDB" }, { value: "ARCHIVE", originalValue: "MyISAM" }, false, true)).toEqual({
|
||||
value: "ARCHIVE",
|
||||
originalValue: "InnoDB",
|
||||
});
|
||||
});
|
||||
|
||||
it("sends the selected engine for creates and only changed engines for edits", () => {
|
||||
expect(mysqlTableEngineSqlOption({ value: "InnoDB", originalValue: "InnoDB" }, true, true)).toBe("InnoDB");
|
||||
expect(mysqlTableEngineSqlOption({ value: "innodb", originalValue: "InnoDB" }, false, true)).toBeUndefined();
|
||||
expect(mysqlTableEngineSqlOption({ value: "MyISAM", originalValue: "InnoDB" }, false, true)).toBe("MyISAM");
|
||||
expect(mysqlTableEngineSqlOption({ value: "MyISAM", originalValue: "InnoDB" }, false, false)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("is limited to native MySQL connections", () => {
|
||||
expect(supportsMysqlTableEngine({ db_type: "mysql", driver_profile: "mysql" })).toBe(true);
|
||||
expect(supportsMysqlTableEngine({ db_type: "mysql", driver_profile: undefined })).toBe(true);
|
||||
expect(supportsMysqlTableEngine({ db_type: "jdbc", driver_profile: "mysql" })).toBe(false);
|
||||
expect(supportsMysqlTableEngine({ db_type: "mysql", driver_profile: "goldendb" })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import type { ConnectionConfig, QueryResult } from "@/types/database";
|
||||
|
||||
export interface MysqlTableEngineMetadata {
|
||||
currentEngine?: string;
|
||||
defaultEngine?: string;
|
||||
engines: string[];
|
||||
}
|
||||
|
||||
export interface MysqlTableEngineDraft {
|
||||
value: string;
|
||||
originalValue: string;
|
||||
}
|
||||
|
||||
export const MYSQL_STORAGE_ENGINES_SQL = "SHOW ENGINES";
|
||||
|
||||
function mysqlUtf8Literal(value: string): string {
|
||||
const bytes = new TextEncoder().encode(value);
|
||||
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return `CONVERT(X'${hex}' USING utf8mb4)`;
|
||||
}
|
||||
|
||||
export function mysqlTableEngineSql(database: string, table: string): string {
|
||||
return ["SELECT ENGINE AS engine", "FROM information_schema.TABLES", `WHERE TABLE_SCHEMA = ${mysqlUtf8Literal(database)}`, ` AND TABLE_NAME = ${mysqlUtf8Literal(table)}`, "LIMIT 1"].join("\n");
|
||||
}
|
||||
|
||||
function columnIndex(result: QueryResult, name: string): number {
|
||||
return result.columns.findIndex((column) => column.trim().toLowerCase() === name.toLowerCase());
|
||||
}
|
||||
|
||||
function cellText(result: QueryResult, rowIndex: number, index: number): string {
|
||||
if (index < 0) return "";
|
||||
const value = result.rows[rowIndex]?.[index];
|
||||
return value === null || value === undefined ? "" : String(value).trim();
|
||||
}
|
||||
|
||||
export function parseMysqlTableEngineMetadata(enginesResult: QueryResult, tableResult?: QueryResult): MysqlTableEngineMetadata {
|
||||
const engineIndex = columnIndex(enginesResult, "Engine");
|
||||
const supportIndex = columnIndex(enginesResult, "Support");
|
||||
const engines: string[] = [];
|
||||
let defaultEngine: string | undefined;
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (let rowIndex = 0; rowIndex < enginesResult.rows.length; rowIndex += 1) {
|
||||
const engine = cellText(enginesResult, rowIndex, engineIndex);
|
||||
const support = cellText(enginesResult, rowIndex, supportIndex).toUpperCase();
|
||||
if (!engine || (support !== "YES" && support !== "DEFAULT")) continue;
|
||||
const key = engine.toLowerCase();
|
||||
if (!seen.has(key)) {
|
||||
engines.push(engine);
|
||||
seen.add(key);
|
||||
}
|
||||
if (support === "DEFAULT") defaultEngine = engine;
|
||||
}
|
||||
|
||||
let currentEngine: string | undefined;
|
||||
if (tableResult) {
|
||||
currentEngine = cellText(tableResult, 0, columnIndex(tableResult, "engine")) || undefined;
|
||||
if (currentEngine && !seen.has(currentEngine.toLowerCase())) engines.unshift(currentEngine);
|
||||
}
|
||||
|
||||
return { currentEngine, defaultEngine, engines };
|
||||
}
|
||||
|
||||
export function mysqlTableEngineDraft(metadata: MysqlTableEngineMetadata, isCreateMode: boolean): MysqlTableEngineDraft {
|
||||
const current = (isCreateMode ? metadata.defaultEngine || metadata.engines[0] : metadata.currentEngine) || "";
|
||||
return { value: current, originalValue: current };
|
||||
}
|
||||
|
||||
export function refreshMysqlTableEngineDraft(metadata: MysqlTableEngineMetadata, current: MysqlTableEngineDraft, isCreateMode: boolean, preserveDraft: boolean): MysqlTableEngineDraft {
|
||||
const server = mysqlTableEngineDraft(metadata, isCreateMode);
|
||||
if (!preserveDraft || current.value === current.originalValue) return server;
|
||||
return { value: current.value, originalValue: server.originalValue };
|
||||
}
|
||||
|
||||
export function mysqlTableEngineSqlOption(draft: MysqlTableEngineDraft, isCreateMode: boolean, supported: boolean): string | undefined {
|
||||
const value = draft.value.trim();
|
||||
if (!supported || !value) return undefined;
|
||||
if (!isCreateMode && value.toLowerCase() === draft.originalValue.trim().toLowerCase()) return undefined;
|
||||
return value;
|
||||
}
|
||||
|
||||
export function supportsMysqlTableEngine(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";
|
||||
}
|
||||
@@ -83,6 +83,7 @@ export interface BuildTableStructureChangeSqlOptions {
|
||||
triggers?: EditableStructureTrigger[];
|
||||
tableComment?: string;
|
||||
originalTableComment?: string;
|
||||
mysqlEngine?: string;
|
||||
/** The target table is a PostgreSQL partitioned parent (`relkind = 'p'`);
|
||||
* the backend rejects `CREATE INDEX CONCURRENTLY` on such tables (fail
|
||||
* closed) instead of downgrading to a blocking `CREATE INDEX`. */
|
||||
|
||||
@@ -1022,6 +1022,8 @@ export interface TableStructureEditorDraft {
|
||||
originalTableComment: string;
|
||||
mysqlAutoIncrementValue?: string;
|
||||
originalMysqlAutoIncrementValue?: string;
|
||||
mysqlTableEngine?: string;
|
||||
originalMysqlTableEngine?: string;
|
||||
tableOwner?: string;
|
||||
originalTableOwner?: string;
|
||||
columns: import("@/lib/table/tableStructureEditorSql").EditableStructureColumn[];
|
||||
|
||||
Reference in new issue
Block a user