fix(iotdb): expose Tree Time in data grid filters

Closes #6434
This commit is contained in:
t8y2 committed 2026-08-23 09:56:15 +00:00
1 parent 493e18755d
commit d616e6b1a8
3 files changed
+178 -8

No files matched your search

@@ -1,5 +1,16 @@
import { describe, expect, it } from "vitest";
import { dataGridConditionColumnOptions, dataGridConditionIdentifierQuote } from "@/lib/dataGrid/dataGridConditionCompletion";
import { dataGridConditionColumnOptions, dataGridConditionIdentifierQuote, dataGridFilterColumns } from "@/lib/dataGrid/dataGridConditionCompletion";
function column(name: string, dataType: string) {
return {
name,
data_type: dataType,
is_nullable: true,
column_default: null,
is_primary_key: false,
extra: null,
};
}
describe("dataGridConditionColumnOptions", () => {
it("reuses PostgreSQL completion quoting while preserving display metadata", () => {
@@ -26,3 +37,108 @@ describe("dataGridConditionColumnOptions", () => {
expect(dataGridConditionIdentifierQuote("kingbase", "`")).toBe("`");
});
});
describe("dataGridFilterColumns", () => {
it.each(["ms", "us", "ns"])("adds metadata-less IoTDB Tree Time for %s precision", (precision) => {
const value = column("value", "DOUBLE");
expect(
dataGridFilterColumns({
databaseType: "iotdb",
context: "table-data",
urlParams: "time_zone=Asia%2FShanghai",
tableColumns: [value],
resultColumns: ["Time", "value"],
resultColumnTypes: [`TIMESTAMP(${precision})`, "DOUBLE"],
}),
).toEqual([{ name: "Time" }, { name: "value", columnInfo: value }]);
});
it("lets URL params override the same connection-string dialect key", () => {
const value = column("value", "DOUBLE");
expect(
dataGridFilterColumns({
databaseType: "iotdb",
context: "table-data",
connectionString: "jdbc:iotdb://localhost:6667/root.db?sql_dialect=table",
urlParams: "sql_dialect=tree",
tableColumns: [value],
resultColumns: ["Time", "value"],
resultColumnTypes: ["TIMESTAMP(ms)", "DOUBLE"],
}),
).toEqual([{ name: "Time" }, { name: "value", columnInfo: value }]);
});
it("keeps explicit Tree Time metadata exactly once", () => {
const time = column("Time", "TIMESTAMP");
const value = column("value", "DOUBLE");
expect(
dataGridFilterColumns({
databaseType: "iotdb",
context: "table-data",
urlParams: "sql_dialect=tree",
tableColumns: [time, value],
resultColumns: ["Time", "value"],
resultColumnTypes: ["TIMESTAMP(ms)", "DOUBLE"],
}),
).toEqual([
{ name: "Time", columnInfo: time },
{ name: "value", columnInfo: value },
]);
});
it.each([{ urlParams: "sql_dialect=table" }, { urlParams: "DIALECT=TABLE" }, { connectionString: "jdbc:iotdb://localhost:6667/root.db?sql_dialect=table" }] as const)("does not synthesize Time for IoTDB Table dialect ($urlParams$connectionString)", (connection) => {
const value = column("value", "DOUBLE");
expect(
dataGridFilterColumns({
databaseType: "iotdb",
context: "table-data",
...connection,
tableColumns: [value],
resultColumns: ["Time", "value"],
resultColumnTypes: ["TIMESTAMP(ms)", "DOUBLE"],
}),
).toEqual([{ name: "value", columnInfo: value }]);
});
it.each([
{ name: "query results", context: "results", databaseType: "iotdb", resultColumns: ["Time", "value"], resultColumnTypes: ["TIMESTAMP(ms)", "DOUBLE"] },
{ name: "non-IoTDB", context: "table-data", databaseType: "mysql", resultColumns: ["Time", "value"], resultColumnTypes: ["TIMESTAMP(ms)", "DOUBLE"] },
{ name: "non-leading Time", context: "table-data", databaseType: "iotdb", resultColumns: ["value", "Time"], resultColumnTypes: ["DOUBLE", "TIMESTAMP(ms)"] },
{ name: "non-canonical time", context: "table-data", databaseType: "iotdb", resultColumns: ["time", "value"], resultColumnTypes: ["TIMESTAMP(ms)", "DOUBLE"] },
{ name: "unqualified timestamp", context: "table-data", databaseType: "iotdb", resultColumns: ["Time", "value"], resultColumnTypes: ["TIMESTAMP", "DOUBLE"] },
] as const)("keeps physical candidates unchanged for $name", ({ context, databaseType, resultColumns, resultColumnTypes }) => {
const value = column("value", "DOUBLE");
expect(
dataGridFilterColumns({
databaseType,
context,
tableColumns: [value],
resultColumns,
resultColumnTypes,
}),
).toEqual([{ name: "value", columnInfo: value }]);
});
it("does not duplicate a case-variant metadata Time column", () => {
const time = column("time", "TIMESTAMP");
const value = column("value", "DOUBLE");
expect(
dataGridFilterColumns({
databaseType: "iotdb",
context: "table-data",
tableColumns: [time, value],
resultColumns: ["Time", "value"],
resultColumnTypes: ["TIMESTAMP(ns)", "DOUBLE"],
}),
).toEqual([
{ name: "time", columnInfo: time },
{ name: "value", columnInfo: value },
]);
});
});
@@ -2,7 +2,51 @@ import type { DataGridConditionColumnOption } from "@/composables/useDataGridCon
import { codeMirrorSqlDialect } from "@/lib/database/jdbcDialect";
import { quoteSqlIdentifier } from "@/lib/sql/sqlCompletion";
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
import type { DatabaseType } from "@/types/database";
import type { ColumnInfo, DatabaseType } from "@/types/database";
export interface DataGridFilterColumn {
name: string;
columnInfo?: ColumnInfo;
}
export interface DataGridFilterColumnsOptions {
databaseType?: DatabaseType;
context?: "results" | "table-data";
urlParams?: string;
connectionString?: string;
tableColumns: readonly ColumnInfo[];
resultColumns: readonly string[];
resultColumnTypes?: readonly (string | null | undefined)[];
}
export function dataGridFilterColumns(options: DataGridFilterColumnsOptions): DataGridFilterColumn[] {
const physicalColumns = options.tableColumns.map((columnInfo) => ({ name: columnInfo.name, columnInfo }));
if (!usesImplicitIoTDBTreeTime(options)) return physicalColumns;
return [{ name: "Time" }, ...physicalColumns];
}
function usesImplicitIoTDBTreeTime(options: DataGridFilterColumnsOptions): boolean {
return (
options.databaseType === "iotdb" &&
options.context === "table-data" &&
iotdbSqlDialect(options.urlParams, options.connectionString) !== "table" &&
options.resultColumns[0] === "Time" &&
/^TIMESTAMP\((ms|us|ns)\)$/i.test(options.resultColumnTypes?.[0]?.trim() ?? "") &&
!options.tableColumns.some((column) => column.name.toLowerCase() === "time")
);
}
function iotdbSqlDialect(urlParams: string | undefined, connectionString: string | undefined): string | undefined {
const merged = new Map<string, string>();
addIoTDBConnectionParams(merged, connectionString?.split("?", 2)[1]?.split("#", 1)[0]);
addIoTDBConnectionParams(merged, urlParams);
return (merged.get("sql_dialect") || merged.get("dialect"))?.trim().toLowerCase();
}
function addIoTDBConnectionParams(target: Map<string, string>, raw: string | undefined) {
const params = new URLSearchParams((raw ?? "").trim().replace(/^\?/, ""));
for (const [key, value] of params) target.set(key.toLowerCase(), value);
}
export function dataGridConditionColumnOptions(columns: readonly DataGridConditionColumnOption[], databaseType?: DatabaseType): DataGridConditionColumnOption[] {
const dialect = codeMirrorSqlDialect(databaseType);