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

+16 -6
View File
@@ -320,7 +320,7 @@ import { mongoCollectionSupportsIndexes, supportsMongoIndexMutations } from "@/l
import { refreshLoadedMongoIndexes } from "@/lib/mongo/mongoIndexMetadata";
import { isProtectedMongoIndex, mongoDropAllIndexesPreview, mongoDropIndexFailureCount, mongoDropIndexPreview } from "@/lib/sidebar/mongoCollectionMutation";
import { runMongoMutation } from "@/lib/sidebar/runMongoSidebarMutation";
import { dataGridConditionColumnOptions, dataGridConditionIdentifierQuote } from "@/lib/dataGrid/dataGridConditionCompletion";
import { dataGridConditionColumnOptions, dataGridConditionIdentifierQuote, dataGridFilterColumns } from "@/lib/dataGrid/dataGridConditionCompletion";
import { isMacOS } from "@/lib/backend/platform";
import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog";
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
@@ -931,7 +931,18 @@ const whereFilterInput = ref(props.initialWhereInput ?? "");
const conditionInputRevision = ref(0);
const appliedConditionInputRevision = ref(0);
const queryControlError = ref("");
const conditionColumns = computed(() => dataGridConditionColumnOptions(props.tableMeta?.columns ?? props.result.columns, resolvedDatabaseType.value));
const filterBuilderColumns = computed(() =>
dataGridFilterColumns({
databaseType: resolvedDatabaseType.value,
context: props.context,
urlParams: resolvedConnectionConfig.value?.url_params,
connectionString: resolvedConnectionConfig.value?.connection_string,
tableColumns: props.tableMeta?.columns ?? [],
resultColumns: props.result.columns,
resultColumnTypes: props.result.column_types,
}),
);
const conditionColumns = computed(() => dataGridConditionColumnOptions(props.tableMeta ? filterBuilderColumns.value.map((column) => column.columnInfo ?? column.name) : props.result.columns, resolvedDatabaseType.value));
const conditionIdentifierQuote = computed(() => dataGridConditionIdentifierQuote(resolvedDatabaseType.value, connectionStore.connectionIdentifierQuote?.(props.connectionId)));
const conditionHistoryScope = computed(() => ({
connectionId: props.connectionId,
@@ -1000,16 +1011,15 @@ const allFilterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [
{ value: "is-not-null", labelKey: "grid.filterBuilderIsNotNull" },
];
const filterModeOptions = computed(() => allFilterModeOptions.filter((option) => filterModeIsSupportedForDatabase(option.value, resolvedDatabaseType.value)));
const filterBuilderColumns = computed(() => props.tableMeta?.columns ?? []);
const filterBuilderColumnOptions = computed(() => filterBuilderColumns.value.map((column) => column.name));
const structuredFilterCacheKey = computed(() => props.cacheKey || [props.connectionId ?? "", props.database ?? "", props.context ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? ""].join("\u0001"));
const structuredFilterScopeKey = computed(() => [props.connectionId ?? "", props.database ?? "", props.schema ?? "", props.context ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? "", filterBuilderColumnOptions.value.join("\0")].join("\u0001"));
const structuredFilterScopeKey = computed(() => [props.connectionId ?? "", props.database ?? "", props.schema ?? "", props.context ?? "", props.tableMeta?.schema ?? "", props.tableMeta?.tableName ?? "", props.tableMeta?.columns.map((column) => column.name).join("\0") ?? ""].join("\u0001"));
function isStructuredFilterRuleComplete(rule: StructuredFilterRule): boolean {
return filterModeIsSupportedForDatabase(rule.mode, resolvedDatabaseType.value) && filterModeHasCompleteValue(rule.mode, rule.rawValue, rule.rawEndValue);
}
async function buildStructuredFilterCondition(rule: StructuredFilterRule): Promise<string | undefined> {
const columnInfo = filterBuilderColumns.value.find((column) => column.name === rule.columnName);
const columnInfo = filterBuilderColumns.value.find((column) => column.name === rule.columnName)?.columnInfo;
const usesList = filterModeUsesList(rule.mode);
const usesRange = filterModeUsesRange(rule.mode);
return (
@@ -2081,7 +2091,7 @@ async function buildStructuredWhereFromRules(rules: StructuredFilterRule[]): Pro
if (!rule.columnName) return { rule, condition: null };
if (!filterModeIsSupportedForDatabase(rule.mode, resolvedDatabaseType.value)) return { rule, condition: null };
if (!filterModeHasCompleteValue(rule.mode, rule.rawValue, rule.rawEndValue)) return { rule, condition: null };
const columnInfo = filterBuilderColumns.value.find((column) => column.name === rule.columnName);
const columnInfo = filterBuilderColumns.value.find((column) => column.name === rule.columnName)?.columnInfo;
const usesList = filterModeUsesList(rule.mode);
const usesRange = filterModeUsesRange(rule.mode);
const options = {
@@ -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);