fix(mysql): show inherited charset and collation in structure editor
This commit is contained in:
1 parent
48410b70b4
commit
06dcd0013c
16 files changed
+562
-91
No files matched your search
@@ -37,6 +37,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 { mysqlTableCollationSql, parseMysqlTableCollation } from "@/lib/table/mysqlTableCollation";
|
||||
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";
|
||||
@@ -928,6 +929,9 @@ const showCharacterSet = computed(() => structureDialect.value === "mysql");
|
||||
|
||||
const serverCharsetMetadata = ref<CreateDatabaseCharsetMetadata>();
|
||||
const charsetMetadataLoading = ref(false);
|
||||
// The table's own default collation, used only to keep inherited charsets out of the
|
||||
// generated DDL. Columns keep the real values MySQL reports so the pickers stay filled.
|
||||
const mysqlTableDefaultCollation = ref("");
|
||||
|
||||
const mysqlCharsetOptions = computed<string[]>(() => {
|
||||
const meta = serverCharsetMetadata.value;
|
||||
@@ -956,6 +960,22 @@ async function loadCharsetMetadata() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMysqlTableDefaultCollation() {
|
||||
if (!showCharacterSet.value || isCreateMode.value || !props.connectionId || !props.database || !props.tableName) {
|
||||
mysqlTableDefaultCollation.value = "";
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await store.ensureConnected(props.connectionId);
|
||||
const result = await api.executeQuery(props.connectionId, props.database, mysqlTableCollationSql(props.database, props.tableName), undefined, undefined, { maxRows: 1 });
|
||||
mysqlTableDefaultCollation.value = parseMysqlTableCollation(result);
|
||||
} catch {
|
||||
// Optional metadata: without it the DDL just spells out the charset the column
|
||||
// already has, which is equivalent SQL — never block the editor on this lookup.
|
||||
mysqlTableDefaultCollation.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function onCharsetChange(column: EditableStructureColumn, charset: string) {
|
||||
column.characterSet = charset;
|
||||
// If the collation is no longer valid for the new charset, clear it so the
|
||||
@@ -1614,6 +1634,7 @@ function structureChangeOptions(): BuildTableStructureChangeSqlOptions {
|
||||
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),
|
||||
tableCollation: mysqlTableDefaultCollation.value || undefined,
|
||||
partitioned: isPartitionedParent.value,
|
||||
isGaussdbMMode: connection.value?.driver_profile?.toLowerCase() === "gaussdb-m",
|
||||
};
|
||||
@@ -1772,6 +1793,7 @@ function resetState() {
|
||||
mysqlTableEngineLoadRequestId += 1;
|
||||
mysqlTableEngineLoading.value = false;
|
||||
mysqlTableEngineLoadError.value = "";
|
||||
mysqlTableDefaultCollation.value = "";
|
||||
tableOwner.value = "";
|
||||
originalTableOwner.value = "";
|
||||
tableOwnerLoadRequestId += 1;
|
||||
@@ -2040,6 +2062,7 @@ async function loadStructure(
|
||||
// Load live charset/collation metadata from the MySQL server so the column
|
||||
// editor shows the correct options for the server version.
|
||||
void loadCharsetMetadata();
|
||||
void loadMysqlTableDefaultCollation();
|
||||
const nextColumnDrafts = createColumnDrafts(nextColumns, databaseType.value);
|
||||
const hydratedColumnDrafts = supportsCharacterLengthUnits.value && options.characterLengthUnitsAfterSave ? restoreCharacterLengthUnitsAfterSave(databaseType.value, nextColumnDrafts, options.characterLengthUnitsAfterSave) : nextColumnDrafts;
|
||||
columns.value = applyStoredLocalColumnOrder(hydratedColumnDrafts);
|
||||
@@ -3862,6 +3885,7 @@ watch(
|
||||
originalMysqlTableEngine,
|
||||
mysqlTableEngineLoading,
|
||||
mysqlTableEngineLoadError,
|
||||
mysqlTableDefaultCollation,
|
||||
tableOwner,
|
||||
ddlDraft,
|
||||
columns,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import { mysqlTableCollationSql, parseMysqlTableCollation } from "@/lib/table/mysqlTableCollation";
|
||||
|
||||
function result(columns: string[], rows: QueryResult["rows"]): QueryResult {
|
||||
return { columns, rows, affected_rows: 0, execution_time_ms: 1 };
|
||||
}
|
||||
|
||||
describe("mysqlTableCollation", () => {
|
||||
it("builds mode-independent lookup SQL for quoted and Unicode names", () => {
|
||||
const sql = mysqlTableCollationSql("db'x", "订单\\archive");
|
||||
expect(sql).toContain("TABLE_COLLATION AS table_collation");
|
||||
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("reads the reported collation regardless of column-name casing", () => {
|
||||
expect(parseMysqlTableCollation(result(["TABLE_COLLATION"], [["utf8mb4_0900_ai_ci"]]))).toBe("utf8mb4_0900_ai_ci");
|
||||
expect(parseMysqlTableCollation(result(["table_collation"], [[" utf8mb4_bin "]]))).toBe("utf8mb4_bin");
|
||||
});
|
||||
|
||||
it("falls back to no table default when the server reports nothing usable", () => {
|
||||
// MySQL reports TABLE_COLLATION as NULL for views, and the lookup itself may fail.
|
||||
expect(parseMysqlTableCollation(result(["table_collation"], [[null]]))).toBe("");
|
||||
expect(parseMysqlTableCollation(result(["table_collation"], []))).toBe("");
|
||||
expect(parseMysqlTableCollation(result(["engine"], [["InnoDB"]]))).toBe("");
|
||||
expect(parseMysqlTableCollation(undefined)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import { mysqlUtf8Literal } from "@/lib/table/mysqlTableEngine";
|
||||
|
||||
/**
|
||||
* Reads the table's default collation. MySQL reports the *effective* collation on every
|
||||
* character column and never records whether it was written out explicitly, so a column
|
||||
* matching this value is one that simply inherits the table default. The structure editor
|
||||
* passes it to the SQL builder, which then leaves the redundant `CHARACTER SET`/`COLLATE`
|
||||
* clauses out of the generated DDL — the column metadata itself keeps the real values so
|
||||
* the charset and collation pickers can show what the column currently uses.
|
||||
*/
|
||||
export function mysqlTableCollationSql(database: string, table: string): string {
|
||||
return ["SELECT TABLE_COLLATION AS table_collation", "FROM information_schema.TABLES", `WHERE TABLE_SCHEMA = ${mysqlUtf8Literal(database)}`, ` AND TABLE_NAME = ${mysqlUtf8Literal(table)}`, "LIMIT 1"].join("\n");
|
||||
}
|
||||
|
||||
/** Empty string when the server reported nothing usable — MySQL returns NULL for views. */
|
||||
export function parseMysqlTableCollation(result: QueryResult | undefined): string {
|
||||
if (!result) return "";
|
||||
const index = result.columns.findIndex((column) => column.trim().toLowerCase() === "table_collation");
|
||||
if (index < 0) return "";
|
||||
const value = result.rows[0]?.[index];
|
||||
return value === null || value === undefined ? "" : String(value).trim();
|
||||
}
|
||||
@@ -13,7 +13,9 @@ export interface MysqlTableEngineDraft {
|
||||
|
||||
export const MYSQL_STORAGE_ENGINES_SQL = "SHOW ENGINES";
|
||||
|
||||
function mysqlUtf8Literal(value: string): string {
|
||||
/** Encodes a schema/table name as a byte literal so non-ASCII names survive whatever
|
||||
* charset the session happens to use. Shared with the table collation lookup. */
|
||||
export 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)`;
|
||||
|
||||
@@ -84,6 +84,10 @@ export interface BuildTableStructureChangeSqlOptions {
|
||||
tableComment?: string;
|
||||
originalTableComment?: string;
|
||||
mysqlEngine?: string;
|
||||
/** MySQL only: the table's current default collation. Columns whose collation merely
|
||||
* matches it inherit the table default, so the backend leaves their redundant
|
||||
* `CHARACTER SET`/`COLLATE` clauses out of the generated DDL. */
|
||||
tableCollation?: 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`. */
|
||||
|
||||
Reference in new issue
Block a user