From ac2dcc1b799dcdda509b8345168b5052e19970ba Mon Sep 17 00:00:00 2001 From: zipg Date: Mon, 14 Sep 2026 12:37:57 +0800 Subject: [PATCH] fix(structure): unquote plain Oracle identifiers when quoting off --- .../structure/TableStructureEditor.vue | 4 +-- .../src/lib/sql/__tests__/ddlDisplay.spec.ts | 17 +++++++++- apps/desktop/src/lib/sql/ddlDisplay.ts | 31 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue index 9159e9418c..6934860a14 100644 --- a/apps/desktop/src/components/structure/TableStructureEditor.vue +++ b/apps/desktop/src/components/structure/TableStructureEditor.vue @@ -27,7 +27,7 @@ import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect import { useToast } from "@/composables/useToast"; import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sql/sqlHighlighter"; import { joinSqlStatementsForScript } from "@/lib/sql/sqlBatchScript"; -import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; +import { formatGeneratedDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; import { splitSqlStatementRanges } from "@/lib/sql/sqlStatementRanges"; import { copyToClipboard } from "@/lib/common/clipboard"; import { formatSqlForDisplay, sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; @@ -1767,7 +1767,7 @@ async function refreshSqlPreview() { if (requestId !== sqlPreviewRequestId) return; const statements = [...result.statements, ...ownerResult.statements, ...(mysqlAutoIncrementStatement ? [mysqlAutoIncrementStatement] : [])]; // SQLite type-change apply regenerates this revision-checked plan, so its preview must stay byte-for-byte aligned. - pendingStatements.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers || hasSqliteTypeChange.value ? statements : statements.map((statement) => omitDdlIdentifierQuotes(statement, sqlFormatDialectForDbType(databaseType.value))); + pendingStatements.value = settingsStore.editorSettings.generateSqlQuoteIdentifiers !== false || hasSqliteTypeChange.value ? statements : statements.map((statement) => formatGeneratedDdlIdentifierQuotes(statement, sqlFormatDialectForDbType(databaseType.value), false)); warnings.value = [...result.warnings, ...ownerResult.warnings]; sqliteSchemaRevision.value = "schemaRevision" in result && typeof result.schemaRevision === "string" ? result.schemaRevision : undefined; } catch (e: any) { diff --git a/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts b/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts index ab9e4b547b..01876d35b3 100644 --- a/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts +++ b/apps/desktop/src/lib/sql/__tests__/ddlDisplay.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; +import { formatGeneratedDdlIdentifierQuotes, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; describe("omitDdlIdentifierQuotes", () => { it("removes safe MySQL identifier quotes without changing literals or comments", () => { @@ -40,4 +40,19 @@ describe("omitDdlIdentifierQuotes", () => { const ddl = 'CREATE TABLE "CamelCase" ("lowercase" INT, "WITH SPACE" INT, "ORDER" INT, "A$B" INT)'; expect(omitDdlIdentifierQuotes(ddl, "dameng")).toBe(ddl); }); + + it("folds safe Oracle identifiers when generated SQL quoting is disabled", () => { + const ddl = 'ALTER TABLE "SYSTEM"."TEST" RENAME COLUMN "ABCD" TO "AbCd";'; + expect(formatGeneratedDdlIdentifierQuotes(ddl, "oracle", false)).toBe("ALTER TABLE SYSTEM.TEST RENAME COLUMN ABCD TO ABCD;"); + }); + + it("keeps case-sensitive Dameng identifiers quoted when generated SQL quoting is disabled", () => { + const ddl = 'ALTER TABLE "user_login_log" ADD "status" INT; ALTER TABLE "CamelCase" ADD "order" INT;'; + expect(formatGeneratedDdlIdentifierQuotes(ddl, "dameng", false)).toBe(ddl); + }); + + it("preserves generated identifier quotes when the setting is enabled", () => { + const ddl = 'ALTER TABLE "SYSTEM"."TEST" RENAME COLUMN "ABCD" TO "AbCd";'; + expect(formatGeneratedDdlIdentifierQuotes(ddl, "oracle", true)).toBe(ddl); + }); }); diff --git a/apps/desktop/src/lib/sql/ddlDisplay.ts b/apps/desktop/src/lib/sql/ddlDisplay.ts index d400beec95..8c7dd57e8f 100644 --- a/apps/desktop/src/lib/sql/ddlDisplay.ts +++ b/apps/desktop/src/lib/sql/ddlDisplay.ts @@ -43,3 +43,34 @@ export function omitDdlIdentifierQuotes(sql: string, dialect: SqlFormatDialect): } return result; } + +const SIMPLE_ORACLE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$#]*$/; + +/** + * Applies the identifier-quoting preference to SQL generated by a structure + * editor. Oracle folds bare identifiers to uppercase, so safe quoted names + * are uppercased before their quotes are removed. Dameng keeps its stricter + * case-sensitive identifier rules through omitDdlIdentifierQuotes instead. + */ +export function formatGeneratedDdlIdentifierQuotes(sql: string, dialect: SqlFormatDialect, quoteIdentifiers: boolean): string { + if (quoteIdentifiers) return sql; + if (dialect !== "oracle") return omitDdlIdentifierQuotes(sql, dialect); + + const tokens = tokenizeSqlSemantic(sql, dialect); + const replacements: Array<{ start: number; end: number; value: string }> = []; + + for (const token of tokens) { + if (token.kind !== "quoted_identifier") continue; + const identifier = unquoteSqlSemanticIdentifier(token); + if (SIMPLE_ORACLE_IDENTIFIER.test(identifier) && !requiresOracleIdentifierQuote(identifier.toUpperCase())) { + replacements.push({ start: token.span.start, end: token.span.end, value: identifier.toUpperCase() }); + } + } + + let result = sql; + for (let index = replacements.length - 1; index >= 0; index -= 1) { + const replacement = replacements[index]!; + result = `${result.slice(0, replacement.start)}${replacement.value}${result.slice(replacement.end)}`; + } + return result; +}