feat(data-grid): support manual reference display values
This commit is contained in:
24 files changed
+893
-90
No files matched your search
@@ -1,5 +1,42 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyColumnFormatter, defaultIoTDBTimestampFormatter, formatIoTDBTimestampEditorValue, normalizeSupportedDateTimePattern, parseIoTDBTimestampEditorValue } from "@/lib/dataGrid/columnFormatter";
|
||||
import { applyColumnFormatter, defaultIoTDBTimestampFormatter, formatIoTDBTimestampEditorValue, normalizeColumnFormatter, normalizeSupportedDateTimePattern, parseIoTDBTimestampEditorValue } from "@/lib/dataGrid/columnFormatter";
|
||||
|
||||
describe("normalizeColumnFormatter", () => {
|
||||
it("preserves a structured manual reference filter", () => {
|
||||
expect(
|
||||
normalizeColumnFormatter({
|
||||
kind: "foreign-key-display",
|
||||
referenceMode: "manual",
|
||||
refSchema: " public ",
|
||||
refTable: " dictionary ",
|
||||
refColumn: " dict_key ",
|
||||
displayColumn: " dict_value ",
|
||||
filter: { column: " dict_type ", mode: "equals", value: " order_status " },
|
||||
}),
|
||||
).toEqual({
|
||||
kind: "foreign-key-display",
|
||||
referenceMode: "manual",
|
||||
refSchema: "public",
|
||||
refTable: "dictionary",
|
||||
refColumn: "dict_key",
|
||||
displayColumn: "dict_value",
|
||||
filter: { column: "dict_type", mode: "equals", value: "order_status", endValue: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects incomplete or unsupported manual reference filters", () => {
|
||||
const base = {
|
||||
kind: "foreign-key-display",
|
||||
referenceMode: "manual",
|
||||
refTable: "dictionary",
|
||||
refColumn: "dict_key",
|
||||
displayColumn: "dict_value",
|
||||
};
|
||||
expect(normalizeColumnFormatter({ ...base, filter: { column: "dict_type", mode: "equals", value: "" } })).toBeUndefined();
|
||||
expect(normalizeColumnFormatter({ ...base, filter: { column: "dict_type", mode: "between", value: "A" } })).toBeUndefined();
|
||||
expect(normalizeColumnFormatter({ ...base, filter: { column: "dict_type", mode: "raw-sql", value: "1=1" } })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeSupportedDateTimePattern", () => {
|
||||
it("accepts the format grammar shared by the frontend and backend", () => {
|
||||
|
||||
@@ -2,15 +2,20 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectForeignKeyDisplayValues,
|
||||
createForeignKeyDisplayRequestCoordinator,
|
||||
foreignKeyDisplayConfigIsUsable,
|
||||
foreignKeyDisplayConfigMatches,
|
||||
foreignKeyDisplayLookupRequestKey,
|
||||
foreignKeyDisplayMapFromResult,
|
||||
formatForeignKeyDisplayValue,
|
||||
manualReferenceColumnValidation,
|
||||
manualReferenceKeyColumnIsUnique,
|
||||
manualReferenceKeyColumns,
|
||||
reconcileManualReferenceColumn,
|
||||
singleColumnForeignKey,
|
||||
splitForeignKeyDisplayValues,
|
||||
} from "@/lib/dataGrid/dataGridForeignKeyDisplay";
|
||||
import { buildColumnForeignKeyMap } from "@/lib/dataGrid/dataGridForeignKeyNavigation";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
import type { ColumnInfo, QueryResult } from "@/types/database";
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
@@ -41,6 +46,46 @@ describe("dataGridForeignKeyDisplay", () => {
|
||||
expect(foreignKeyDisplayConfigMatches(config, { name: "fk_user", column: "user_id", ref_schema: "archive", ref_table: "users", ref_column: "id" })).toBe(false);
|
||||
});
|
||||
|
||||
it("allows explicit manual references without foreign-key metadata", () => {
|
||||
const manual = {
|
||||
kind: "foreign-key-display" as const,
|
||||
referenceMode: "manual" as const,
|
||||
refTable: "dictionary",
|
||||
refColumn: "dict_key",
|
||||
displayColumn: "dict_value",
|
||||
};
|
||||
const automatic = { ...manual, referenceMode: "foreign-key" as const };
|
||||
|
||||
expect(foreignKeyDisplayConfigIsUsable(manual, undefined)).toBe(true);
|
||||
expect(foreignKeyDisplayConfigIsUsable(automatic, undefined)).toBe(false);
|
||||
expect(foreignKeyDisplayConfigIsUsable(automatic, { name: "fk_dict", column: "status", ref_table: "dictionary", ref_column: "dict_key" })).toBe(true);
|
||||
});
|
||||
|
||||
it("uses backend-approved reference keys with exact metadata identity", () => {
|
||||
const columns = [
|
||||
{ name: "tenant_id", is_primary_key: true },
|
||||
{ name: "record_id", is_primary_key: true },
|
||||
{ name: "code", is_primary_key: false },
|
||||
{ name: "Code", is_primary_key: false },
|
||||
].map((column) => ({ data_type: "varchar", is_nullable: false, column_default: null, extra: null, ...column })) as ColumnInfo[];
|
||||
|
||||
expect(manualReferenceKeyColumns(columns, ["code"]).map((column) => column.name)).toEqual(["code"]);
|
||||
expect(manualReferenceKeyColumnIsUnique(columns, ["code"], "code")).toBe(true);
|
||||
expect(manualReferenceKeyColumnIsUnique(columns, ["code"], "Code")).toBe(false);
|
||||
expect(manualReferenceKeyColumnIsUnique(columns, [], "tenant_id")).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves saved reference columns when metadata is invalid or unavailable", () => {
|
||||
const columns = [{ name: "id", data_type: "bigint", is_nullable: false, column_default: null, is_primary_key: true, extra: null }] as ColumnInfo[];
|
||||
|
||||
expect(reconcileManualReferenceColumn("legacy_code", columns, "available")).toBe("legacy_code");
|
||||
expect(reconcileManualReferenceColumn("legacy_code", [], "unavailable")).toBe("legacy_code");
|
||||
expect(reconcileManualReferenceColumn("", columns, "available")).toBe("id");
|
||||
expect(reconcileManualReferenceColumn("", columns, "unavailable")).toBe("");
|
||||
expect(manualReferenceColumnValidation(columns, [], "legacy_code", "available")).toBe("invalid");
|
||||
expect(manualReferenceColumnValidation(columns, [], "legacy_code", "unavailable")).toBe("unavailable");
|
||||
});
|
||||
|
||||
it("deduplicates current-page keys with type-safe identities and bounded batches", () => {
|
||||
const rows = [[100], [100], ["100"], [null], [101], [{ id: 1 }]] as QueryResult["rows"];
|
||||
const values = collectForeignKeyDisplayValues(rows, 0);
|
||||
@@ -48,6 +93,13 @@ describe("dataGridForeignKeyDisplay", () => {
|
||||
expect(splitForeignKeyDisplayValues(values, 2)).toEqual([[100, "100"], [101]]);
|
||||
});
|
||||
|
||||
it("deduplicates SQL-equivalent numeric values without collapsing text keys", () => {
|
||||
const rows = [[100], ["100.0"], ["1e2"], ["0100"], [101]] as QueryResult["rows"];
|
||||
|
||||
expect(collectForeignKeyDisplayValues(rows, 0, "decimal(20, 4)")).toEqual([100, 101]);
|
||||
expect(collectForeignKeyDisplayValues(rows, 0, "varchar")).toEqual([100, "100.0", "1e2", "0100", 101]);
|
||||
});
|
||||
|
||||
it("caps a large page at 2000 unique values and four 500-value batches", () => {
|
||||
const rows = Array.from({ length: 2500 }, (_, index) => [index]) as QueryResult["rows"];
|
||||
const values = collectForeignKeyDisplayValues(rows, 0);
|
||||
@@ -70,17 +122,47 @@ describe("dataGridForeignKeyDisplay", () => {
|
||||
],
|
||||
} as QueryResult;
|
||||
const labels = foreignKeyDisplayMapFromResult(result);
|
||||
const codeLabels = foreignKeyDisplayMapFromResult(result, "ID", "CODE");
|
||||
const codeLabels = foreignKeyDisplayMapFromResult(result, "id", "code");
|
||||
const mismatchedCaseLabels = foreignKeyDisplayMapFromResult(result, "ID", "CODE");
|
||||
|
||||
expect(formatForeignKeyDisplayValue(100, labels)).toBe("100 (张三)");
|
||||
expect(formatForeignKeyDisplayValue(102, labels)).toBe("102");
|
||||
expect(formatForeignKeyDisplayValue(103, labels)).toBe("103");
|
||||
expect(formatForeignKeyDisplayValue(null, labels)).toBe("NULL");
|
||||
expect(mismatchedCaseLabels.size).toBe(0);
|
||||
expect(formatForeignKeyDisplayValue("same", new Map([["string\u0000same", "same"]]))).toBe("same");
|
||||
expect(formatForeignKeyDisplayValue(100, codeLabels)).toBe("100 (U100)");
|
||||
expect(foreignKeyDisplayMapFromResult(result, "missing", "code")).toEqual(new Map());
|
||||
});
|
||||
|
||||
it("matches numeric driver strings while preserving distinct text values", () => {
|
||||
const numericResult = { columns: ["id", "name"], rows: [["100.00", "numeric label"]] } as QueryResult;
|
||||
const numericLabels = foreignKeyDisplayMapFromResult(numericResult, "id", "name", "decimal(20, 2)");
|
||||
expect(formatForeignKeyDisplayValue(100, numericLabels, "decimal(20, 2)")).toBe("100 (numeric label)");
|
||||
|
||||
const textResult = {
|
||||
columns: ["id", "name"],
|
||||
rows: [
|
||||
["01", "leading zero"],
|
||||
["1", "plain"],
|
||||
],
|
||||
} as QueryResult;
|
||||
const textLabels = foreignKeyDisplayMapFromResult(textResult, "id", "name", "varchar");
|
||||
expect(formatForeignKeyDisplayValue("01", textLabels, "varchar")).toBe("01 (leading zero)");
|
||||
expect(formatForeignKeyDisplayValue("1", textLabels, "varchar")).toBe("1 (plain)");
|
||||
|
||||
const largeNumericResult = {
|
||||
columns: ["id", "name"],
|
||||
rows: [
|
||||
["9007199254740992", "even"],
|
||||
["9007199254740993", "odd"],
|
||||
],
|
||||
} as QueryResult;
|
||||
const largeNumericLabels = foreignKeyDisplayMapFromResult(largeNumericResult, "id", "name", "decimal(20, 0)");
|
||||
expect(formatForeignKeyDisplayValue("9007199254740992", largeNumericLabels, "decimal(20, 0)")).toBe("9007199254740992 (even)");
|
||||
expect(formatForeignKeyDisplayValue("9007199254740993", largeNumericLabels, "decimal(20, 0)")).toBe("9007199254740993 (odd)");
|
||||
});
|
||||
|
||||
it("deduplicates in-flight requests and reuses the bounded cache across generations", async () => {
|
||||
const coordinator = createForeignKeyDisplayRequestCoordinator({ concurrency: 2 });
|
||||
const gate = deferred<number>();
|
||||
@@ -94,6 +176,13 @@ describe("dataGridForeignKeyDisplay", () => {
|
||||
const firstKey = foreignKeyDisplayLookupRequestKey({ ...requestScope, values: [100, "100"] });
|
||||
const reorderedKey = foreignKeyDisplayLookupRequestKey({ ...requestScope, values: ["100", 100] });
|
||||
expect(reorderedKey).toBe(firstKey);
|
||||
expect(
|
||||
foreignKeyDisplayLookupRequestKey({
|
||||
...requestScope,
|
||||
filter: { column: "dict_type", mode: "equals", value: "order_status" },
|
||||
values: [100, "100"],
|
||||
}),
|
||||
).not.toBe(firstKey);
|
||||
|
||||
const first = coordinator.request(generation, firstKey, task);
|
||||
const duplicate = coordinator.request(generation, reorderedKey, task);
|
||||
|
||||
@@ -172,6 +172,7 @@ export const getAllColumns = forward("getAllColumns");
|
||||
export const getSqlServerColumnMetadata = forward("getSqlServerColumnMetadata");
|
||||
export const listDataTypes = forward("listDataTypes");
|
||||
export const listIndexes = forward("listIndexes");
|
||||
export const listReferenceKeyColumns = forward("listReferenceKeyColumns");
|
||||
export const listForeignKeys = forward("listForeignKeys");
|
||||
export const listTriggers = forward("listTriggers");
|
||||
export const listConstraints = forward("listConstraints");
|
||||
|
||||
@@ -891,6 +891,10 @@ export async function listIndexes(connectionId: string, database: string, schema
|
||||
return get(`/api/schema/indexes?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
|
||||
}
|
||||
|
||||
export async function listReferenceKeyColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<string[]> {
|
||||
return get(`/api/schema/reference-key-columns?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
|
||||
}
|
||||
|
||||
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ForeignKeyInfo[]> {
|
||||
return get(`/api/schema/foreign-keys?${qs({ connection_id: connectionId, database, schema, table, catalog })}`);
|
||||
}
|
||||
|
||||
@@ -1726,6 +1726,16 @@ export async function listIndexes(connectionId: string, database: string, schema
|
||||
});
|
||||
}
|
||||
|
||||
export async function listReferenceKeyColumns(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<string[]> {
|
||||
return invoke("list_reference_key_columns", {
|
||||
connectionId,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
catalog,
|
||||
});
|
||||
}
|
||||
|
||||
export async function listForeignKeys(connectionId: string, database: string, schema: string, table: string, catalog?: string): Promise<ForeignKeyInfo[]> {
|
||||
return invoke("list_foreign_keys", {
|
||||
connectionId,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { displayCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import type { DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
@@ -60,6 +61,13 @@ export interface CustomColumnFormatterConfig {
|
||||
template: string;
|
||||
}
|
||||
|
||||
export interface ForeignKeyDisplayFilterConfig {
|
||||
column: string;
|
||||
mode: DataGridContextFilterMode;
|
||||
value?: string;
|
||||
endValue?: string;
|
||||
}
|
||||
|
||||
interface IntlTimeZoneSupport {
|
||||
supportedValuesOf?: (key: "timeZone") => string[];
|
||||
}
|
||||
@@ -105,7 +113,15 @@ export type ColumnFormatterConfig =
|
||||
| { kind: "iotdb-timestamp"; precision: IoTDBTimestampPrecision; timezone: string }
|
||||
| { kind: "json-path"; path: string }
|
||||
| { kind: "mask"; prefix: number; suffix: number }
|
||||
| { kind: "foreign-key-display"; refSchema?: string; refTable: string; refColumn: string; displayColumn: string }
|
||||
| {
|
||||
kind: "foreign-key-display";
|
||||
referenceMode?: "foreign-key" | "manual";
|
||||
refSchema?: string;
|
||||
refTable: string;
|
||||
refColumn: string;
|
||||
displayColumn: string;
|
||||
filter?: ForeignKeyDisplayFilterConfig;
|
||||
}
|
||||
| { kind: "custom-template"; template: string }
|
||||
| { kind: "custom-ref"; formatterId: string };
|
||||
|
||||
@@ -150,12 +166,16 @@ export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig
|
||||
if (typeof config.refTable !== "string" || !config.refTable.trim()) return undefined;
|
||||
if (typeof config.refColumn !== "string" || !config.refColumn.trim()) return undefined;
|
||||
if (typeof config.displayColumn !== "string" || !config.displayColumn.trim()) return undefined;
|
||||
const filter = normalizeForeignKeyDisplayFilter(config.filter);
|
||||
if (config.filter !== undefined && !filter) return undefined;
|
||||
return {
|
||||
kind: "foreign-key-display",
|
||||
...(config.referenceMode === "manual" || config.referenceMode === "foreign-key" ? { referenceMode: config.referenceMode } : {}),
|
||||
refSchema: typeof config.refSchema === "string" && config.refSchema.trim() ? config.refSchema.trim() : undefined,
|
||||
refTable: config.refTable.trim(),
|
||||
refColumn: config.refColumn.trim(),
|
||||
displayColumn: config.displayColumn.trim(),
|
||||
...(filter ? { filter } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,6 +190,26 @@ export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const FOREIGN_KEY_DISPLAY_FILTER_MODES = new Set<DataGridContextFilterMode>(["equals", "not-equals", "is-null", "is-not-null", "like", "not-like", "less-than", "less-than-or-equal", "greater-than", "greater-than-or-equal", "in", "not-in", "between", "not-between"]);
|
||||
|
||||
function normalizeForeignKeyDisplayFilter(value: unknown): ForeignKeyDisplayFilterConfig | undefined {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
const filter = value as Record<string, unknown>;
|
||||
if (typeof filter.column !== "string" || !filter.column.trim()) return undefined;
|
||||
if (typeof filter.mode !== "string" || !FOREIGN_KEY_DISPLAY_FILTER_MODES.has(filter.mode as DataGridContextFilterMode)) return undefined;
|
||||
const mode = filter.mode as DataGridContextFilterMode;
|
||||
const rawValue = typeof filter.value === "string" ? filter.value.trim() : "";
|
||||
const rawEndValue = typeof filter.endValue === "string" ? filter.endValue.trim() : "";
|
||||
if (mode !== "is-null" && mode !== "is-not-null" && !rawValue) return undefined;
|
||||
if ((mode === "between" || mode === "not-between") && !rawEndValue) return undefined;
|
||||
return {
|
||||
column: filter.column.trim(),
|
||||
mode,
|
||||
value: rawValue || undefined,
|
||||
endValue: rawEndValue || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeCustomColumnFormatter(value: unknown): CustomColumnFormatterConfig | undefined {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
const config = value as Record<string, unknown>;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { displayCellValue, type CellValue } from "@/lib/dataGrid/cellValue";
|
||||
import type { ColumnFormatterConfig } from "@/lib/dataGrid/columnFormatter";
|
||||
import type { ColumnFormatterConfig, ForeignKeyDisplayFilterConfig } from "@/lib/dataGrid/columnFormatter";
|
||||
import { isNumericColumnType } from "@/lib/dataGrid/dataGridColumnType";
|
||||
import type { ForeignKeyAssociation } from "@/lib/dataGrid/dataGridForeignKeyNavigation";
|
||||
import type { ForeignKeyInfo, QueryResult } from "@/types/database";
|
||||
import type { ColumnInfo, ForeignKeyInfo, QueryResult } from "@/types/database";
|
||||
|
||||
export type ForeignKeyDisplayConfig = Extract<ColumnFormatterConfig, { kind: "foreign-key-display" }>;
|
||||
|
||||
@@ -160,22 +161,71 @@ export function foreignKeyDisplayConfigMatches(config: ForeignKeyDisplayConfig,
|
||||
return config.refTable.toLowerCase() === foreignKey.ref_table.toLowerCase() && config.refColumn.toLowerCase() === foreignKey.ref_column.toLowerCase() && expectedSchema.toLowerCase() === actualSchema.toLowerCase();
|
||||
}
|
||||
|
||||
export function foreignKeyDisplayValueKey(value: CellValue | undefined): string | undefined {
|
||||
export function foreignKeyDisplayConfigIsUsable(config: ForeignKeyDisplayConfig, foreignKey: ForeignKeyInfo | undefined, currentSchema?: string): boolean {
|
||||
if (config.referenceMode === "manual") return true;
|
||||
return !!foreignKey && foreignKeyDisplayConfigMatches(config, foreignKey, currentSchema);
|
||||
}
|
||||
|
||||
export type ManualReferenceMetadataStatus = "loading" | "available" | "unavailable";
|
||||
export type ManualReferenceColumnValidation = "pending" | "valid" | "invalid" | "unavailable";
|
||||
|
||||
export function manualReferenceKeyColumns(columns: readonly ColumnInfo[], referenceKeyColumnNames: readonly string[]): ColumnInfo[] {
|
||||
const uniqueColumnNames = new Set(referenceKeyColumnNames);
|
||||
return columns.filter((column) => uniqueColumnNames.has(column.name));
|
||||
}
|
||||
|
||||
export function manualReferenceKeyColumnIsUnique(columns: readonly ColumnInfo[], referenceKeyColumnNames: readonly string[], columnName: string): boolean {
|
||||
return !!columnName && manualReferenceKeyColumns(columns, referenceKeyColumnNames).some((column) => column.name === columnName);
|
||||
}
|
||||
|
||||
export function manualReferenceColumnValidation(columns: readonly ColumnInfo[], referenceKeyColumnNames: readonly string[], columnName: string, metadataStatus: ManualReferenceMetadataStatus): ManualReferenceColumnValidation {
|
||||
if (metadataStatus === "loading") return "pending";
|
||||
if (metadataStatus === "unavailable") return "unavailable";
|
||||
return manualReferenceKeyColumnIsUnique(columns, referenceKeyColumnNames, columnName) ? "valid" : "invalid";
|
||||
}
|
||||
|
||||
export function reconcileManualReferenceColumn(currentColumn: string, referenceColumns: readonly ColumnInfo[], metadataStatus: ManualReferenceMetadataStatus): string {
|
||||
if (currentColumn || metadataStatus !== "available") return currentColumn;
|
||||
return referenceColumns[0]?.name ?? "";
|
||||
}
|
||||
|
||||
function canonicalNumericReferenceValue(value: string | number): string | undefined {
|
||||
const text = typeof value === "number" ? (Number.isFinite(value) ? String(value) : "") : value.trim();
|
||||
const match = /^([+-]?)(?:(\d+)(?:\.(\d*))?|\.(\d+))(?:[eE]([+-]?\d+))?$/.exec(text);
|
||||
if (!match) return undefined;
|
||||
const integerDigits = match[2] ?? "";
|
||||
const fractionalDigits = match[3] ?? match[4] ?? "";
|
||||
const explicitExponent = Number(match[5] ?? 0);
|
||||
if (!Number.isSafeInteger(explicitExponent)) return undefined;
|
||||
let digits = `${integerDigits}${fractionalDigits}`;
|
||||
const firstNonZero = digits.search(/[1-9]/);
|
||||
if (firstNonZero < 0) return "0";
|
||||
let decimalPosition = integerDigits.length + explicitExponent - firstNonZero;
|
||||
digits = digits.slice(firstNonZero).replace(/0+$/, "");
|
||||
decimalPosition -= digits.length;
|
||||
return `${match[1] === "-" ? "-" : ""}${digits}e${decimalPosition}`;
|
||||
}
|
||||
|
||||
export function foreignKeyDisplayValueKey(value: CellValue | undefined, keyDataType?: string): string | undefined {
|
||||
if (value === null || value === undefined || typeof value === "object") return undefined;
|
||||
if (isNumericColumnType(keyDataType) && (typeof value === "number" || typeof value === "string")) {
|
||||
const numericValue = canonicalNumericReferenceValue(value);
|
||||
if (numericValue !== undefined) return `numeric\u0000${numericValue}`;
|
||||
}
|
||||
return `${typeof value}\u0000${String(value)}`;
|
||||
}
|
||||
|
||||
export function foreignKeyDisplayLookupRequestKey(options: { connectionId: string; database?: string; catalog?: string; schema?: string; table: string; refColumn: string; displayColumn: string; values: readonly CellValue[] }): string {
|
||||
const valueKeys = options.values.map((value) => foreignKeyDisplayValueKey(value) ?? "").sort();
|
||||
return JSON.stringify(["lookup", options.connectionId, options.database ?? "", options.catalog ?? "", options.schema ?? "", options.table, options.refColumn, options.displayColumn, valueKeys]);
|
||||
export function foreignKeyDisplayLookupRequestKey(options: { connectionId: string; database?: string; catalog?: string; schema?: string; table: string; refColumn: string; displayColumn: string; keyDataType?: string; filter?: ForeignKeyDisplayFilterConfig; values: readonly CellValue[] }): string {
|
||||
const valueKeys = options.values.map((value) => foreignKeyDisplayValueKey(value, options.keyDataType) ?? "").sort();
|
||||
return JSON.stringify(["lookup", options.connectionId, options.database ?? "", options.catalog ?? "", options.schema ?? "", options.table, options.refColumn, options.displayColumn, options.keyDataType ?? "", options.filter ?? null, valueKeys]);
|
||||
}
|
||||
|
||||
export function collectForeignKeyDisplayValues(rows: QueryResult["rows"], columnIndex: number, maxValues = FOREIGN_KEY_DISPLAY_MAX_VALUES): CellValue[] {
|
||||
export function collectForeignKeyDisplayValues(rows: QueryResult["rows"], columnIndex: number, keyDataType?: string, maxValues = FOREIGN_KEY_DISPLAY_MAX_VALUES): CellValue[] {
|
||||
const values: CellValue[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const row of rows) {
|
||||
const value = row[columnIndex] as CellValue | undefined;
|
||||
const key = foreignKeyDisplayValueKey(value);
|
||||
const key = foreignKeyDisplayValueKey(value, keyDataType);
|
||||
if (!key || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
values.push(value!);
|
||||
@@ -191,13 +241,13 @@ export function splitForeignKeyDisplayValues(values: readonly CellValue[], batch
|
||||
return batches;
|
||||
}
|
||||
|
||||
export function foreignKeyDisplayMapFromResult(result: QueryResult, keyColumn = result.columns[0], displayColumn = result.columns[1]): Map<string, string> {
|
||||
export function foreignKeyDisplayMapFromResult(result: QueryResult, keyColumn = result.columns[0], displayColumn = result.columns[1], keyDataType?: string): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
const keyIndex = result.columns.findIndex((column) => column.toLowerCase() === keyColumn?.toLowerCase());
|
||||
const displayIndex = result.columns.findIndex((column) => column.toLowerCase() === displayColumn?.toLowerCase());
|
||||
const keyIndex = result.columns.findIndex((column) => column === keyColumn);
|
||||
const displayIndex = result.columns.findIndex((column) => column === displayColumn);
|
||||
if (keyIndex < 0 || displayIndex < 0) return map;
|
||||
for (const row of result.rows) {
|
||||
const key = foreignKeyDisplayValueKey(row[keyIndex] as CellValue | undefined);
|
||||
const key = foreignKeyDisplayValueKey(row[keyIndex] as CellValue | undefined, keyDataType);
|
||||
const labelValue = row[displayIndex] as CellValue | undefined;
|
||||
if (!key || labelValue === null || labelValue === undefined || map.has(key)) continue;
|
||||
map.set(key, displayCellValue(labelValue));
|
||||
@@ -205,9 +255,9 @@ export function foreignKeyDisplayMapFromResult(result: QueryResult, keyColumn =
|
||||
return map;
|
||||
}
|
||||
|
||||
export function formatForeignKeyDisplayValue(value: CellValue, labels: ReadonlyMap<string, string> | undefined): string {
|
||||
export function formatForeignKeyDisplayValue(value: CellValue, labels: ReadonlyMap<string, string> | undefined, keyDataType?: string): string {
|
||||
const raw = displayCellValue(value);
|
||||
const key = foreignKeyDisplayValueKey(value);
|
||||
const key = foreignKeyDisplayValueKey(value, keyDataType);
|
||||
const label = key ? labels?.get(key) : undefined;
|
||||
if (label === undefined || label === raw || !label.trim()) return raw;
|
||||
return `${raw} (${label})`;
|
||||
|
||||
Reference in new issue
Block a user