fix(mysql): show inherited charset and collation in structure editor

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-04 09:48:41 +08:00
1 parent 48410b70b4
commit 06dcd0013c
16 files changed
+562 -91

No files matched your search

@@ -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`. */