feat(grid): copy column names with quoting options
This commit is contained in:
1 parent
d341581ff0
commit
7eda73fbd8
15 files changed
+336
-11
No files matched your search
@@ -0,0 +1,75 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { COLUMN_NAME_COPY_SEPARATOR_OPTIONS, columnNamesForCopy, formatColumnNamesForCopy, isColumnNameCopySeparator, loadColumnNameCopySeparator, saveColumnNameCopySeparator, supportsColumnNameQuoting } from "@/lib/dataGrid/dataGridColumnNameCopy";
|
||||
|
||||
describe("dataGridColumnNameCopy", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("joins names with every supported separator", () => {
|
||||
const names = ["id", "type", "order"];
|
||||
expect(formatColumnNamesForCopy(names, { separator: "tab" })).toBe("id\ttype\torder");
|
||||
expect(formatColumnNamesForCopy(names, { separator: "comma" })).toBe("id,type,order");
|
||||
expect(formatColumnNamesForCopy(names, { separator: "newline" })).toBe("id\ntype\norder");
|
||||
expect(formatColumnNamesForCopy(names, { separator: "comma-newline" })).toBe("id,\ntype,\norder");
|
||||
});
|
||||
|
||||
it("quotes names with the database-specific identifier quote", () => {
|
||||
const names = ["type", "order"];
|
||||
expect(formatColumnNamesForCopy(names, { separator: "comma", quote: true, databaseType: "mysql" })).toBe("`type`,`order`");
|
||||
expect(formatColumnNamesForCopy(names, { separator: "comma", quote: true, databaseType: "clickhouse" })).toBe("`type`,`order`");
|
||||
expect(formatColumnNamesForCopy(names, { separator: "comma", quote: true, databaseType: "postgres" })).toBe('"type","order"');
|
||||
expect(formatColumnNamesForCopy(names, { separator: "comma", quote: true, databaseType: "sqlserver" })).toBe("[type],[order]");
|
||||
});
|
||||
|
||||
it("escapes embedded quote characters when quoting", () => {
|
||||
expect(formatColumnNamesForCopy(["a`b"], { separator: "tab", quote: true, databaseType: "mysql" })).toBe("`a``b`");
|
||||
expect(formatColumnNamesForCopy(['a"b'], { separator: "tab", quote: true, databaseType: "postgres" })).toBe('"a""b"');
|
||||
});
|
||||
|
||||
it("ignores the quote flag for databases without SQL identifier quoting", () => {
|
||||
expect(formatColumnNamesForCopy(["type"], { separator: "tab", quote: true, databaseType: "mongodb" })).toBe("type");
|
||||
expect(formatColumnNamesForCopy(["type"], { separator: "tab", quote: true, databaseType: "qdrant" })).toBe("type");
|
||||
expect(formatColumnNamesForCopy(["type"], { separator: "tab", quote: true })).toBe("type");
|
||||
});
|
||||
|
||||
it("reports quoting support only for SQL databases with a usable quote character", () => {
|
||||
expect(supportsColumnNameQuoting("mysql")).toBe(true);
|
||||
expect(supportsColumnNameQuoting("clickhouse")).toBe(true);
|
||||
expect(supportsColumnNameQuoting("postgres")).toBe(true);
|
||||
expect(supportsColumnNameQuoting("mongodb")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("redis")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("elasticsearch")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("qdrant")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("jdbc")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("iotdb")).toBe(false);
|
||||
expect(supportsColumnNameQuoting(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps hidden columns when copying all column names", () => {
|
||||
const allColumns = ["id", "hidden_value", "created_at"];
|
||||
const visibleColumns = ["id", "created_at"];
|
||||
expect(columnNamesForCopy(allColumns, visibleColumns, "all")).toEqual(allColumns);
|
||||
expect(columnNamesForCopy(allColumns, visibleColumns, "visible")).toEqual(visibleColumns);
|
||||
});
|
||||
|
||||
it("validates separator values", () => {
|
||||
for (const option of COLUMN_NAME_COPY_SEPARATOR_OPTIONS) expect(isColumnNameCopySeparator(option)).toBe(true);
|
||||
expect(isColumnNameCopySeparator(";")).toBe(false);
|
||||
expect(isColumnNameCopySeparator(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("persists the separator choice and falls back to tab for missing or invalid values", () => {
|
||||
const store = new Map<string, string>();
|
||||
vi.stubGlobal("localStorage", {
|
||||
getItem: (key: string) => store.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => void store.set(key, value),
|
||||
removeItem: (key: string) => void store.delete(key),
|
||||
});
|
||||
expect(loadColumnNameCopySeparator()).toBe("tab");
|
||||
saveColumnNameCopySeparator("comma-newline");
|
||||
expect(loadColumnNameCopySeparator()).toBe("comma-newline");
|
||||
store.set("dbx-copy-column-names-separator", "bogus");
|
||||
expect(loadColumnNameCopySeparator()).toBe("tab");
|
||||
});
|
||||
});
|
||||
@@ -39,6 +39,11 @@ describe("quoteTableIdentifier", () => {
|
||||
expect(quoteTableIdentifier("mysql", "orders")).toBe("`orders`");
|
||||
});
|
||||
|
||||
it("uses BigQuery quoted identifiers and escape sequences", () => {
|
||||
expect(quoteTableIdentifier("bigquery", "order")).toBe("`order`");
|
||||
expect(quoteTableIdentifier("bigquery", "a`b")).toBe("`a\\`b`");
|
||||
});
|
||||
|
||||
it("bracket-quotes sqlserver identifiers", () => {
|
||||
expect(quoteTableIdentifier("sqlserver", "orders")).toBe("[orders]");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { quoteTableIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
|
||||
|
||||
export type ColumnNameCopySeparator = "tab" | "comma" | "newline" | "comma-newline";
|
||||
|
||||
export const COLUMN_NAME_COPY_SEPARATOR_VALUES: Record<ColumnNameCopySeparator, string> = {
|
||||
tab: "\t",
|
||||
comma: ",",
|
||||
newline: "\n",
|
||||
"comma-newline": ",\n",
|
||||
};
|
||||
|
||||
export const COLUMN_NAME_COPY_SEPARATOR_OPTIONS = Object.keys(COLUMN_NAME_COPY_SEPARATOR_VALUES) as ColumnNameCopySeparator[];
|
||||
|
||||
export const COLUMN_NAME_COPY_SEPARATOR_LABELS: Record<ColumnNameCopySeparator, string> = {
|
||||
tab: "\\t",
|
||||
comma: ",",
|
||||
newline: "\\n",
|
||||
"comma-newline": ",\\n",
|
||||
};
|
||||
|
||||
export function isColumnNameCopySeparator(value: unknown): value is ColumnNameCopySeparator {
|
||||
return typeof value === "string" && value in COLUMN_NAME_COPY_SEPARATOR_VALUES;
|
||||
}
|
||||
|
||||
// SQL 执行能力是产品能力,不代表数据库支持 SQL 标识符引用。
|
||||
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos"]);
|
||||
// quoteTableIdentifier 对 jdbc/iotdb 原样返回(无引用字符可用)。
|
||||
const UNQUOTABLE_DATABASE_TYPES = new Set<DatabaseType>(["jdbc", "iotdb"]);
|
||||
|
||||
export function supportsColumnNameQuoting(databaseType?: DatabaseType): boolean {
|
||||
return !!databaseType && !NON_SQL_DATABASE_TYPES.has(databaseType) && !UNQUOTABLE_DATABASE_TYPES.has(databaseType);
|
||||
}
|
||||
|
||||
export function columnNamesForCopy(allColumnNames: readonly string[], visibleColumnNames: readonly string[], scope: "all" | "visible"): string[] {
|
||||
return [...(scope === "all" ? allColumnNames : visibleColumnNames)];
|
||||
}
|
||||
|
||||
export function formatColumnNamesForCopy(names: readonly string[], options: { separator: ColumnNameCopySeparator; quote?: boolean; databaseType?: DatabaseType }): string {
|
||||
const quote = !!options.quote && supportsColumnNameQuoting(options.databaseType);
|
||||
const parts = quote ? names.map((name) => quoteTableIdentifier(options.databaseType, name)) : [...names];
|
||||
return parts.join(COLUMN_NAME_COPY_SEPARATOR_VALUES[options.separator]);
|
||||
}
|
||||
|
||||
const SEPARATOR_STORAGE_KEY = "dbx-copy-column-names-separator";
|
||||
|
||||
export function loadColumnNameCopySeparator(): ColumnNameCopySeparator {
|
||||
const stored = safeLocalStorageGet(SEPARATOR_STORAGE_KEY);
|
||||
return isColumnNameCopySeparator(stored) ? stored : "tab";
|
||||
}
|
||||
|
||||
export function saveColumnNameCopySeparator(separator: ColumnNameCopySeparator) {
|
||||
safeLocalStorageSet(SEPARATOR_STORAGE_KEY, separator);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
|
||||
// JDBC connections use the driver-reported identifier quote string
|
||||
// (DatabaseMetaData.getIdentifierQuoteString()) — pass through unquoted.
|
||||
if (databaseType === "jdbc") return name;
|
||||
if (databaseType === "bigquery") return `\`${name.replace(/`/g, "\\`")}\``;
|
||||
if (databaseType === "mysql" || databaseType === "clickhouse" || databaseType === "hive" || databaseType === "spark" || databaseType === "databend" || databaseType === "tdengine" || databaseType === "access" || databaseType === "doris" || databaseType === "starrocks")
|
||||
return `\`${name.replace(/`/g, "``")}\``;
|
||||
if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name;
|
||||
|
||||
Reference in new issue
Block a user