diff --git a/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts b/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts index 023f5de0ce..ab9e4b547b 100644 --- a/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts +++ b/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts @@ -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); + }); }); diff --git a/apps/desktop/src/lib/sql/ddlDisplay.ts b/apps/desktop/src/lib/sql/ddlDisplay.ts index 5fc28286fe..d400beec95 100644 --- a/apps/desktop/src/lib/sql/ddlDisplay.ts +++ b/apps/desktop/src/lib/sql/ddlDisplay.ts @@ -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: diff --git a/apps/desktop/src/lib/sql/sqlIdentifier.ts b/apps/desktop/src/lib/sql/sqlIdentifier.ts index 808ecfe4c0..9df82fd4aa 100644 --- a/apps/desktop/src/lib/sql/sqlIdentifier.ts +++ b/apps/desktop/src/lib/sql/sqlIdentifier.ts @@ -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();