From cf46aec15846d96bf5f99aaaf444e7a0012afdbf Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 19 May 2026 12:15:49 +0800 Subject: [PATCH] feat(structure): add DDL capability matrix --- .../structure/TableStructureEditorDialog.vue | 113 +++++++--- .../desktop/src/lib/databaseCapabilitySets.ts | 13 ++ .../desktop/src/lib/databaseFeatureSupport.ts | 4 +- .../src/lib/tableStructureCapabilities.ts | 202 ++++++++++++++++++ .../src/lib/tableStructureEditorSql.ts | 183 ++++++++++++---- .../app-tests/databaseCapabilities.test.ts | 5 + .../tableStructureCapabilities.test.ts | 75 +++++++ .../tableStructureEditorPresentation.test.ts | 15 ++ .../app-tests/tableStructureEditorSql.test.ts | 148 +++++++++++++ 9 files changed, 687 insertions(+), 71 deletions(-) create mode 100644 apps/desktop/src/lib/tableStructureCapabilities.ts create mode 100644 packages/app-tests/tableStructureCapabilities.test.ts diff --git a/apps/desktop/src/components/structure/TableStructureEditorDialog.vue b/apps/desktop/src/components/structure/TableStructureEditorDialog.vue index 4fa46371a..017a0f0a8 100644 --- a/apps/desktop/src/components/structure/TableStructureEditorDialog.vue +++ b/apps/desktop/src/components/structure/TableStructureEditorDialog.vue @@ -38,6 +38,7 @@ import { type EditableStructureColumn, type EditableStructureIndex, } from "@/lib/tableStructureEditorSql"; +import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities"; import { createColumnDrafts, createIndexDrafts, toColumnNames } from "@/lib/tableStructureEditorState"; import type { ForeignKeyInfo, TriggerInfo } from "@/types/database"; import * as api from "@/lib/api"; @@ -89,6 +90,7 @@ function onIndexColResize(e: MouseEvent, col: number) { const connection = computed(() => (props.prefillConnectionId ? store.getConfig(props.prefillConnectionId) : undefined)); const databaseType = computed(() => connection.value?.db_type); +const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value)); const indexTypesByDb: Record = { postgres: ["BTREE", "HASH", "GIST", "SPGIST", "GIN", "BRIN"], @@ -97,7 +99,9 @@ const indexTypesByDb: Record = { oracle: ["NORMAL", "BITMAP", "FUNCTION-BASED NORMAL", "FUNCTION-BASED DOMAIN", "DOMAIN", "CLUSTER"], sqlite: ["BTREE"], }; -const indexTypeOptions = computed(() => indexTypesByDb[databaseType.value ?? ""] ?? []); +const indexTypeOptions = computed(() => + structureCapabilities.value.indexType ? (indexTypesByDb[databaseType.value ?? ""] ?? []) : [], +); const indexColLabels = computed(() => [ t("structureEditor.indexName"), @@ -195,6 +199,7 @@ async function loadStructure() { } function addColumn() { + if (!structureCapabilities.value.addColumn) return; columns.value.push({ id: `new:${uuid()}`, name: "", @@ -212,11 +217,38 @@ function removeNewColumn(column: EditableStructureColumn) { } function toggleDropColumn(column: EditableStructureColumn) { - if (!column.original || column.isPrimaryKey) return; + if (!canDropColumn(column)) return; column.markedForDrop = !column.markedForDrop; } +function isColumnNameDisabled(column: EditableStructureColumn): boolean { + return column.markedForDrop || (!!column.original && !structureCapabilities.value.renameColumn); +} + +function isColumnTypeDisabled(column: EditableStructureColumn): boolean { + return column.markedForDrop || (!!column.original && !structureCapabilities.value.alterType); +} + +function isColumnNullableDisabled(column: EditableStructureColumn): boolean { + return ( + column.markedForDrop || column.isPrimaryKey || (!!column.original && !structureCapabilities.value.alterNullability) + ); +} + +function isColumnDefaultDisabled(column: EditableStructureColumn): boolean { + return column.markedForDrop || (!!column.original && !structureCapabilities.value.alterDefault); +} + +function isColumnCommentDisabled(column: EditableStructureColumn): boolean { + return column.markedForDrop || !structureCapabilities.value.comment; +} + +function canDropColumn(column: EditableStructureColumn): boolean { + return !!column.original && !column.isPrimaryKey && structureCapabilities.value.dropColumn; +} + function addIndex() { + if (!structureCapabilities.value.createIndex) return; indexes.value.push({ id: `new:${uuid()}`, name: "", @@ -252,6 +284,7 @@ function toggleIndexColumn(index: EditableStructureIndex, col: string) { } function toggleIncludedColumn(index: EditableStructureIndex, col: string) { + if (!structureCapabilities.value.indexInclude) return; const i = index.includedColumns.indexOf(col); if (i >= 0) index.includedColumns.splice(i, 1); else index.includedColumns.push(col); @@ -262,10 +295,22 @@ function removeNewIndex(index: EditableStructureIndex) { } function toggleDropIndex(index: EditableStructureIndex) { - if (!index.original || index.isPrimary) return; + if (!canDropIndex(index)) return; index.markedForDrop = !index.markedForDrop; } +function canEditIndexDraft(index: EditableStructureIndex): boolean { + return !index.original && !index.markedForDrop && structureCapabilities.value.createIndex; +} + +function canEditIndexFilter(index: EditableStructureIndex): boolean { + return canEditIndexDraft(index) && structureCapabilities.value.indexFilter; +} + +function canDropIndex(index: EditableStructureIndex): boolean { + return !!index.original && !index.isPrimary && structureCapabilities.value.dropIndex; +} + async function applyChanges() { if (!canApply.value || !props.prefillConnectionId || !props.prefillDatabase) return; saving.value = true; @@ -346,11 +391,23 @@ watch(open, (value) => { {{ t("structureEditor.foreignKeys") }} {{ t("structureEditor.triggers") }} - - @@ -394,13 +451,17 @@ watch(open, (value) => { - + @@ -409,7 +470,7 @@ watch(open, (value) => { v-model="column.isNullable" type="checkbox" class="h-3.5 w-3.5" - :disabled="column.markedForDrop || column.isPrimaryKey" + :disabled="isColumnNullableDisabled(column)" /> {{ column.isNullable ? t("structureEditor.yes") : t("structureEditor.no") }} @@ -418,7 +479,7 @@ watch(open, (value) => { @@ -426,7 +487,7 @@ watch(open, (value) => { @@ -434,7 +495,7 @@ watch(open, (value) => { variant="ghost" size="icon" class="h-7 w-7 shrink-0" - :disabled="column.markedForDrop" + :disabled="isColumnCommentDisabled(column)" :aria-label="t('structureEditor.editComment')" :title="t('structureEditor.editComment')" > @@ -454,7 +515,7 @@ watch(open, (value) => { v-model="column.comment" class="min-h-36 w-full resize-y rounded-md border bg-background px-2.5 py-2 text-xs leading-5 outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50" :placeholder="t('structureEditor.commentPlaceholder')" - :disabled="column.markedForDrop" + :disabled="isColumnCommentDisabled(column)" /> @@ -466,7 +527,7 @@ watch(open, (value) => { variant="ghost" size="sm" class="h-7 gap-1" - :disabled="column.isPrimaryKey" + :disabled="!canDropColumn(column)" @click="toggleDropColumn(column)" > @@ -512,14 +573,10 @@ watch(open, (value) => { :class="index.markedForDrop ? 'bg-destructive/5 opacity-60' : ''" > - + - +