fix(postgres): quote data grid condition completions

This commit is contained in:
t8y2 2026-07-21 23:13:18 +08:00
parent ff4eac3782
commit 4e926cf22a
6 changed files with 77 additions and 4 deletions

View File

@ -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<ContextMenuItem[]>(() => {
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"

View File

@ -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({

View File

@ -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;
}

View File

@ -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" },
]);
});
});

View File

@ -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 } : {}) };
});
}

View File

@ -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('"', '""')}"`;
}