feat(mysql): edit column charset and collation
This commit is contained in:
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 -->
|
||||
|
||||
Reference in new issue
Block a user