fix(ddl): preserve safe Oracle and Dameng identifiers

This commit is contained in:
zipg authored and GitHub committed 2026-09-09 12:28:47 +08:00
1 parent 99c698f72e
commit ef1d7ae9c4
3 files changed
+385 -3

No files matched your search

+20
View File
@@ -20,4 +20,24 @@ describe("omitDdlIdentifierQuotes", () => {
const ddl = "CREATE TABLE [demo_table] ([order] int, [key] int, [user] nvarchar(50), [id] int)";
expect(omitDdlIdentifierQuotes(ddl, "sqlserver")).toBe("CREATE TABLE demo_table ([order] int, [key] int, [user] nvarchar(50), id int)");
});
it("removes quotes from ordinary uppercase Oracle identifiers", () => {
const ddl = 'CREATE TABLE "DBX_TEST"."PRODUCTS" ("ID" NUMBER(10), "SKU" VARCHAR2(32)) TABLESPACE "USERS";';
expect(omitDdlIdentifierQuotes(ddl, "oracle")).toBe("CREATE TABLE DBX_TEST.PRODUCTS (ID NUMBER(10), SKU VARCHAR2(32)) TABLESPACE USERS;");
});
it("keeps quotes required by Oracle case and naming rules", () => {
const ddl = 'CREATE TABLE "CamelCase" ("lowercase" NUMBER, "WITH SPACE" NUMBER, "ORDER" NUMBER)';
expect(omitDdlIdentifierQuotes(ddl, "oracle")).toBe(ddl);
});
it("removes quotes from ordinary uppercase Dameng identifiers", () => {
const ddl = 'CREATE TABLE "DBX_TEST"."PRODUCTS" ("ID" INT, "NAME" VARCHAR(128)) STORAGE (ON "MAIN", CLUSTERBTR)';
expect(omitDdlIdentifierQuotes(ddl, "dameng")).toBe("CREATE TABLE DBX_TEST.PRODUCTS (ID INT, NAME VARCHAR(128)) STORAGE (ON MAIN, CLUSTERBTR)");
});
it("keeps quotes required by Dameng case, naming, and reserved-word rules", () => {
const ddl = 'CREATE TABLE "CamelCase" ("lowercase" INT, "WITH SPACE" INT, "ORDER" INT, "A$B" INT)';
expect(omitDdlIdentifierQuotes(ddl, "dameng")).toBe(ddl);
});
});
+5 -3
View File
@@ -1,4 +1,4 @@
import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
import { requiresDamengIdentifierQuote, requiresMysqlIdentifierQuote, requiresOracleIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
import { tokenizeSqlSemantic, unquoteSqlSemanticIdentifier } from "@/lib/sql/semantic/tokens";
import type { SqlFormatDialect } from "@/lib/sql/sqlFormatter";
@@ -11,11 +11,13 @@ function canRenderUnquoted(identifier: string, dialect: SqlFormatDialect): boole
return !requiresMysqlIdentifierQuote(identifier);
case "postgres":
case "sqlite":
case "oracle":
case "duckdb":
case "dameng":
case "generic":
return !requiresPostgresIdentifierQuote(identifier);
case "dameng":
return !requiresDamengIdentifierQuote(identifier);
case "oracle":
return !requiresOracleIdentifierQuote(identifier);
case "sqlserver":
return SIMPLE_SQLSERVER_IDENTIFIER.test(identifier) && !requiresMysqlIdentifierQuote(identifier.toLowerCase());
default:
+360
View File
@@ -1,5 +1,7 @@
const SIMPLE_LOWER_IDENTIFIER = /^[a-z_][a-z0-9_$]*$/;
const SIMPLE_MYSQL_IDENTIFIER = /^[A-Za-z_$\u0080-\uFFFF][A-Za-z0-9_$\u0080-\uFFFF]*$/u;
const SIMPLE_DAMENG_IDENTIFIER = /^[A-Z_][A-Z0-9_]*$/;
const SIMPLE_ORACLE_IDENTIFIER = /^[A-Z][A-Z0-9_$#]*$/;
// PostgreSQL pg_get_keywords() categories R and T. GaussDB's PostgreSQL-
// compatible modes use the same identifier constraints for bare names.
@@ -181,6 +183,356 @@ const MYSQL_ONLY_RESERVED_IDENTIFIER_KEYWORDS = new Set([
"zerofill",
]);
// Oracle 11g SQL reserved words. Ordinary unquoted Oracle identifiers are
// uppercase-folded, so only uppercase names outside this set are safe to unwrap.
const ORACLE_RESERVED_IDENTIFIER_KEYWORDS = new Set([
"ACCESS",
"ADD",
"ALL",
"ALTER",
"AND",
"ANY",
"AS",
"ASC",
"AUDIT",
"BETWEEN",
"BY",
"CHAR",
"CHECK",
"CLUSTER",
"COLUMN",
"COMMENT",
"COMPRESS",
"CONNECT",
"CREATE",
"CURRENT",
"DATE",
"DECIMAL",
"DEFAULT",
"DELETE",
"DESC",
"DISTINCT",
"DROP",
"ELSE",
"EXCLUSIVE",
"EXISTS",
"FILE",
"FLOAT",
"FOR",
"FROM",
"GRANT",
"GROUP",
"HAVING",
"IDENTIFIED",
"IMMEDIATE",
"IN",
"INCREMENT",
"INDEX",
"INITIAL",
"INSERT",
"INTEGER",
"INTERSECT",
"INTO",
"IS",
"LEVEL",
"LIKE",
"LOCK",
"LONG",
"MAXEXTENTS",
"MINUS",
"MLSLABEL",
"MODE",
"MODIFY",
"NOAUDIT",
"NOCOMPRESS",
"NOT",
"NOWAIT",
"NULL",
"NUMBER",
"OF",
"OFFLINE",
"ON",
"ONLINE",
"OPTION",
"OR",
"ORDER",
"PCTFREE",
"PRIOR",
"PRIVILEGES",
"PUBLIC",
"RAW",
"RENAME",
"RESOURCE",
"REVOKE",
"ROW",
"ROWID",
"ROWNUM",
"ROWS",
"SELECT",
"SESSION",
"SET",
"SHARE",
"SIZE",
"SMALLINT",
"START",
"SUCCESSFUL",
"SYNONYM",
"SYSDATE",
"TABLE",
"THEN",
"TO",
"TRIGGER",
"UID",
"UNION",
"UNIQUE",
"UPDATE",
"USER",
"VALIDATE",
"VALUES",
"VARCHAR",
"VARCHAR2",
"VIEW",
"WHENEVER",
"WHERE",
"WITH",
]);
// V$RESERVED_WORDS rows marked RESERVED = Y across the supported DM8 test
// instances. DM's native identifier rules use uppercase ordinary names.
const DAMENG_RESERVED_IDENTIFIER_KEYWORDS = new Set([
"ABSOLUTE",
"ABSTRACT",
"ADD",
"ADMIN",
"ALL",
"ALTER",
"AND",
"ANY",
"ARRAY",
"ARRAYLEN",
"AS",
"ASC",
"ASSIGN",
"AUDIT",
"AUTHORIZATION",
"AUTO_INCREMENT",
"BEGIN",
"BETWEEN",
"BIGDATEDIFF",
"BINARY",
"BOOL",
"BOTH",
"BREAK",
"BSTRING",
"BY",
"BYTE",
"CALL",
"CASE",
"CAST",
"CATCH",
"CHAR",
"CHECK",
"CLASS",
"CLUSTER",
"CLUSTERBTR",
"COLLATION",
"COLLECT",
"COLUMN",
"COMMENT",
"COMMIT",
"COMMITWORK",
"CONNECT",
"CONNECT_BY_ROOT",
"CONST",
"CONSTRAINT",
"CONTAINS",
"CONTEXT",
"CONTINUE",
"CONVERT",
"CORRESPONDING",
"CREATE",
"CROSS",
"CRYPTO",
"CUBE",
"CURRENT",
"CURSOR",
"DATEADD",
"DATEDIFF",
"DATEPART",
"DECIMAL",
"DECLARE",
"DECODE",
"DEFAULT",
"DELETE",
"DESC",
"DISABLE",
"DISKSPACE",
"DISTINCT",
"DISTRIBUTED",
"DO",
"DOMAIN",
"DOUBLE",
"DROP",
"ELSE",
"ELSEIF",
"ELSIF",
"ENABLE",
"END",
"EQU",
"EXCHANGE",
"EXEC",
"EXECUTE",
"EXISTS",
"EXIT",
"EXPLAIN",
"EXTERN",
"EXTRACT",
"FETCH",
"FINAL",
"FINALLY",
"FIRST",
"FLASHBACK",
"FLOAT",
"FOR",
"FOREIGN",
"FROM",
"FULL",
"FULLY",
"FUNCTION",
"GET",
"GOTO",
"GRANT",
"GROUP",
"GROUPING",
"HAVING",
"IDENTITY",
"IF",
"IFNULL",
"IMMEDIATE",
"IN",
"INDEX",
"INLINE",
"INNER",
"INSERT",
"INT",
"INTERSECT",
"INTERVAL",
"INTO",
"IS",
"JOIN",
"KEEP",
"LEADING",
"LEFT",
"LEXER",
"LIKE",
"LIST",
"LNNVL",
"LOGIN",
"LOOP",
"MEMBER",
"MINUS",
"MULTISET",
"NATURAL",
"NEW",
"NEXT",
"NOCOPY",
"NOCYCLE",
"NOT",
"NULL",
"OBJECT",
"OF",
"ON",
"OR",
"ORDER",
"OUT",
"OVER",
"OVERLAY",
"OVERRIDE",
"PARTITION",
"PENDANT",
"PERCENT",
"PRIMARY",
"PRINT",
"PRIOR",
"PRIVATE",
"PRIVILEGES",
"PROCEDURE",
"PROTECTED",
"PUBLIC",
"RAISE",
"RECORD",
"REF",
"REFERENCE",
"REFERENCES",
"REFERENCING",
"RELATIVE",
"REPEAT",
"REPLICATE",
"RETURN",
"RETURNING",
"REVERSE",
"REVOKE",
"RIGHT",
"ROLLBACK",
"ROLLUP",
"ROW",
"ROWNUM",
"ROWS",
"SAVEPOINT",
"SBYTE",
"SCHEMA",
"SEALED",
"SECTION",
"SELECT",
"SET",
"SETS",
"SHORT",
"SIZEOF",
"SOME",
"STATIC",
"STRUCT",
"SUBPARTITION",
"SWITCH",
"SYNONYM",
"TABLE",
"THROW",
"TIMESTAMPADD",
"TIMESTAMPDIFF",
"TO",
"TOP",
"TRAILING",
"TRIGGER",
"TRIM",
"TRUNCATE",
"TRY",
"TYPEDEF",
"TYPEOF",
"UINT",
"ULONG",
"UNION",
"UNIQUE",
"UNTIL",
"UPDATE",
"USER",
"USHORT",
"USING",
"VALUES",
"VARRAY",
"VERIFY",
"VIEW",
"VIRTUAL",
"VOID",
"WHEN",
"WHENEVER",
"WHERE",
"WHILE",
"WITH",
"WITHIN",
"XMLAGG",
"XMLPARSE",
"XMLTABLE",
]);
export function isExplicitlyQuotedSqlIdentifier(value: string): boolean {
if (value.length < 2) return false;
return (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("`") && value.endsWith("`")) || (value.startsWith("[") && value.endsWith("]"));
@@ -197,6 +549,14 @@ export function requiresMysqlIdentifierQuote(identifier: string, additionalKeywo
return POSTGRES_RESERVED_IDENTIFIER_KEYWORDS.has(normalized) || additionalKeywords?.has(normalized) === true || MYSQL_ONLY_RESERVED_IDENTIFIER_KEYWORDS.has(normalized);
}
export function requiresOracleIdentifierQuote(identifier: string): boolean {
return !SIMPLE_ORACLE_IDENTIFIER.test(identifier) || ORACLE_RESERVED_IDENTIFIER_KEYWORDS.has(identifier);
}
export function requiresDamengIdentifierQuote(identifier: string): boolean {
return !SIMPLE_DAMENG_IDENTIFIER.test(identifier) || DAMENG_RESERVED_IDENTIFIER_KEYWORDS.has(identifier);
}
export function quoteGaussDbJdbcIdentifier(identifier: string, identifierQuote: string): string {
if (isExplicitlyQuotedSqlIdentifier(identifier)) return identifier;
const quote = identifierQuote.trim();