From c2432611257fbbbcbbe79b2ce8dd543087784a7a Mon Sep 17 00:00:00 2001 From: Luis Araya <35828647+Totopo27@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:00:11 -0600 Subject: [PATCH] fix(oracle): format byte and code-point function variants as functions Closes #11251 --- apps/desktop/src/lib/sql/sqlFormatter.ts | 24 +++++++++++++++++++++++- packages/app-tests/sqlFormatter.test.ts | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/sql/sqlFormatter.ts b/apps/desktop/src/lib/sql/sqlFormatter.ts index edf3a20979..33a40927c4 100644 --- a/apps/desktop/src/lib/sql/sqlFormatter.ts +++ b/apps/desktop/src/lib/sql/sqlFormatter.ts @@ -28,6 +28,28 @@ function resolveClickHouseIdentifierSafeDialect(sqlFormatter: SqlFormatterModule return clickHouseIdentifierSafeDialect; } +// sql-formatter's plsql dialect only registers a minimal subset of character +// functions (e.g. SUBSTR, INSTR, LENGTH) and omits Oracle's byte/multibyte/code-point +// variants. Register them as reserved function names so functionCase normalizes them. +const ORACLE_EXTENDED_FUNCTION_NAMES = ["SUBSTRB", "SUBSTR2", "SUBSTR4", "SUBSTRC", "INSTRB", "INSTR2", "INSTR4", "INSTRC", "LENGTHB", "LENGTH2", "LENGTH4", "LENGTHC"]; +let oracleExtendedDialect: SqlFormatterModule["plsql"] | null = null; + +function resolveOracleExtendedDialect(sqlFormatter: SqlFormatterModule): SqlFormatterModule["plsql"] { + if (oracleExtendedDialect) return oracleExtendedDialect; + + const existingFunctions = new Set(sqlFormatter.plsql.tokenizerOptions.reservedFunctionNames); + const extraFunctions = ORACLE_EXTENDED_FUNCTION_NAMES.filter((name) => !existingFunctions.has(name)); + + oracleExtendedDialect = { + ...sqlFormatter.plsql, + tokenizerOptions: { + ...sqlFormatter.plsql.tokenizerOptions, + reservedFunctionNames: [...sqlFormatter.plsql.tokenizerOptions.reservedFunctionNames, ...extraFunctions], + }, + }; + return oracleExtendedDialect; +} + export function canFormatSqlForDatabaseType(dbType: string | null | undefined): boolean { return dbType !== "redis" && dbType !== "victoriametrics"; } @@ -367,7 +389,7 @@ export async function formatSqlText(sql: string, dialect: SqlFormatDialect = "ge }, } : options; - const resolvedDialect = dialect === "clickhouse" ? resolveClickHouseIdentifierSafeDialect(sqlFormatter) : undefined; + const resolvedDialect = dialect === "clickhouse" ? resolveClickHouseIdentifierSafeDialect(sqlFormatter) : dialect === "oracle" ? resolveOracleExtendedDialect(sqlFormatter) : undefined; const formatWithFallback = (input: string): string => { try { if (resolvedDialect) { diff --git a/packages/app-tests/sqlFormatter.test.ts b/packages/app-tests/sqlFormatter.test.ts index 18da440376..d319fafb2f 100644 --- a/packages/app-tests/sqlFormatter.test.ts +++ b/packages/app-tests/sqlFormatter.test.ts @@ -301,3 +301,21 @@ test("compressSqlText preserves unterminated block comments", () => { const sql = "DELETE FROM users /* unfinished"; assert.equal(compressSqlText(sql), sql); }); + +test("Oracle: formats byte and code-point function variants with functionCase", async () => { + const formattedLower = await formatSqlText("SELECT SUBSTR(a, 1), SUBSTRB(b, 1), INSTRB(c, 'x'), LENGTHB(d) FROM dual", "oracle", { + functionCase: "lower", + }); + assert.match(formattedLower, /\bsubstr\(/); + assert.match(formattedLower, /\bsubstrb\(/); + assert.match(formattedLower, /\binstrb\(/); + assert.match(formattedLower, /\blengthb\(/); + + const formattedUpper = await formatSqlText("select substr(a, 1), substrb(b, 1), instrb(c, 'x'), lengthb(d) from dual", "oracle", { + functionCase: "upper", + }); + assert.match(formattedUpper, /\bSUBSTR\(/); + assert.match(formattedUpper, /\bSUBSTRB\(/); + assert.match(formattedUpper, /\bINSTRB\(/); + assert.match(formattedUpper, /\bLENGTHB\(/); +});