1 parent
493e18755d
commit
d616e6b1a8
3 files changed
+178
-8
No files matched your search
@@ -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 = {
|
||||
|
||||
+117
-1
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user