From e942f7dc5a64a2bf78bd5c2e192acec2f82ff7dc Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 31 Jul 2026 20:06:25 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(ddl):=20=E4=BF=9D=E7=95=99?= =?UTF-8?q?=20Oracle=20=E8=A7=86=E5=9B=BE=20DDL=20=E5=8E=9F=E5=A7=8B?= =?UTF-8?q?=E6=8E=92=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Oracle 视图与物化视图跳过展示格式化 - 补充预览及对象编辑原始排版回归测试 --- .../DefinitionViewer.object-edit.test.tsx | 13 +-- frontend/src/utils/ddlFormat.test.ts | 13 +-- frontend/src/utils/ddlFormat.ts | 105 ++---------------- 3 files changed, 14 insertions(+), 117 deletions(-) diff --git a/frontend/src/components/DefinitionViewer.object-edit.test.tsx b/frontend/src/components/DefinitionViewer.object-edit.test.tsx index 80c3a9ca..10fee935 100644 --- a/frontend/src/components/DefinitionViewer.object-edit.test.tsx +++ b/frontend/src/components/DefinitionViewer.object-edit.test.tsx @@ -148,11 +148,12 @@ describe('DefinitionViewer object edit entry', () => { expect(storeState.addTab.mock.calls[0][0].query).toContain('SELECT id, name FROM users;'); }); - it('formats a one-line Oracle catalog view definition for display and editing', async () => { + it('preserves Oracle view DDL formatting for display and editing', async () => { + const rawDDL = `CREATE OR REPLACE VIEW APP.V_RISK AS SELECT a.id,NVL(b.org_name,'-') AS org_name FROM org a LEFT JOIN org_info b ON b.org_id=a.id WHERE a.deleted_flag=0`; storeState.connections[0].config.type = 'oracle'; backendApp.DBShowCreateTable.mockResolvedValue({ success: true, - data: `CREATE OR REPLACE VIEW APP.V_RISK AS SELECT a.id,NVL(b.org_name,'-') AS org_name FROM org a LEFT JOIN org_info b ON b.org_id=a.id WHERE a.deleted_flag=0`, + data: rawDDL, }); let renderer: any; @@ -170,10 +171,7 @@ describe('DefinitionViewer object edit entry', () => { expect(backendApp.DBShowCreateTable).toHaveBeenCalledWith(expect.anything(), 'APP', 'APP.V_RISK'); expect(backendApp.DBQuery).not.toHaveBeenCalled(); const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join('')); - expect(editorText).toContain('CREATE OR REPLACE VIEW APP.V_RISK AS'); - expect(editorText).toContain('SELECT\n a.id,'); - expect(editorText).toContain('\nFROM\n org a'); - expect(editorText).toContain('\nWHERE\n a.deleted_flag = 0;'); + expect(editorText).toBe(`${rawDDL};`); const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0]; await act(async () => { @@ -181,8 +179,7 @@ describe('DefinitionViewer object edit entry', () => { }); const query = String(storeState.addTab.mock.calls[0][0].query || ''); - expect(query).toContain('SELECT\n a.id,'); - expect(query).toContain('\nWHERE\n a.deleted_flag = 0;'); + expect(query).toContain(`${rawDDL};`); }); it('adds CREATE OR REPLACE without duplicating view fragments returned without ddl prefix', async () => { diff --git a/frontend/src/utils/ddlFormat.test.ts b/frontend/src/utils/ddlFormat.test.ts index 0a3d3ef7..82bab8a3 100644 --- a/frontend/src/utils/ddlFormat.test.ts +++ b/frontend/src/utils/ddlFormat.test.ts @@ -34,18 +34,9 @@ COMMENT ON COLUMN "H2"."S_BUSI"."ID" IS '主键';`; expect(formatted).toContain(`COMMENT ON COLUMN "H2"."S_BUSI"."ID" IS '主键';`); }); - it('formats Oracle view columns and keeps the trailing read-only clause intact', () => { + it('preserves Oracle view DDL formatting', () => { const raw = `CREATE OR REPLACE FORCE EDITIONABLE VIEW "APP"."V_RISK" ("ID", "ORG_NAME", "PARENT_NAME", "LEVEL_NO") DEFAULT COLLATION "USING_NLS_COMP" AS SELECT a.id,NVL(b.org_name,'-') org_name,DECODE(a.parent_id,NULL,'ROOT',c.org_name) parent_name,LEVEL level_no FROM org a,org_info b,org_info c WHERE a.id=b.org_id(+) AND a.parent_id=c.org_id(+) START WITH a.parent_id IS NULL CONNECT BY PRIOR a.id=a.parent_id WITH READ ONLY;`; - const formatted = formatDdlForDisplay(raw, 'oracle'); - - expect(formatted).toContain(`VIEW "APP"."V_RISK" ( - "ID", - "ORG_NAME", - "PARENT_NAME", - "LEVEL_NO" -) DEFAULT COLLATION`); - expect(formatted).toContain('\nWITH READ ONLY;'); - expect(formatted).not.toContain('\nWITH\n READ ONLY;'); + expect(formatDdlForDisplay(raw, 'oracle')).toBe(raw); }); }); diff --git a/frontend/src/utils/ddlFormat.ts b/frontend/src/utils/ddlFormat.ts index f3a8fa15..dc7a597d 100644 --- a/frontend/src/utils/ddlFormat.ts +++ b/frontend/src/utils/ddlFormat.ts @@ -36,116 +36,25 @@ const resolveDdlFormatterLanguage = (dbType: string): SqlLanguage => { } }; -const findOracleViewColumnList = (sql: string): { start: number; end: number } | null => { - const viewMatch = /^\s*CREATE\b[\s\S]*?\bVIEW\b/i.exec(sql); - if (!viewMatch) { - return null; - } - - let inQuotedIdentifier = false; - let columnListStart = -1; - let parenthesisDepth = 0; - - for (let index = viewMatch[0].length; index < sql.length; index += 1) { - const current = sql[index]; - if (current === '"') { - if (inQuotedIdentifier && sql[index + 1] === '"') { - index += 1; - continue; - } - inQuotedIdentifier = !inQuotedIdentifier; - continue; - } - if (inQuotedIdentifier) { - continue; - } - - if (parenthesisDepth === 0) { - const remaining = sql.slice(index); - if (/^AS\b/i.test(remaining)) { - return null; - } - if (current === '(') { - columnListStart = index; - parenthesisDepth = 1; - } - continue; - } - - if (current === '(') { - parenthesisDepth += 1; - } else if (current === ')') { - parenthesisDepth -= 1; - if (parenthesisDepth === 0) { - return { start: columnListStart, end: index }; - } - } - } - - return null; -}; - -const splitOracleViewColumns = (columnList: string): string[] => { - const columns: string[] = []; - let inQuotedIdentifier = false; - let start = 0; - - for (let index = 0; index < columnList.length; index += 1) { - const current = columnList[index]; - if (current === '"') { - if (inQuotedIdentifier && columnList[index + 1] === '"') { - index += 1; - continue; - } - inQuotedIdentifier = !inQuotedIdentifier; - } else if (current === ',' && !inQuotedIdentifier) { - columns.push(columnList.slice(start, index).trim()); - start = index + 1; - } - } - - columns.push(columnList.slice(start).trim()); - return columns.filter(Boolean); -}; - -const normalizeOracleViewDdlFormatting = (sql: string): string => { - const columnList = findOracleViewColumnList(sql); - let normalized = sql; - - if (columnList) { - const columns = splitOracleViewColumns(sql.slice(columnList.start + 1, columnList.end)); - if (columns.length > 1) { - normalized = [ - sql.slice(0, columnList.start + 1), - '\n ', - columns.join(',\n '), - '\n', - sql.slice(columnList.end), - ].join(''); - } - } - - return normalized.replace( - /\bWITH[ \t]*\r?\n[ \t]*(READ[ \t]+ONLY|CHECK[ \t]+OPTION)\b/gi, - 'WITH $1', - ); -}; +const isOracleViewDdl = (sql: string): boolean => ( + /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NO\s*)?FORCE\s+)?(?:NONEDITIONABLE\s+|EDITIONABLE\s+)?(?:MATERIALIZED\s+)?VIEW\b/i.test(sql) +); export const formatDdlForDisplay = (ddlText: unknown, dbType: string): string => { const raw = String(ddlText ?? '').trim(); if (!raw) { return ''; } + if (normalizeDbType(dbType) === 'oracle' && isOracleViewDdl(raw)) { + return raw; + } const language = resolveDdlFormatterLanguage(dbType); try { - const formatted = format(raw, { + return format(raw, { language, keywordCase: 'upper', linesBetweenQueries: 1, }); - return normalizeDbType(dbType) === 'oracle' - ? normalizeOracleViewDdlFormatting(formatted) - : formatted; } catch { return raw; }