diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 42865256ff..403a39cbf0 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -20,7 +20,7 @@ import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc, import { currentStatementFrameRangeTo } from "@/lib/sql/currentStatementFrame"; import { expandToSqlStatementWindow } from "@/lib/sql/insertValueHints"; import { insertValueHintColumnNames } from "@/lib/sql/insertValueHintColumns"; -import { canFormatSqlForDatabaseType, formatSqlForEditing, compressSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter"; +import { canFormatSqlForDatabaseType, formatSqlForDisplay, formatSqlForEditing, compressSqlText, sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter"; import { detectAndFormatStructured } from "@/lib/sql/autoFormat"; import { enabledSqlParameterSyntaxes, resolveSqlVariableSyntaxToggles } from "@/lib/sql/sqlVariableSyntax"; import { blankLineDeletionChanges, replaceSelectedEditorText } from "@/lib/editor/queryEditorTextEdits"; @@ -2825,7 +2825,13 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number) const { ddl } = await loadObjectDdl(objectMetadataRequest); const rawDdl = ddlForHoverPreview(ddl); if (rawDdl && rawDdl.trim()) { - sqlContent = reformatHoverDdl(rawDdl, quoteQualifiedName(hoverQualifiedName)); + // A view's display DDL wraps the raw (often single-line) view source + // in `CREATE ... VIEW ... AS`; the table-oriented reformatter cannot + // lay out a SELECT body, so views reuse the shared display formatter + // (the same one the sidebar/object-source viewers use). Tables keep + // the aligned column layout from reformatHoverDdl. + const isViewObject = objectMetadataRequest.objectType === "VIEW" || objectMetadataRequest.objectType === "MATERIALIZED_VIEW"; + sqlContent = isViewObject ? await formatSqlForDisplay(rawDdl, props.formatDialect ?? sqlFormatDialectForDbType(props.databaseType), settingsStore.editorSettings.sqlFormatter) : reformatHoverDdl(rawDdl, quoteQualifiedName(hoverQualifiedName)); } } catch (error) { console.warn(`[DBX] Failed to load table DDL for ${hoverDatabase}.${hoverSchema}.${table.name}:`, error); diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts index d992682cf5..e3901bdd41 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts @@ -60,9 +60,63 @@ describe("sqlFormatter", () => { expect(sqlFormatDialectForDbType("duckdb")).toBe("duckdb"); }); - it("maps only Oracle to the PL/SQL formatter dialect", () => { + it("maps Oracle and OceanBase Oracle mode to the PL/SQL formatter dialect", () => { expect(sqlFormatDialectForDbType("oracle")).toBe("oracle"); - expect(sqlFormatDialectForDbType("oceanbase-oracle")).toBe("generic"); + // OceanBase Oracle mode speaks Oracle SQL — issue #7540: it must reuse the + // Oracle dialect so view DDL previews are formatted instead of one line. + expect(sqlFormatDialectForDbType("oceanbase-oracle")).toBe("oracle"); + }); + + it("formats an OceanBase Oracle single-line view DDL into readable multi-line SQL (issue #7540)", async () => { + const singleLine = `CREATE OR REPLACE VIEW "APP"."ACTIVE_USERS" AS SELECT ID, NAME FROM USERS WHERE STATUS = 'ACTIVE'`; + const formatted = await formatSqlForDisplay(singleLine, sqlFormatDialectForDbType("oceanbase-oracle")); + + expect(formatted).not.toBe(singleLine); + expect(formatted.split("\n").length).toBeGreaterThan(1); + expect(formatted).toContain("CREATE OR REPLACE VIEW"); + expect(formatted).toMatch(/\bSELECT\b/); + expect(formatted).toMatch(/\bFROM\b/); + expect(formatted).toMatch(/\bWHERE\b/); + // String literals and quoted identifiers must survive formatting untouched. + expect(formatted).toContain("'ACTIVE'"); + expect(formatted).toContain('"ACTIVE_USERS"'); + }); + + it("formats a bare OceanBase Oracle view source wrapped as CREATE VIEW (issue #7540)", async () => { + // Mirrors the backend build_view_ddl_sql output for a fallback ALL_VIEWS.TEXT + // body: `CREATE VIEW AS `. + const wrapped = `CREATE VIEW "APP"."ACTIVE_USERS" AS +SELECT ID, NAME FROM USERS WHERE STATUS = 'ACTIVE';`; + const formatted = await formatSqlForDisplay(wrapped, sqlFormatDialectForDbType("oceanbase-oracle")); + + expect(formatted.split("\n").length).toBeGreaterThan(2); + expect(formatted).toMatch(/\bSELECT\b/); + expect(formatted).toMatch(/\bFROM\b/); + expect(formatted).toMatch(/\bWHERE\b/); + expect(formatted).toContain("'ACTIVE'"); + }); + + it("does not split string literals when formatting an OceanBase Oracle view (issue #7540)", async () => { + const singleLine = `CREATE OR REPLACE VIEW "APP"."V" AS SELECT 'SELECT X FROM Y' AS TXT FROM DUAL`; + const formatted = await formatSqlForDisplay(singleLine, sqlFormatDialectForDbType("oceanbase-oracle")); + + expect(formatted).toContain("'SELECT X FROM Y'"); + }); + + it("leaves an already multi-line OceanBase Oracle view DDL semantically intact (issue #7540)", async () => { + const multiLine = `CREATE OR REPLACE VIEW "APP"."ACTIVE_USERS" AS +SELECT + ID, + NAME +FROM USERS +WHERE STATUS = 'ACTIVE';`; + const formatted = await formatSqlForDisplay(multiLine, sqlFormatDialectForDbType("oceanbase-oracle")); + + expect(formatted).toContain('"ACTIVE_USERS"'); + expect(formatted).toContain("'ACTIVE'"); + expect(formatted).toMatch(/\bID\b/); + expect(formatted).toMatch(/\bNAME\b/); + expect(formatted).toMatch(/\bWHERE\b/); }); it("keeps issue #7138 Oracle hierarchy clauses intact", async () => { diff --git a/apps/desktop/src/lib/sql/sqlFormatter.ts b/apps/desktop/src/lib/sql/sqlFormatter.ts index 82b88782bd..f66d4f2c21 100644 --- a/apps/desktop/src/lib/sql/sqlFormatter.ts +++ b/apps/desktop/src/lib/sql/sqlFormatter.ts @@ -77,6 +77,8 @@ export function sqlFormatDialectForDbType(dbType: string | null | undefined): Sq case "sqlserver": return "sqlserver"; case "oracle": + // OceanBase Oracle mode speaks Oracle SQL, so it reuses the PL/SQL grammar. + case "oceanbase-oracle": return "oracle"; case "clickhouse": return "clickhouse";