feat: add selectable database charset options
This commit is contained in:
1 parent
aed2192d8c
commit
a36d2f12f7
10 files changed
+317
-3
No files matched your search
@@ -122,8 +122,10 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import { flattenTree } from "@/composables/useFlatTree";
|
||||
import { createDatabaseCollationOptionsForCharset, fallbackCreateDatabaseCharsetMetadata, nextCreateDatabaseCollation, normalizeCreateDatabaseCharset, parseCreateDatabaseCharsetMetadata } from "@/lib/createDatabaseCharsetOptions";
|
||||
|
||||
const { t } = useI18n();
|
||||
const labelRef = ref<HTMLElement>();
|
||||
@@ -1357,6 +1359,10 @@ const showCreateDatabaseDialog = ref(false);
|
||||
const createDatabaseName = ref("");
|
||||
const createDatabaseCharset = ref("utf8mb4");
|
||||
const createDatabaseCollation = ref("utf8mb4_unicode_ci");
|
||||
const fallbackCreateDatabaseCharset = fallbackCreateDatabaseCharsetMetadata();
|
||||
const createDatabaseCharsetOptions = ref<string[]>(fallbackCreateDatabaseCharset.charsets);
|
||||
const createDatabaseCollationsByCharset = ref<Record<string, string[]>>(fallbackCreateDatabaseCharset.collationsByCharset);
|
||||
const createDatabaseCharsetLoading = ref(false);
|
||||
const showDropDatabaseConfirm = ref(false);
|
||||
const dropDatabaseLoading = ref(false);
|
||||
const showDropMongoCollectionConfirm = ref(false);
|
||||
@@ -1983,7 +1989,40 @@ function openCreateDatabaseDialog() {
|
||||
createDatabaseName.value = "";
|
||||
createDatabaseCharset.value = "utf8mb4";
|
||||
createDatabaseCollation.value = "utf8mb4_unicode_ci";
|
||||
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
|
||||
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
|
||||
showCreateDatabaseDialog.value = true;
|
||||
void loadCreateDatabaseCharsetMetadata();
|
||||
}
|
||||
|
||||
function updateCreateDatabaseCharset(value: string) {
|
||||
const previousCharset = createDatabaseCharset.value;
|
||||
createDatabaseCharset.value = value;
|
||||
createDatabaseCollation.value = nextCreateDatabaseCollation(value, previousCharset, createDatabaseCollation.value, createDatabaseCollationsByCharset.value);
|
||||
}
|
||||
|
||||
async function loadCreateDatabaseCharsetMetadata() {
|
||||
const node = props.node;
|
||||
if (!node.connectionId || createDatabaseCharsetLoading.value) return;
|
||||
createDatabaseCharsetLoading.value = true;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const [charsetResult, collationResult] = await Promise.all([api.executeQuery(node.connectionId, "", "SHOW CHARACTER SET", undefined, undefined, { maxRows: 1000 }), api.executeQuery(node.connectionId, "", "SHOW COLLATION", undefined, undefined, { maxRows: 10000 })]);
|
||||
if (!showCreateDatabaseDialog.value) return;
|
||||
const metadata = parseCreateDatabaseCharsetMetadata(charsetResult, collationResult);
|
||||
createDatabaseCharsetOptions.value = metadata.charsets;
|
||||
createDatabaseCollationsByCharset.value = metadata.collationsByCharset;
|
||||
if (!createDatabaseCharsetOptions.value.includes(createDatabaseCharset.value) && createDatabaseCharsetOptions.value.length) {
|
||||
updateCreateDatabaseCharset(createDatabaseCharsetOptions.value[0]);
|
||||
} else {
|
||||
createDatabaseCollation.value = nextCreateDatabaseCollation(createDatabaseCharset.value, createDatabaseCharset.value, createDatabaseCollation.value, createDatabaseCollationsByCharset.value);
|
||||
}
|
||||
} catch {
|
||||
createDatabaseCharsetOptions.value = fallbackCreateDatabaseCharset.charsets;
|
||||
createDatabaseCollationsByCharset.value = fallbackCreateDatabaseCharset.collationsByCharset;
|
||||
} finally {
|
||||
createDatabaseCharsetLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureDuckDbFileExtension(path: string): string {
|
||||
@@ -3904,11 +3943,46 @@ function treeItemMenuItems(): ContextMenuItem[] {
|
||||
<div v-if="canSetCreateDatabaseCharset" class="grid gap-2">
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCharset") }}</label>
|
||||
<Input v-model="createDatabaseCharset" :placeholder="t('contextMenu.createDatabaseCharsetPlaceholder')" @keydown.enter.prevent="confirmCreateDatabase" />
|
||||
<SearchableSelect
|
||||
:model-value="createDatabaseCharset"
|
||||
:options="createDatabaseCharsetOptions"
|
||||
:placeholder="t('contextMenu.createDatabaseCharsetPlaceholder')"
|
||||
:search-placeholder="t('contextMenu.createDatabaseCharsetSearchPlaceholder')"
|
||||
:empty-text="t('contextMenu.createDatabaseCharsetEmpty')"
|
||||
:loading-text="t('contextMenu.createDatabaseCharsetLoading')"
|
||||
:loading="createDatabaseCharsetLoading"
|
||||
:normalize-custom="normalizeCreateDatabaseCharset"
|
||||
allow-custom
|
||||
trigger-variant="outline"
|
||||
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
|
||||
content-class="w-[var(--reka-popover-trigger-width)]"
|
||||
@update:model-value="updateCreateDatabaseCharset"
|
||||
>
|
||||
<template #custom-option-label="{ value }">
|
||||
<span class="truncate">{{ t("contextMenu.createDatabaseCharsetCustomOption", { value }) }}</span>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
<div class="grid gap-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("contextMenu.createDatabaseCollation") }}</label>
|
||||
<Input v-model="createDatabaseCollation" :placeholder="t('contextMenu.createDatabaseCollationPlaceholder')" @keydown.enter.prevent="confirmCreateDatabase" />
|
||||
<SearchableSelect
|
||||
v-model="createDatabaseCollation"
|
||||
:options="createDatabaseCollationOptionsForCharset(createDatabaseCharset, createDatabaseCollationsByCharset)"
|
||||
:placeholder="t('contextMenu.createDatabaseCollationPlaceholder')"
|
||||
:search-placeholder="t('contextMenu.createDatabaseCollationSearchPlaceholder')"
|
||||
:empty-text="t('contextMenu.createDatabaseCollationEmpty')"
|
||||
:loading-text="t('contextMenu.createDatabaseCollationLoading')"
|
||||
:loading="createDatabaseCharsetLoading"
|
||||
:normalize-custom="normalizeCreateDatabaseCharset"
|
||||
allow-custom
|
||||
trigger-variant="outline"
|
||||
trigger-class="h-9 w-full max-w-none justify-between border bg-background px-3 text-sm shadow-xs hover:bg-accent"
|
||||
content-class="w-[var(--reka-popover-trigger-width)]"
|
||||
>
|
||||
<template #custom-option-label="{ value }">
|
||||
<span class="truncate">{{ t("contextMenu.createDatabaseCollationCustomOption", { value }) }}</span>
|
||||
</template>
|
||||
</SearchableSelect>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, nextTick, ref, watch } from "vue";
|
||||
import type { HTMLAttributes } from "vue";
|
||||
import { Check, ChevronDown, Search } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { ButtonVariants } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { filterDatabaseOptions } from "@/lib/databaseOptionSearch";
|
||||
@@ -18,6 +19,7 @@ const props = withDefaults(
|
||||
loadingText: string;
|
||||
loading?: boolean;
|
||||
allowCustom?: boolean;
|
||||
triggerVariant?: ButtonVariants["variant"];
|
||||
triggerClass?: HTMLAttributes["class"];
|
||||
contentClass?: HTMLAttributes["class"];
|
||||
itemClass?: HTMLAttributes["class"];
|
||||
@@ -27,6 +29,7 @@ const props = withDefaults(
|
||||
{
|
||||
loading: false,
|
||||
allowCustom: false,
|
||||
triggerVariant: "ghost",
|
||||
displayName: (option: string) => option,
|
||||
normalizeCustom: (value: string) => value,
|
||||
},
|
||||
@@ -146,7 +149,7 @@ function handleKeydown(event: KeyboardEvent) {
|
||||
<template>
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button type="button" variant="ghost" :title="selectedLabel" :class="cn('h-6 w-auto max-w-56 min-w-0 justify-between gap-1 border-0 bg-transparent px-1 text-xs font-normal shadow-none hover:bg-muted/50 focus-visible:ring-0', triggerClass)">
|
||||
<Button type="button" :variant="triggerVariant" :title="selectedLabel" :class="cn('h-6 w-auto max-w-56 min-w-0 justify-between gap-1 border-0 bg-transparent px-1 text-xs font-normal shadow-none hover:bg-muted/50 focus-visible:ring-0', triggerClass)">
|
||||
<slot name="trigger-label" :value="modelValue" :label="selectedLabel" :loading="loading">
|
||||
<span class="truncate">{{ loading ? loadingText : selectedLabel }}</span>
|
||||
</slot>
|
||||
|
||||
@@ -1270,8 +1270,16 @@ export default {
|
||||
createDatabaseNamePlaceholder: "Database name",
|
||||
createDatabaseCharset: "Character set",
|
||||
createDatabaseCharsetPlaceholder: "e.g. utf8mb4",
|
||||
createDatabaseCharsetSearchPlaceholder: "Search or type a character set",
|
||||
createDatabaseCharsetEmpty: "No character sets found",
|
||||
createDatabaseCharsetLoading: "Loading character sets...",
|
||||
createDatabaseCharsetCustomOption: 'Use "{value}"',
|
||||
createDatabaseCollation: "Collation",
|
||||
createDatabaseCollationPlaceholder: "e.g. utf8mb4_unicode_ci",
|
||||
createDatabaseCollationSearchPlaceholder: "Search or type a collation",
|
||||
createDatabaseCollationEmpty: "No collations found",
|
||||
createDatabaseCollationLoading: "Loading collations...",
|
||||
createDatabaseCollationCustomOption: 'Use "{value}"',
|
||||
createSchema: "Create Schema",
|
||||
dropSchema: "Drop Schema",
|
||||
confirmDropSchemaTitle: "Drop Schema",
|
||||
|
||||
@@ -1207,8 +1207,16 @@ export default {
|
||||
createDatabaseNamePlaceholder: "Nome database",
|
||||
createDatabaseCharset: "Set di caratteri",
|
||||
createDatabaseCharsetPlaceholder: "es. utf8mb4",
|
||||
createDatabaseCharsetSearchPlaceholder: "Cerca o digita un set di caratteri",
|
||||
createDatabaseCharsetEmpty: "Nessun set di caratteri trovato",
|
||||
createDatabaseCharsetLoading: "Caricamento set di caratteri...",
|
||||
createDatabaseCharsetCustomOption: 'Usa "{value}"',
|
||||
createDatabaseCollation: "Collation",
|
||||
createDatabaseCollationPlaceholder: "es. utf8mb4_unicode_ci",
|
||||
createDatabaseCollationSearchPlaceholder: "Cerca o digita una collation",
|
||||
createDatabaseCollationEmpty: "Nessuna collation trovata",
|
||||
createDatabaseCollationLoading: "Caricamento collation...",
|
||||
createDatabaseCollationCustomOption: 'Usa "{value}"',
|
||||
createSchema: "Crea Schema",
|
||||
dropSchema: "Elimina Schema",
|
||||
confirmDropSchemaTitle: "Elimina Schema",
|
||||
|
||||
@@ -1259,8 +1259,16 @@ export default {
|
||||
createDatabaseNamePlaceholder: "データベース名",
|
||||
createDatabaseCharset: "文字セット",
|
||||
createDatabaseCharsetPlaceholder: "例: utf8mb4",
|
||||
createDatabaseCharsetSearchPlaceholder: "文字セットを検索または入力",
|
||||
createDatabaseCharsetEmpty: "文字セットが見つかりません",
|
||||
createDatabaseCharsetLoading: "文字セットを読み込み中...",
|
||||
createDatabaseCharsetCustomOption: "「{value}」を使用",
|
||||
createDatabaseCollation: "照合順序",
|
||||
createDatabaseCollationPlaceholder: "例: utf8mb4_unicode_ci",
|
||||
createDatabaseCollationSearchPlaceholder: "照合順序を検索または入力",
|
||||
createDatabaseCollationEmpty: "照合順序が見つかりません",
|
||||
createDatabaseCollationLoading: "照合順序を読み込み中...",
|
||||
createDatabaseCollationCustomOption: "「{value}」を使用",
|
||||
createSchema: "スキーマを作成",
|
||||
dropSchema: "スキーマを削除",
|
||||
confirmDropSchemaTitle: "スキーマを削除",
|
||||
|
||||
@@ -1207,8 +1207,16 @@ export default {
|
||||
createDatabaseNamePlaceholder: "Nome do banco de dados",
|
||||
createDatabaseCharset: "Conjunto de caracteres",
|
||||
createDatabaseCharsetPlaceholder: "por exemplo, utf8mb4",
|
||||
createDatabaseCharsetSearchPlaceholder: "Pesquise ou digite um conjunto de caracteres",
|
||||
createDatabaseCharsetEmpty: "Nenhum conjunto de caracteres encontrado",
|
||||
createDatabaseCharsetLoading: "Carregando conjuntos de caracteres...",
|
||||
createDatabaseCharsetCustomOption: 'Usar "{value}"',
|
||||
createDatabaseCollation: "Collation",
|
||||
createDatabaseCollationPlaceholder: "por exemplo, utf8mb4_unicode_ci",
|
||||
createDatabaseCollationSearchPlaceholder: "Pesquise ou digite uma collation",
|
||||
createDatabaseCollationEmpty: "Nenhuma collation encontrada",
|
||||
createDatabaseCollationLoading: "Carregando collations...",
|
||||
createDatabaseCollationCustomOption: 'Usar "{value}"',
|
||||
createSchema: "Criar Schema",
|
||||
dropSchema: "Remover Schema",
|
||||
confirmDropSchemaTitle: "Remover Schema",
|
||||
|
||||
@@ -1269,8 +1269,16 @@ export default {
|
||||
createDatabaseNamePlaceholder: "数据库名称",
|
||||
createDatabaseCharset: "字符集",
|
||||
createDatabaseCharsetPlaceholder: "例如 utf8mb4",
|
||||
createDatabaseCharsetSearchPlaceholder: "搜索或输入字符集",
|
||||
createDatabaseCharsetEmpty: "未找到字符集",
|
||||
createDatabaseCharsetLoading: "正在加载字符集...",
|
||||
createDatabaseCharsetCustomOption: "使用“{value}”",
|
||||
createDatabaseCollation: "排序规则",
|
||||
createDatabaseCollationPlaceholder: "例如 utf8mb4_unicode_ci",
|
||||
createDatabaseCollationSearchPlaceholder: "搜索或输入排序规则",
|
||||
createDatabaseCollationEmpty: "未找到排序规则",
|
||||
createDatabaseCollationLoading: "正在加载排序规则...",
|
||||
createDatabaseCollationCustomOption: "使用“{value}”",
|
||||
createSchema: "新建 Schema",
|
||||
dropSchema: "删除 Schema",
|
||||
confirmDropSchemaTitle: "删除 Schema",
|
||||
|
||||
@@ -1187,8 +1187,16 @@ export default {
|
||||
createDatabaseNamePlaceholder: "資料庫名稱",
|
||||
createDatabaseCharset: "字元集",
|
||||
createDatabaseCharsetPlaceholder: "例如 utf8mb4",
|
||||
createDatabaseCharsetSearchPlaceholder: "搜尋或輸入字元集",
|
||||
createDatabaseCharsetEmpty: "找不到字元集",
|
||||
createDatabaseCharsetLoading: "正在載入字元集...",
|
||||
createDatabaseCharsetCustomOption: "使用「{value}」",
|
||||
createDatabaseCollation: "排序規則",
|
||||
createDatabaseCollationPlaceholder: "例如 utf8mb4_unicode_ci",
|
||||
createDatabaseCollationSearchPlaceholder: "搜尋或輸入排序規則",
|
||||
createDatabaseCollationEmpty: "找不到排序規則",
|
||||
createDatabaseCollationLoading: "正在載入排序規則...",
|
||||
createDatabaseCollationCustomOption: "使用「{value}」",
|
||||
createSchema: "建立 Schema",
|
||||
dropSchema: "刪除 Schema",
|
||||
confirmDropSchemaTitle: "刪除 Schema",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { CREATE_DATABASE_CHARSET_OPTIONS, createDatabaseCollationOptionsForCharset, defaultCreateDatabaseCollationForCharset, normalizeCreateDatabaseCharset, parseCreateDatabaseCharsetMetadata, nextCreateDatabaseCollation } from "@/lib/createDatabaseCharsetOptions";
|
||||
|
||||
describe("createDatabaseCharsetOptions", () => {
|
||||
it("offers common MySQL character sets with utf8mb4 first", () => {
|
||||
expect(CREATE_DATABASE_CHARSET_OPTIONS.slice(0, 4)).toEqual(["utf8mb4", "utf8", "gbk", "latin1"]);
|
||||
expect(CREATE_DATABASE_CHARSET_OPTIONS).toEqual(expect.arrayContaining(["ascii", "big5", "gb2312", "utf16", "utf32"]));
|
||||
});
|
||||
|
||||
it("normalizes typed custom values without changing their case", () => {
|
||||
expect(normalizeCreateDatabaseCharset(" utf8mb3 ")).toBe("utf8mb3");
|
||||
expect(normalizeCreateDatabaseCharset("Big5")).toBe("Big5");
|
||||
});
|
||||
|
||||
it("offers common collations for the selected character set", () => {
|
||||
expect(createDatabaseCollationOptionsForCharset("utf8mb4").slice(0, 3)).toEqual(["utf8mb4_unicode_ci", "utf8mb4_general_ci", "utf8mb4_bin"]);
|
||||
expect(createDatabaseCollationOptionsForCharset("utf8mb4")).toEqual(expect.arrayContaining(["utf8mb4_bin"]));
|
||||
expect(createDatabaseCollationOptionsForCharset("utf8mb4").some((collation) => collation.includes("0900"))).toBe(false);
|
||||
expect(createDatabaseCollationOptionsForCharset("gbk")).toEqual(["gbk_chinese_ci", "gbk_bin"]);
|
||||
expect(createDatabaseCollationOptionsForCharset("utf8")).toEqual(createDatabaseCollationOptionsForCharset("utf8mb3"));
|
||||
});
|
||||
|
||||
it("parses server-provided character sets and collations", () => {
|
||||
const metadata = parseCreateDatabaseCharsetMetadata(
|
||||
{
|
||||
columns: ["Charset", "Description", "Default collation", "Maxlen"],
|
||||
rows: [
|
||||
["utf8mb4", "UTF-8 Unicode", "utf8mb4_general_ci", 4],
|
||||
["gbk", "GBK Simplified Chinese", "gbk_chinese_ci", 2],
|
||||
],
|
||||
},
|
||||
{
|
||||
columns: ["Collation", "Charset", "Id", "Default", "Compiled", "Sortlen"],
|
||||
rows: [
|
||||
["utf8mb4_general_ci", "utf8mb4", 45, "Yes", "Yes", 1],
|
||||
["utf8mb4_unicode_ci", "utf8mb4", 224, "", "Yes", 8],
|
||||
["gbk_chinese_ci", "gbk", 28, "Yes", "Yes", 1],
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expect(metadata.charsets).toEqual(["utf8mb4", "gbk"]);
|
||||
expect(metadata.collationsByCharset.utf8mb4).toEqual(["utf8mb4_general_ci", "utf8mb4_unicode_ci"]);
|
||||
expect(metadata.collationsByCharset.gbk).toEqual(["gbk_chinese_ci"]);
|
||||
});
|
||||
|
||||
it("keeps server-provided utf8mb4 collations while hiding 0900 entries from the dropdown", () => {
|
||||
const metadata = parseCreateDatabaseCharsetMetadata(
|
||||
{
|
||||
columns: ["Charset", "Description", "Default collation", "Maxlen"],
|
||||
rows: [["utf8mb4", "UTF-8 Unicode", "utf8mb4_0900_ai_ci", 4]],
|
||||
},
|
||||
{
|
||||
columns: ["Collation", "Charset", "Id", "Default", "Compiled", "Sortlen"],
|
||||
rows: [
|
||||
["utf8mb4_0900_ai_ci", "utf8mb4", 255, "Yes", "Yes", 0],
|
||||
["utf8mb4_general_ci", "utf8mb4", 45, "", "Yes", 1],
|
||||
["utf8mb4_unicode_ci", "utf8mb4", 224, "", "Yes", 8],
|
||||
["utf8mb4_unicode_520_ci", "utf8mb4", 246, "", "Yes", 8],
|
||||
["utf8mb4_bin", "utf8mb4", 46, "", "Yes", 1],
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
expect(metadata.collationsByCharset.utf8mb4).toEqual(["utf8mb4_general_ci", "utf8mb4_unicode_ci", "utf8mb4_unicode_520_ci", "utf8mb4_bin"]);
|
||||
});
|
||||
|
||||
it("keeps custom collations unless the previous value was the old charset default", () => {
|
||||
expect(defaultCreateDatabaseCollationForCharset("latin1")).toBe("latin1_swedish_ci");
|
||||
expect(nextCreateDatabaseCollation("gbk", "utf8mb4", "utf8mb4_unicode_ci")).toBe("gbk_chinese_ci");
|
||||
expect(nextCreateDatabaseCollation("gbk", "utf8mb4", "custom_ci")).toBe("custom_ci");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
type QueryLikeResult = {
|
||||
columns: string[];
|
||||
rows: (string | number | boolean | null)[][];
|
||||
};
|
||||
|
||||
export interface CreateDatabaseCharsetMetadata {
|
||||
charsets: string[];
|
||||
collationsByCharset: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export const CREATE_DATABASE_CHARSET_OPTIONS = ["utf8mb4", "utf8", "gbk", "latin1", "ascii", "big5", "gb2312", "utf16", "utf32"] as const;
|
||||
|
||||
const FALLBACK_COLLATION_OPTIONS: Record<string, string[]> = {
|
||||
ascii: ["ascii_general_ci", "ascii_bin"],
|
||||
big5: ["big5_chinese_ci", "big5_bin"],
|
||||
gb2312: ["gb2312_chinese_ci", "gb2312_bin"],
|
||||
gbk: ["gbk_chinese_ci", "gbk_bin"],
|
||||
latin1: ["latin1_swedish_ci", "latin1_general_ci", "latin1_bin"],
|
||||
utf16: ["utf16_general_ci", "utf16_unicode_ci", "utf16_bin"],
|
||||
utf32: ["utf32_general_ci", "utf32_unicode_ci", "utf32_bin"],
|
||||
utf8: ["utf8mb3_general_ci", "utf8mb3_unicode_ci", "utf8mb3_bin"],
|
||||
utf8mb3: ["utf8mb3_general_ci", "utf8mb3_unicode_ci", "utf8mb3_bin"],
|
||||
utf8mb4: ["utf8mb4_unicode_ci", "utf8mb4_general_ci", "utf8mb4_bin"],
|
||||
};
|
||||
|
||||
export function normalizeCreateDatabaseCharset(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
export function normalizeCreateDatabaseCharsetKey(value: string): string {
|
||||
const normalized = normalizeCreateDatabaseCharset(value).toLowerCase();
|
||||
return normalized === "utf8" ? "utf8mb3" : normalized;
|
||||
}
|
||||
|
||||
export function createDatabaseCollationOptionsForCharset(charset: string, collationsByCharset: Record<string, string[]> = FALLBACK_COLLATION_OPTIONS): string[] {
|
||||
return collationsByCharset[normalizeCreateDatabaseCharsetKey(charset)] ?? [];
|
||||
}
|
||||
|
||||
export function defaultCreateDatabaseCollationForCharset(charset: string, collationsByCharset: Record<string, string[]> = FALLBACK_COLLATION_OPTIONS): string {
|
||||
return createDatabaseCollationOptionsForCharset(charset, collationsByCharset)[0] ?? "";
|
||||
}
|
||||
|
||||
export function nextCreateDatabaseCollation(nextCharset: string, previousCharset: string, currentCollation: string, collationsByCharset: Record<string, string[]> = FALLBACK_COLLATION_OPTIONS): string {
|
||||
const previousDefault = defaultCreateDatabaseCollationForCharset(previousCharset, collationsByCharset);
|
||||
if (currentCollation && currentCollation !== previousDefault) return currentCollation;
|
||||
return defaultCreateDatabaseCollationForCharset(nextCharset, collationsByCharset) || currentCollation;
|
||||
}
|
||||
|
||||
export function fallbackCreateDatabaseCharsetMetadata(): CreateDatabaseCharsetMetadata {
|
||||
return {
|
||||
charsets: [...CREATE_DATABASE_CHARSET_OPTIONS],
|
||||
collationsByCharset: { ...FALLBACK_COLLATION_OPTIONS },
|
||||
};
|
||||
}
|
||||
|
||||
export function parseCreateDatabaseCharsetMetadata(charsetResult: QueryLikeResult, collationResult: QueryLikeResult): CreateDatabaseCharsetMetadata {
|
||||
const charsetNameIndex = columnIndex(charsetResult.columns, ["charset", "character set", "character_set_name"]);
|
||||
const charsetDefaultCollationIndex = columnIndex(charsetResult.columns, ["default collation", "default_collate_name", "default_collation_name"]);
|
||||
const collationNameIndex = columnIndex(collationResult.columns, ["collation", "collation_name"]);
|
||||
const collationCharsetIndex = columnIndex(collationResult.columns, ["charset", "character_set_name"]);
|
||||
const collationDefaultIndex = columnIndex(collationResult.columns, ["default", "is_default"]);
|
||||
|
||||
const charsets = uniqueStrings(charsetResult.rows.map((row) => cellString(row[charsetNameIndex])));
|
||||
const collationsByCharset: Record<string, string[]> = {};
|
||||
|
||||
for (const row of collationResult.rows) {
|
||||
const collation = cellString(row[collationNameIndex]);
|
||||
const charset = normalizeCreateDatabaseCharsetKey(cellString(row[collationCharsetIndex]));
|
||||
if (!collation || !charset || isHiddenCreateDatabaseCollation(collation)) continue;
|
||||
collationsByCharset[charset] ??= [];
|
||||
if (isDefaultFlag(row[collationDefaultIndex])) {
|
||||
collationsByCharset[charset].unshift(collation);
|
||||
} else if (!collationsByCharset[charset].includes(collation)) {
|
||||
collationsByCharset[charset].push(collation);
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of charsetResult.rows) {
|
||||
const charset = normalizeCreateDatabaseCharsetKey(cellString(row[charsetNameIndex]));
|
||||
const defaultCollation = cellString(row[charsetDefaultCollationIndex]);
|
||||
if (!charset || !defaultCollation || isHiddenCreateDatabaseCollation(defaultCollation)) continue;
|
||||
collationsByCharset[charset] ??= [];
|
||||
const existingIndex = collationsByCharset[charset].indexOf(defaultCollation);
|
||||
if (existingIndex > 0) collationsByCharset[charset].splice(existingIndex, 1);
|
||||
if (existingIndex !== 0) collationsByCharset[charset].unshift(defaultCollation);
|
||||
}
|
||||
|
||||
return {
|
||||
charsets: charsets.length ? charsets : [...CREATE_DATABASE_CHARSET_OPTIONS],
|
||||
collationsByCharset: Object.keys(collationsByCharset).length ? collationsByCharset : { ...FALLBACK_COLLATION_OPTIONS },
|
||||
};
|
||||
}
|
||||
|
||||
function columnIndex(columns: string[], names: string[]): number {
|
||||
const lowerNames = names.map((name) => name.toLowerCase());
|
||||
const index = columns.findIndex((column) => lowerNames.includes(column.trim().toLowerCase()));
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
function cellString(value: unknown): string {
|
||||
return typeof value === "string" ? value.trim() : value == null ? "" : String(value).trim();
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]): string[] {
|
||||
return [...new Set(values.filter(Boolean))];
|
||||
}
|
||||
|
||||
function isHiddenCreateDatabaseCollation(value: string): boolean {
|
||||
return value.toLowerCase().includes("_0900_");
|
||||
}
|
||||
|
||||
function isDefaultFlag(value: unknown): boolean {
|
||||
if (value === true) return true;
|
||||
if (typeof value !== "string") return false;
|
||||
return ["yes", "y", "1", "true"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
Reference in new issue
Block a user