feat: add selectable database charset options

This commit is contained in:
luoianun authored and GitHub committed 2026-06-23 21:17:55 +08:00
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>
+8
View File
@@ -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",
+8
View File
@@ -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",
+8
View File
@@ -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: "スキーマを削除",
+8
View File
@@ -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",
+8
View File
@@ -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",
+8
View File
@@ -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());
}