fix(sql): quote postgres reserved identifiers

This commit is contained in:
vrustx 2026-07-15 19:37:00 +08:00 committed by GitHub
parent 3b54fa56df
commit cc340605b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 141 additions and 7 deletions

View File

@ -71,7 +71,7 @@ import { isTableDataEditable } from "@/lib/table/tableEditing";
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
import { dataTabExecutionDatabase } from "@/lib/table/dataTabExecutionDatabase";
import { formatShortcut } from "@/lib/editor/shortcutRegistry";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { codeMirrorSqlDialect, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { chartableColumnIndexes } from "@/lib/dataGrid/chartData";
import { elasticsearchJsonResponseForResult } from "@/lib/elasticsearch/elasticsearchJsonResponse";
import * as api from "@/lib/backend/api";
@ -268,11 +268,7 @@ const activeTabDimension = computed(() => {
const activeSqlFormatDialect = computed<SqlFormatDialect>(() => sqlFormatDialectForDbType(activeEffectiveDatabaseType.value));
const editorDialect = computed<"mysql" | "postgres" | "sqlserver">(() => {
if (activeEffectiveDatabaseType.value === "postgres" || activeEffectiveDatabaseType.value === "kwdb") return "postgres";
if (activeEffectiveDatabaseType.value === "sqlserver") return "sqlserver";
return "mysql";
});
const editorDialect = computed<"mysql" | "postgres" | "sqlserver">(() => codeMirrorSqlDialect(activeEffectiveDatabaseType.value));
const shortcutModifier = computed(() => (navigator.platform.toLowerCase().includes("mac") ? "Cmd" : "Ctrl"));

View File

@ -2763,7 +2763,115 @@ function requiresPostgresIdentifierQuote(identifier: string): boolean {
return POSTGRES_IDENTIFIER_KEYWORDS.has(identifier);
}
const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keyword.toLowerCase()).concat(["current_user", "session_user", "user"]));
// PostgreSQL reserved_keyword + type_func_name_keyword categories (pg_get_keywords()
// catcodes R and T) — identifiers matching these cannot appear as a bare column
// reference and must be quoted. SQL_KEYWORDS alone misses most of them because it is
// a completion phrase list ("ORDER BY", not "order").
const POSTGRES_RESERVED_KEYWORDS = [
"all",
"analyse",
"analyze",
"and",
"any",
"array",
"as",
"asc",
"asymmetric",
"authorization",
"binary",
"both",
"case",
"cast",
"check",
"collate",
"collation",
"column",
"concurrently",
"constraint",
"create",
"cross",
"current_catalog",
"current_date",
"current_role",
"current_schema",
"current_time",
"current_timestamp",
"current_user",
"default",
"deferrable",
"desc",
"distinct",
"do",
"else",
"end",
"except",
"false",
"fetch",
"for",
"foreign",
"freeze",
"from",
"full",
"grant",
"group",
"having",
"ilike",
"in",
"initially",
"inner",
"intersect",
"into",
"is",
"isnull",
"join",
"lateral",
"leading",
"left",
"like",
"limit",
"localtime",
"localtimestamp",
"natural",
"not",
"notnull",
"null",
"offset",
"on",
"only",
"or",
"order",
"outer",
"overlaps",
"placing",
"primary",
"references",
"returning",
"right",
"select",
"session_user",
"similar",
"some",
"symmetric",
"system_user",
"table",
"tablesample",
"then",
"to",
"trailing",
"true",
"union",
"unique",
"user",
"using",
"variadic",
"verbose",
"when",
"where",
"window",
"with",
];
const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keyword.toLowerCase()).concat(POSTGRES_RESERVED_KEYWORDS));
function buildTableItems(
context: Pick<SqlCompletionContext, "prefix" | "qualifier">,

View File

@ -344,6 +344,36 @@ test("leaves safe PostgreSQL column identifiers unquoted when completion inserts
assert.equal(column?.apply, "article");
});
test("quotes PostgreSQL reserved-word column identifiers when completion inserts them", () => {
// Reserved words absent from the completion keyword phrase list (which has
// "ORDER BY" but not "order") must still be quoted as column references.
const reservedColumns = ["order", "do", "returning", "ilike", "window", "true"];
const reservedColumnsByTable = new Map<string, SqlCompletionColumn[]>([["public.bookings", reservedColumns.map((name) => ({ name, table: "bookings", schema: "public", dataType: "text" }))]]);
const sql = "select from bookings";
const items = buildSqlCompletionItems(sql, "select ".length, {
tables: [{ name: "bookings", schema: "public", type: "table" }],
columnsByTable: reservedColumnsByTable,
dialect: "postgres",
});
for (const name of reservedColumns) {
const column = items.find((item) => item.type === "column" && item.label === name);
assert.equal(column?.apply, `"${name}"`, `expected reserved column "${name}" to be quoted`);
}
});
test("quotes PostgreSQL reserved-word table identifiers when completion inserts them", () => {
const sql = "select * from ord";
const items = buildSqlCompletionItems(sql, sql.length, {
tables: [{ name: "order", schema: "public", type: "table" }],
columnsByTable: new Map(),
dialect: "postgres",
});
const table = items.find((item) => item.type === "table" && item.label === "order");
assert.equal(table?.apply, '"order"');
});
test("suggests matching table names after FROM", () => {
const sql = "select * from us";
const items = buildSqlCompletionItems(sql, sql.length, {