feat(grid): inherit physical column formatting in query results
This commit is contained in:
19 files changed
+329
-53
No files matched your search
@@ -1,5 +1,86 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyColumnFormatter, defaultIoTDBTimestampFormatter, formatIoTDBTimestampEditorValue, normalizeColumnFormatter, normalizeSupportedDateTimePattern, parseIoTDBTimestampEditorValue } from "@/lib/dataGrid/columnFormatter";
|
||||
import { applyColumnFormatter, columnFormatterKeys, defaultIoTDBTimestampFormatter, formatIoTDBTimestampEditorValue, normalizeColumnFormatter, normalizeSupportedDateTimePattern, parseIoTDBTimestampEditorValue } from "@/lib/dataGrid/columnFormatter";
|
||||
|
||||
describe("columnFormatterKeys", () => {
|
||||
// MySQL query metadata mirrors the database into the schema slot, while the
|
||||
// table view has always keyed formatters with an empty schema.
|
||||
const mysqlQueryColumn = {
|
||||
connectionId: "conn1",
|
||||
database: "app",
|
||||
databaseType: "mysql",
|
||||
resultColumn: "created_at",
|
||||
displaySource: { database: "app", schema: "app", tableName: "users", sourceColumn: "created_at" },
|
||||
};
|
||||
const mysqlTableColumn = {
|
||||
connectionId: "conn1",
|
||||
database: "app",
|
||||
databaseType: "mysql",
|
||||
resultColumn: "created_at",
|
||||
tableMeta: { database: "app", tableName: "users" },
|
||||
};
|
||||
|
||||
it("writes MySQL query-result saves under the table-view empty-schema key", () => {
|
||||
const keys = columnFormatterKeys(mysqlQueryColumn);
|
||||
expect(keys[0]).toBe("conn1::app::::users::created_at");
|
||||
// The mirrored spelling stays readable so older saves keep applying.
|
||||
expect(keys).toContain("conn1::app::app::users::created_at");
|
||||
});
|
||||
|
||||
it("resolves a MySQL formatter saved from a query result in the table view", () => {
|
||||
const queryKeys = columnFormatterKeys(mysqlQueryColumn);
|
||||
const tableKeys = columnFormatterKeys(mysqlTableColumn);
|
||||
// A save writes keys[0]; the table-view read list must include that key.
|
||||
expect(tableKeys).toContain(queryKeys[0]!);
|
||||
expect(tableKeys[0]).toBe(queryKeys[0]);
|
||||
// Clearing from the table view removes the legacy mirrored save too.
|
||||
expect(tableKeys).toContain("conn1::app::app::users::created_at");
|
||||
});
|
||||
|
||||
it("keeps the physical schema as the write key for schema-aware dialects", () => {
|
||||
const keys = columnFormatterKeys({
|
||||
connectionId: "conn1",
|
||||
database: "app",
|
||||
databaseType: "postgres",
|
||||
resultColumn: "created_at",
|
||||
displaySource: { database: "app", schema: "public", tableName: "users", sourceColumn: "created_at" },
|
||||
});
|
||||
expect(keys).toEqual(["conn1::app::public::users::created_at"]);
|
||||
});
|
||||
|
||||
it("returns no keys without a physical table identity", () => {
|
||||
expect(columnFormatterKeys({ connectionId: "conn1", database: "app", databaseType: "mysql", resultColumn: "total" })).toEqual([]);
|
||||
expect(
|
||||
columnFormatterKeys({
|
||||
connectionId: "conn1",
|
||||
database: "app",
|
||||
databaseType: "mysql",
|
||||
resultColumn: "total",
|
||||
// Cached legacy mapping without a resolvable physical table.
|
||||
displaySource: { sourceColumn: "total" },
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keys writes by the physical source column, not an alias", () => {
|
||||
const keys = columnFormatterKeys({ ...mysqlQueryColumn, resultColumn: "createdAt" });
|
||||
expect(keys[0]).toBe("conn1::app::::users::created_at");
|
||||
expect(keys.every((key) => key.endsWith("::created_at"))).toBe(true);
|
||||
});
|
||||
|
||||
it("canonicalizes writes for unresolved query columns backed by mirrored tab metadata", () => {
|
||||
// A query column without a per-ordinal source mapping still inherits the
|
||||
// tab's tableMeta, whose MySQL schema slot mirrors the database.
|
||||
const keys = columnFormatterKeys({
|
||||
connectionId: "conn1",
|
||||
database: "app",
|
||||
databaseType: "mysql",
|
||||
resultColumn: "created_at",
|
||||
tableMeta: { database: "app", schema: "app", tableName: "users" },
|
||||
});
|
||||
expect(keys[0]).toBe("conn1::app::::users::created_at");
|
||||
expect(keys).toContain("conn1::app::app::users::created_at");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeColumnFormatter", () => {
|
||||
it("preserves a structured manual reference filter", () => {
|
||||
|
||||
@@ -148,6 +148,89 @@ export function buildColumnFormatterKey(parts: ColumnFormatterKeyParts): string
|
||||
return [parts.connectionId, parts.database ?? "", parts.schema ?? "", parts.tableName, parts.column].join("::");
|
||||
}
|
||||
|
||||
export interface ColumnFormatterKeyContext {
|
||||
connectionId: string;
|
||||
database?: string;
|
||||
schema?: string;
|
||||
databaseType?: string;
|
||||
/** Result-column label (may be an alias). */
|
||||
resultColumn: string;
|
||||
/** Physical source column label when the grid already knows it. */
|
||||
sourceColumn?: string;
|
||||
/** Resolved physical source identity of a query result column, when known. */
|
||||
displaySource?: {
|
||||
database?: string;
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
sourceColumn?: string;
|
||||
};
|
||||
tableMeta?: {
|
||||
database?: string;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Ordered formatter-key candidates for one grid column. keys[0] is the write
|
||||
// key: always the physical source identity, never the current tab namespace,
|
||||
// so a same-named table in another database or schema must not inherit this
|
||||
// column's formatter. Remaining keys are read/clear fallbacks for configs
|
||||
// saved by other surfaces or older versions.
|
||||
//
|
||||
// MySQL-family query metadata mirrors the database into the schema slot while
|
||||
// table-view keys have always used an empty schema. Keep the shared
|
||||
// empty-schema key first so a formatter saved from a query result is written
|
||||
// to (and read by) the table view too, and retain the mirrored spelling only
|
||||
// as a read fallback for configs saved by older versions.
|
||||
export function columnFormatterKeys(context: ColumnFormatterKeyContext): string[] {
|
||||
const { connectionId, database, schema, databaseType, resultColumn } = context;
|
||||
const displaySource = context.displaySource;
|
||||
// A joined-result source key is only meaningful within one query. Do not
|
||||
// guess a formatter key for cached legacy mappings that lack a physical table.
|
||||
if (displaySource && !displaySource.tableName) return [];
|
||||
|
||||
const tableMeta = displaySource?.tableName
|
||||
? {
|
||||
database: displaySource.database,
|
||||
schema: displaySource.schema,
|
||||
tableName: displaySource.tableName,
|
||||
}
|
||||
: context.tableMeta;
|
||||
if (!tableMeta?.tableName) return [];
|
||||
|
||||
const sourceColumn = displaySource?.sourceColumn || context.sourceColumn || resultColumn;
|
||||
const keys = new Set<string>();
|
||||
const primaryDatabase = tableMeta.database?.trim() || database;
|
||||
const primarySchema = tableMeta.schema ?? schema;
|
||||
const isMysql = databaseType === "mysql";
|
||||
const mirroredSchema = isMysql ? primaryDatabase : undefined;
|
||||
const addKey = (keyDatabase: string | undefined, keySchema: string | undefined, column: string) => {
|
||||
keys.add(
|
||||
buildColumnFormatterKey({
|
||||
connectionId,
|
||||
database: keyDatabase,
|
||||
schema: keySchema,
|
||||
tableName: tableMeta.tableName,
|
||||
column,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (isMysql) addKey(primaryDatabase, "", sourceColumn);
|
||||
addKey(primaryDatabase, primarySchema, sourceColumn);
|
||||
if (displaySource?.tableName) return [...keys];
|
||||
|
||||
const databases = [...new Set([primaryDatabase, database, context.tableMeta?.database])];
|
||||
const schemas = [...new Set([primarySchema, schema, context.tableMeta?.schema, "", mirroredSchema])];
|
||||
const columns = [...new Set([sourceColumn, resultColumn])];
|
||||
for (const keyDatabase of databases) {
|
||||
for (const keySchema of schemas) {
|
||||
for (const column of columns) addKey(keyDatabase, keySchema, column);
|
||||
}
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig | undefined {
|
||||
if (!value || typeof value !== "object") return undefined;
|
||||
const config = value as Record<string, any>;
|
||||
|
||||
Reference in new issue
Block a user