feat(grid): inherit physical column formatting in query results

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-01 11:19:46 +08:00
1 parent 8be503bf40
commit c7cea5a047
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>;