feat(mysql): edit column charset and collation

This commit is contained in:
DengQingNian authored and GitHub committed 2026-07-10 15:13:57 +08:00
1 parent 0674433eea
commit e429c8945b
38 files changed
+524 -20

No files matched your search

@@ -30,7 +30,9 @@ import { canAddTableStructureColumn, getTableStructureCapabilities } from "@/lib
import { connectionObjectTreeQuerySchema, tableStructureDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import type { TableInfoTab, TableStructureEditorDraft, TableStructureEditorTarget, TableStructureEditorViewport } from "@/types/database";
import {
applyManticoreDdlColumnExtras,
buildStructureTargetLabel,
canEditManticoreColumnProperties,
combineDataTypeForDatabase,
createColumnDrafts,
createForeignKeyDrafts,
@@ -43,15 +45,16 @@ import {
getDataTypeOptions,
getDefaultLengthForType,
isDataTypeLengthDisabled,
isSqlServerIdentityCompatibleDataType,
isMysqlCharacterDataType,
isProtectedManticoreIdColumn,
isSqlServerIdentityCompatibleDataType,
parseExtraToColumnExtra,
rehydrateColumnDraftsFromMetadata,
splitDataType,
toColumnNames,
applyManticoreDdlColumnExtras,
canEditManticoreColumnProperties,
} from "@/lib/table/tableStructureEditorState";
import { CREATE_DATABASE_CHARSET_OPTIONS, createDatabaseCollationOptionsForCharset, fallbackCreateDatabaseCharsetMetadata, normalizeCreateDatabaseCharsetKey, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
import type { CreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
import * as api from "@/lib/backend/api";
const { t } = useI18n();
@@ -174,6 +177,8 @@ function columnChanged(column: EditableStructureColumn, index: number): boolean
!sameText(column.defaultValue, original.column_default) ||
!sameText(column.comment, original.comment) ||
column.isPrimaryKey !== original.is_primary_key ||
!sameText(column.characterSet, original.character_set) ||
!sameText(column.collation, original.collation) ||
JSON.stringify(column.extra) !== JSON.stringify(parseExtraToColumnExtra(original.extra, databaseType.value))
);
}
@@ -232,7 +237,7 @@ const structureDensityValues: StructureEditorDensity[] = ["compact", "standard",
const STRUCTURE_COLUMNS_WIDTHS_STORAGE_KEY = "dbx-structure-editor-column-widths";
const STRUCTURE_INDEX_COLUMNS_WIDTHS_STORAGE_KEY = "dbx-structure-editor-index-column-widths";
const STRUCTURE_SQL_PREVIEW_COLLAPSED_STORAGE_KEY = "dbx-structure-editor-sql-preview-collapsed";
const STRUCTURE_COLUMN_WIDTH_COUNT = 10;
const STRUCTURE_COLUMN_WIDTH_COUNT = 12;
const STRUCTURE_INDEX_COLUMN_WIDTH_COUNT = 8;
const PERSISTED_STRUCTURE_INDEX_COLUMN_WIDTHS = new Set([0, 1, 6]);
const structureDensityMetrics: Record<
@@ -256,7 +261,7 @@ const structureDensityMetrics: Record<
}
> = {
compact: {
columns: [28, 168, 136, 82, 60, 52, 108, 220, 144, 108],
columns: [28, 168, 136, 82, 60, 52, 108, 220, 80, 120, 144, 108],
indexes: [120, 180, 60, 88, 124, 144, 120, 70],
minColumnWidth: 24,
minIndexColumnWidth: 48,
@@ -273,7 +278,7 @@ const structureDensityMetrics: Record<
lineHeight: 1.35,
},
standard: {
columns: [32, 200, 160, 104, 72, 64, 128, 260, 160, 136],
columns: [32, 200, 160, 104, 72, 64, 128, 260, 90, 140, 160, 136],
indexes: [148, 224, 72, 108, 148, 180, 148, 84],
minColumnWidth: 28,
minIndexColumnWidth: 60,
@@ -290,7 +295,7 @@ const structureDensityMetrics: Record<
lineHeight: 1.4,
},
comfortable: {
columns: [36, 232, 188, 116, 84, 76, 152, 300, 188, 148],
columns: [36, 232, 188, 116, 84, 76, 152, 300, 100, 160, 188, 148],
indexes: [176, 260, 84, 124, 176, 216, 176, 104],
minColumnWidth: 32,
minIndexColumnWidth: 64,
@@ -317,10 +322,17 @@ function metricsForDensity(density: StructureEditorDensity) {
}
function normalizeStructureColumnWidths(value: unknown, density: StructureEditorDensity): number[] | null {
if (!Array.isArray(value) || value.length !== STRUCTURE_COLUMN_WIDTH_COUNT) return null;
const minWidth = metricsForDensity(density).minColumnWidth;
const widths = value.map((item) => Number(item));
if (!Array.isArray(value)) return null;
let widths = value.map((item) => Number(item));
if (widths.some((item) => !Number.isFinite(item))) return null;
// Backward compatibility: pad old 11-column persisted layout to 12 by inserting
// a default collation width at index 9.
if (widths.length === STRUCTURE_COLUMN_WIDTH_COUNT - 1) {
const defaultWidths = metricsForDensity(density).columns;
widths = [...widths.slice(0, 9), defaultWidths[9], ...widths.slice(9)];
}
if (widths.length !== STRUCTURE_COLUMN_WIDTH_COUNT) return null;
const minWidth = metricsForDensity(density).minColumnWidth;
return widths.map((item) => Math.max(minWidth, item));
}
@@ -651,7 +663,56 @@ const showExtendedProperties = computed(() => {
const dt = databaseType.value;
return dt === "mysql" || dt === "manticoresearch" || isPostgresIdentityType(dt) || dt === "sqlserver";
});
const extendedPropertiesColumnIndex = 8;
const showCharacterSet = computed(() => structureDialect.value === "mysql");
const serverCharsetMetadata = ref<CreateDatabaseCharsetMetadata>();
const charsetMetadataLoading = ref(false);
const mysqlCharsetOptions = computed<string[]>(() => {
const meta = serverCharsetMetadata.value;
return meta ? meta.charsets : ([...CREATE_DATABASE_CHARSET_OPTIONS] as string[]);
});
function collationOptionsForCharset(charset: string): string[] {
const meta = serverCharsetMetadata.value;
if (meta) {
return meta.collationsByCharset[normalizeCreateDatabaseCharsetKey(charset)] ?? [];
}
return createDatabaseCollationOptionsForCharset(charset);
}
async function loadCharsetMetadata() {
if (charsetMetadataLoading.value || !showCharacterSet.value) return;
charsetMetadataLoading.value = true;
try {
await store.ensureConnected(props.connectionId);
const [charsetResult, collationResult] = await Promise.all([api.executeQuery(props.connectionId, props.database, "SHOW CHARACTER SET"), api.executeQuery(props.connectionId, props.database, "SHOW COLLATION")]);
serverCharsetMetadata.value = parseCreateDatabaseCharsetMetadata(charsetResult, collationResult);
} catch {
serverCharsetMetadata.value = fallbackCreateDatabaseCharsetMetadata();
} finally {
charsetMetadataLoading.value = false;
}
}
function onCharsetChange(column: EditableStructureColumn, charset: string) {
column.characterSet = charset;
// If the collation is no longer valid for the new charset, clear it so the
// server picks its default (COLLATE is only emitted when explicitly chosen).
if (column.collation && !collationOptionsForCharset(charset).includes(column.collation)) {
column.collation = "";
}
}
function columnCharset(column: EditableStructureColumn): string {
return column.characterSet ?? "";
}
function columnCollation(column: EditableStructureColumn): string {
return column.collation ?? "";
}
const extendedPropertiesColumnIndex = 10;
const actionButtonGap = 2;
const columnActionButtonCount = computed(() => (canShowColumnDragControls.value ? 2 : 1));
const columnActionsWidth = computed(() => {
@@ -677,10 +738,12 @@ const colLabels = computed(() => {
if (columnEditorControls.value.primaryKey) labels.push({ key: "primaryKey", label: t("structureEditor.primaryKey"), widthIndex: 5 });
if (columnEditorControls.value.defaultValue) labels.push({ key: "defaultValue", label: t("structureEditor.defaultValue"), widthIndex: 6 });
if (columnEditorControls.value.comment) labels.push({ key: "comment", label: t("structureEditor.comment"), widthIndex: 7 });
if (showCharacterSet.value) labels.push({ key: "characterSet", label: t("structureEditor.characterSet"), widthIndex: 8 });
if (showCharacterSet.value) labels.push({ key: "collation", label: t("structureEditor.collation"), widthIndex: 9 });
if (showExtendedProperties.value) {
labels.push({ key: "extendedProperties", label: t("structureEditor.extendedProperties"), widthIndex: extendedPropertiesColumnIndex });
}
labels.push({ key: "actions", label: t("structureEditor.actions"), widthIndex: 9 });
labels.push({ key: "actions", label: t("structureEditor.actions"), widthIndex: 11 });
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")]);
@@ -1117,6 +1180,9 @@ async function loadStructure(silent = false, scope: StructureRefreshScope = FULL
/* ignore — Manticore column properties can still come from SHOW COLUMNS when available */
}
}
// Load live charset/collation metadata from the MySQL server so the column
// editor shows the correct options for the server version.
void loadCharsetMetadata();
columns.value = createColumnDrafts(nextColumns, databaseType.value);
}
@@ -1185,6 +1251,8 @@ async function addColumn() {
defaultValue: "",
comment: "",
isPrimaryKey: false,
characterSet: "",
collation: "",
extra: {},
markedForDrop: false,
};
@@ -1311,6 +1379,11 @@ function updateSqlServerIdentityIncrement(column: EditableStructureColumn, value
function updateColumnDataType(column: EditableStructureColumn, baseType: string) {
column.dataType = combineDataTypeForDatabase(databaseType.value, baseType, getDefaultLengthForType(databaseType.value, baseType));
syncSqlServerIdentityForDataType(column);
// Clear charset/collation when switching to a non-character MySQL type
if (showCharacterSet.value && !isMysqlCharacterDataType(column.dataType)) {
column.characterSet = "";
column.collation = "";
}
}
function updateColumnDataTypeLength(column: EditableStructureColumn, value: string | number) {
@@ -1630,6 +1703,12 @@ function isColumnCommentDisabled(column: EditableStructureColumn): boolean {
return column.markedForDrop || !structureCapabilities.value.comment;
}
function isColumnCharsetDisabled(column: EditableStructureColumn): boolean {
if (column.markedForDrop) return true;
if (!showCharacterSet.value) return true;
return !isMysqlCharacterDataType(column.dataType);
}
function isPrimaryKeyDisabled(column: EditableStructureColumn): boolean {
if (column.markedForDrop) return true;
if (!column.original) return false;
@@ -2372,6 +2451,32 @@ watch(activeTab, (tab) => {
</Popover>
</div>
</td>
<td v-if="showCharacterSet" :class="structureCellClass">
<SearchableSelect
:model-value="columnCharset(column)"
:options="mysqlCharsetOptions"
:placeholder="t('structureEditor.charsetPlaceholder')"
:search-placeholder="t('structureEditor.charsetPlaceholder')"
:empty-text="t('structureEditor.noMatchingType')"
:allow-custom="true"
:disabled="isColumnCharsetDisabled(column)"
:trigger-class="[structureMonoControlClass, 'w-20']"
@update:model-value="(v: string) => onCharsetChange(column, v)"
/>
</td>
<td v-if="showCharacterSet" :class="structureCellClass">
<SearchableSelect
:model-value="columnCollation(column)"
:options="collationOptionsForCharset(columnCharset(column))"
:placeholder="t('structureEditor.collationPlaceholder')"
:search-placeholder="t('structureEditor.collationPlaceholder')"
:empty-text="t('structureEditor.noMatchingType')"
:allow-custom="true"
:disabled="isColumnCharsetDisabled(column)"
:trigger-class="[structureMonoControlClass, 'w-28']"
@update:model-value="(v: string) => (column.collation = v)"
/>
</td>
<td v-if="showExtendedProperties" :class="structureCellClass">
<div :class="structurePropertyListClass">
<!-- Manticore Search: character data type properties -->