From 4e926cf22a0bbe97c41c015dc66de7a7a8fbdf2d Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 21 Jul 2026 23:13:18 +0800 Subject: [PATCH] fix(postgres): quote data grid condition completions --- apps/desktop/src/components/grid/DataGrid.vue | 4 ++- .../useDataGridConditionEditor.spec.ts | 33 +++++++++++++++++++ .../composables/useDataGridConditionEditor.ts | 8 +++-- .../dataGridConditionCompletion.spec.ts | 20 +++++++++++ .../dataGrid/dataGridConditionCompletion.ts | 14 ++++++++ apps/desktop/src/lib/sql/sqlCompletion.ts | 2 +- 6 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/dataGrid/dataGridConditionCompletion.spec.ts create mode 100644 apps/desktop/src/lib/dataGrid/dataGridConditionCompletion.ts diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 225b90432..8f795f60c 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -205,6 +205,7 @@ import { reserveDataGridHeaderLine } from "@/lib/dataGrid/dataGridHeaderLayout"; import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities"; import { rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory"; import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; +import { dataGridConditionColumnOptions } from "@/lib/dataGrid/dataGridConditionCompletion"; import { isMacOS } from "@/lib/backend/platform"; import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog"; import { formatShortcut } from "@/lib/editor/shortcutRegistry"; @@ -567,6 +568,7 @@ const { searchText, deferredSearchText: deferredClientSearchText, overlayVisible const orderByInput = ref(props.initialOrderByInput ?? ""); const whereFilterInput = ref(props.initialWhereInput ?? ""); +const conditionColumns = computed(() => dataGridConditionColumnOptions(props.tableMeta?.columns ?? props.result.columns, resolvedDatabaseType.value)); const conditionHistoryScope = computed(() => ({ connectionId: props.connectionId, database: props.database, @@ -7482,7 +7484,7 @@ const gridContextMenuItems = computed(() => { v-model:order-by-input="orderByInput" v-model:filter-builder-open="filterBuilderOpen" :columns="props.tableMeta?.columns.map((column) => column.name) ?? props.result.columns" - :condition-columns="props.tableMeta?.columns ?? props.result.columns" + :condition-columns="conditionColumns" :history-scope="conditionHistoryScope" :can-use-where-search="canUseWhereSearch" :compact="compactDataGridToolbar" diff --git a/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts index 896400151..f92a25dc2 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts @@ -70,6 +70,39 @@ describe("useDataGridConditionEditor", () => { expect(editor.suggestions.value).toEqual([{ value: "customer_id = 1", kind: "history" }]); }); + it.each(["where", "orderBy"] as const)("displays raw PostgreSQL column names but inserts their quoted %s text", async (kind) => { + const value = ref(""); + const editor = useDataGridConditionEditor({ + kind, + value, + columns: [{ name: "OrderId", insertText: '"OrderId"', comment: "Mixed-case identifier" }], + historyScope: {}, + }); + + value.value = kind === "where" ? "status = Order" : "created_at DESC, Order"; + await nextTick(); + await vi.waitFor(() => expect(editor.suggestions.value).toEqual([{ value: "OrderId", insertText: '"OrderId"', kind: "column", comment: "Mixed-case identifier" }])); + + expect(editor.accept()).toBe(true); + expect(value.value).toBe(kind === "where" ? 'status = "OrderId"' : 'created_at DESC, "OrderId"'); + }); + + it("restores quoted history verbatim instead of quoting it again", () => { + const scope = { connectionId: "connection", database: "db", tableName: "orders" }; + rememberDataGridConditionHistory("orderBy", scope, '"OrderId" DESC'); + const value = ref(""); + const editor = useDataGridConditionEditor({ + kind: "orderBy", + value, + columns: [{ name: "OrderId", insertText: '"OrderId"' }], + historyScope: scope, + }); + + editor.openHistory(); + expect(editor.accept(0)).toBe(true); + expect(value.value).toBe('"OrderId" DESC'); + }); + it.each(["where", "orderBy"] as const)("normalizes %s comments from different metadata providers", async (kind) => { const value = ref(""); const editor = useDataGridConditionEditor({ diff --git a/apps/desktop/src/composables/useDataGridConditionEditor.ts b/apps/desktop/src/composables/useDataGridConditionEditor.ts index 32bd8e084..984eef4bc 100644 --- a/apps/desktop/src/composables/useDataGridConditionEditor.ts +++ b/apps/desktop/src/composables/useDataGridConditionEditor.ts @@ -5,6 +5,7 @@ export type DataGridConditionSuggestionKind = "column" | "history"; export interface DataGridConditionColumnSuggestion { name: string; + insertText?: string; comment?: string | null; } @@ -12,6 +13,7 @@ export type DataGridConditionColumnOption = string | DataGridConditionColumnSugg export interface DataGridConditionSuggestion { value: string; + insertText?: string; kind: DataGridConditionSuggestionKind; comment?: string; } @@ -97,7 +99,8 @@ export function useDataGridConditionEditor(options: UseDataGridConditionEditorOp if (!normalizedValue.startsWith(normalizedToken) || normalizedValue === normalizedToken || seen.has(value)) continue; seen.add(value); const comment = normalizedColumnComment(column); - suggestions.push({ value, kind: "column", ...(comment ? { comment } : {}) }); + const insertText = typeof column === "string" ? value : column.insertText; + suggestions.push({ value, kind: "column", ...(insertText !== undefined && insertText !== value ? { insertText } : {}), ...(comment ? { comment } : {}) }); } return suggestions; } @@ -177,7 +180,8 @@ export function useDataGridConditionEditor(options: UseDataGridConditionEditorOp const suggestion = suggestions.value[index]; if (!suggestion) return false; suppressNextValueSuggestion = true; - options.value.value = suggestion.kind === "history" ? suggestion.value : replaceActiveToken(options.kind, options.value.value, suggestion.value); + // History is already executable SQL; only fresh column suggestions apply dialect quoting. + options.value.value = suggestion.kind === "history" ? suggestion.value : replaceActiveToken(options.kind, options.value.value, suggestion.insertText ?? suggestion.value); dismiss(); return true; } diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridConditionCompletion.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridConditionCompletion.spec.ts new file mode 100644 index 000000000..d59fc4c99 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridConditionCompletion.spec.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "vitest"; +import { dataGridConditionColumnOptions } from "@/lib/dataGrid/dataGridConditionCompletion"; + +describe("dataGridConditionColumnOptions", () => { + it("reuses PostgreSQL completion quoting while preserving display metadata", () => { + expect(dataGridConditionColumnOptions([{ name: "OrderId", comment: "Mixed case" }, { name: "order", comment: null }, { name: "article", comment: "Safe identifier" }, { name: 'has"quote' }], "postgres")).toEqual([ + { name: "OrderId", comment: "Mixed case", insertText: '"OrderId"' }, + { name: "order", comment: null, insertText: '"order"' }, + { name: "article", comment: "Safe identifier", insertText: "article" }, + { name: 'has"quote', insertText: '"has""quote"' }, + ]); + }); + + it.each(["mysql", "sqlserver", "oracle"] as const)("keeps existing %s condition insertions unchanged", (databaseType) => { + expect(dataGridConditionColumnOptions(["OrderId", "order"], databaseType)).toEqual([ + { name: "OrderId", insertText: "OrderId" }, + { name: "order", insertText: "order" }, + ]); + }); +}); diff --git a/apps/desktop/src/lib/dataGrid/dataGridConditionCompletion.ts b/apps/desktop/src/lib/dataGrid/dataGridConditionCompletion.ts new file mode 100644 index 000000000..5c27a7e6a --- /dev/null +++ b/apps/desktop/src/lib/dataGrid/dataGridConditionCompletion.ts @@ -0,0 +1,14 @@ +import type { DataGridConditionColumnOption } from "@/composables/useDataGridConditionEditor"; +import { codeMirrorSqlDialect } from "@/lib/database/jdbcDialect"; +import { quoteSqlIdentifier } from "@/lib/sql/sqlCompletion"; +import type { DatabaseType } from "@/types/database"; + +export function dataGridConditionColumnOptions(columns: readonly DataGridConditionColumnOption[], databaseType?: DatabaseType): DataGridConditionColumnOption[] { + const dialect = codeMirrorSqlDialect(databaseType); + return columns.map((column) => { + const name = typeof column === "string" ? column : column.name; + const insertText = quoteSqlIdentifier(name, dialect); + const comment = typeof column === "string" ? undefined : column.comment; + return { name, insertText, ...(comment !== undefined ? { comment } : {}) }; + }); +} diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 24c360cfa..274d8c2f9 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -2777,7 +2777,7 @@ function unquoteIdentifier(value: string): string { return value; } -function quoteSqlIdentifier(identifier: string, dialect?: "mysql" | "postgres" | "sqlserver"): string { +export function quoteSqlIdentifier(identifier: string, dialect?: "mysql" | "postgres" | "sqlserver"): string { if (dialect !== "postgres" || !requiresPostgresIdentifierQuote(identifier)) return identifier; return `"${identifier.replaceAll('"', '""')}"`; }