diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index f2e399ecf0..0ae0e7f14b 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -21,12 +21,24 @@ import { useToast } from "@/composables/useToast"; import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sqlHighlighter"; import { copyToClipboard } from "@/lib/clipboard"; import { queryTimeoutSecsForConnection } from "@/lib/queryTimeout"; -import { type EditableStructureColumn, type EditableStructureIndex } from "@/lib/tableStructureEditorSql"; +import { type EditableStructureColumn, type EditableStructureForeignKey, type EditableStructureIndex, type EditableStructureTrigger } from "@/lib/tableStructureEditorSql"; import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities"; import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities"; import { connectionObjectTreeQuerySchema, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect"; -import { buildStructureTargetLabel, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, generateIndexName, generateUniqueIndexName, getDataTypeOptions, getDefaultLengthForType, splitDataType, toColumnNames } from "@/lib/tableStructureEditorState"; -import type { ForeignKeyInfo, TriggerInfo } from "@/types/database"; +import { + buildStructureTargetLabel, + combineDataTypeForDatabase, + createColumnDrafts, + createForeignKeyDrafts, + createIndexDrafts, + createTriggerDrafts, + generateIndexName, + generateUniqueIndexName, + getDataTypeOptions, + getDefaultLengthForType, + splitDataType, + toColumnNames, +} from "@/lib/tableStructureEditorState"; import * as api from "@/lib/api"; const { t } = useI18n(); @@ -90,8 +102,8 @@ const columns = ref([]); const indexes = ref([]); const pendingStatements = ref([]); const warnings = ref([]); -const foreignKeys = ref([]); -const triggers = ref([]); +const foreignKeys = ref([]); +const triggers = ref([]); function isPlainModShortcut(event: KeyboardEvent, key: string): boolean { if (event.isComposing || event.altKey || event.shiftKey) return false; @@ -307,6 +319,9 @@ const colLabels = computed(() => { return labels; }); const indexColLabels = computed(() => [t("structureEditor.indexName"), t("structureEditor.indexColumns"), t("structureEditor.unique"), t("structureEditor.indexType"), t("structureEditor.includedColumns"), t("structureEditor.filter"), t("structureEditor.comment"), t("structureEditor.actions")]); +const foreignKeyActionOptions = ["", "CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]; +const triggerTimingOptions = ["BEFORE", "AFTER"]; +const triggerEventOptions = ["INSERT", "UPDATE", "DELETE"]; const metadataSchema = computed(() => connectionObjectTreeQuerySchema(connection.value, props.database, props.schema)); const refreshVersion = computed(() => (props.connectionId && props.tableName ? queryStore.tableStructureRefreshVersion(props.connectionId, props.database, props.schema, props.tableName) : 0)); const isCreateMode = computed(() => !props.tableName); @@ -327,6 +342,8 @@ async function refreshSqlPreview() { tableName: isCreateMode.value ? newTableName.value : props.tableName || "", columns: columns.value, indexes: indexes.value, + foreignKeys: foreignKeys.value, + triggers: triggers.value, tableComment: tableComment.value, originalTableComment: isCreateMode.value ? undefined : originalTableComment.value, }; @@ -379,8 +396,8 @@ async function loadStructure(silent = false) { ]); columns.value = createColumnDrafts(nextColumns, databaseType.value); indexes.value = createIndexDrafts(nextIndexes); - foreignKeys.value = nextForeignKeys; - triggers.value = nextTriggers; + foreignKeys.value = createForeignKeyDrafts(nextForeignKeys); + triggers.value = createTriggerDrafts(nextTriggers); try { const tables = await api.listTables(props.connectionId, props.database, metadataSchema.value); const table = tables.find((t) => t.name.toLowerCase() === props.tableName!.toLowerCase() && t.table_type !== "VIEW"); @@ -583,6 +600,80 @@ function canDropIndex(index: EditableStructureIndex): boolean { return !!index.original && !index.isPrimary && structureCapabilities.value.dropIndex; } +const canEditMysqlForeignKeys = computed(() => structureDialect.value === "mysql"); +const canEditMysqlTriggers = computed(() => structureDialect.value === "mysql"); + +function generatedForeignKeyName(column = ""): string { + const table = structureIndexTableName() || "table"; + const suffix = column || "column"; + const base = `fk_${table}_${suffix}` + .replace(/[^a-zA-Z0-9_]+/g, "_") + .replace(/_+/g, "_") + .replace(/^_+|_+$/g, ""); + const taken = new Set(foreignKeys.value.map((item) => item.name.trim().toLowerCase()).filter(Boolean)); + if (!taken.has(base.toLowerCase())) return base; + for (let counter = 2; counter < 10_000; counter++) { + const candidate = `${base}_${counter}`; + if (!taken.has(candidate.toLowerCase())) return candidate; + } + return base; +} + +function addForeignKey() { + if (!canEditMysqlForeignKeys.value) return; + activeTab.value = "foreignKeys"; + foreignKeys.value.push({ + id: `new:${uuid()}`, + name: generatedForeignKeyName(), + column: "", + refSchema: "", + refTable: "", + refColumn: "", + onUpdate: "", + onDelete: "", + markedForDrop: false, + }); +} + +function removeNewForeignKey(foreignKey: EditableStructureForeignKey) { + foreignKeys.value = foreignKeys.value.filter((item) => item.id !== foreignKey.id); +} + +function toggleDropForeignKey(foreignKey: EditableStructureForeignKey) { + if (!foreignKey.original) return; + foreignKey.markedForDrop = !foreignKey.markedForDrop; +} + +function canEditForeignKeyDraft(foreignKey: EditableStructureForeignKey): boolean { + return canEditMysqlForeignKeys.value && !foreignKey.markedForDrop; +} + +function addTrigger() { + if (!canEditMysqlTriggers.value) return; + activeTab.value = "triggers"; + triggers.value.push({ + id: `new:${uuid()}`, + name: "", + timing: "BEFORE", + event: "INSERT", + statement: "BEGIN\n \nEND", + markedForDrop: false, + }); +} + +function removeNewTrigger(trigger: EditableStructureTrigger) { + triggers.value = triggers.value.filter((item) => item.id !== trigger.id); +} + +function toggleDropTrigger(trigger: EditableStructureTrigger) { + if (!trigger.original) return; + trigger.markedForDrop = !trigger.markedForDrop; +} + +function canEditTriggerDraft(trigger: EditableStructureTrigger): boolean { + return canEditMysqlTriggers.value && !trigger.markedForDrop; +} + function primarySqlOperation(sql: string): string { const statement = sql .split(";") @@ -657,6 +748,14 @@ function addItemForActiveTab(): boolean { addIndex(); return true; } + if (activeTab.value === "foreignKeys" && canEditMysqlForeignKeys.value) { + addForeignKey(); + return true; + } + if (activeTab.value === "triggers" && canEditMysqlTriggers.value) { + addTrigger(); + return true; + } return false; } @@ -721,7 +820,7 @@ watch(tableMetadataCapabilities, (capabilities) => { }); watch( - [isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes], + [isCreateMode, databaseType, () => props.schema, () => props.tableName, newTableName, tableComment, columns, indexes, foreignKeys, triggers], () => { void refreshSqlPreview(); }, @@ -812,6 +911,14 @@ watch(activeTab, (tab) => { {{ t("structureEditor.addIndex") }} + + @@ -1138,9 +1245,43 @@ watch(activeTab, (tab) => { {{ t("structureEditor.emptyReadonly") }}
-
-
{{ fk.name }}
-
{{ fk.column }} -> {{ fk.ref_table }}.{{ fk.ref_column }}
+
+
+ + + + + +
+ + +
+
+
+ + +
{{ fk.column }} -> {{ fk.refSchema ? `${fk.refSchema}.` : "" }}{{ fk.refTable }}.{{ fk.refColumn }}
+
@@ -1150,9 +1291,42 @@ watch(activeTab, (tab) => { {{ t("structureEditor.emptyReadonly") }}
-
-
{{ trigger.name }}
-
{{ trigger.timing }} {{ trigger.event }}
+
+
+ + + +
+ + +
+
+