From 30d1c080a0b45026528b67a0a4538858ed77eb6e Mon Sep 17 00:00:00 2001 From: Syngnat Date: Thu, 4 Jun 2026 16:18:58 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sql-editor):=20=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=E5=AF=B9=E8=B1=A1=E5=AE=9A=E4=B9=89=E7=89=87=E6=AE=B5?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 视图定义以 VIEW 片段开头时转换为 CREATE OR REPLACE VIEW,避免重复拼接 VIEW - 触发器定义以 TRIGGER 或触发时机片段开头时自动补 CREATE OR REPLACE TRIGGER - 增加视图和触发器对象修改模板回归测试 --- .../DefinitionViewer.object-edit.test.tsx | 25 ++++++++ frontend/src/components/DefinitionViewer.tsx | 3 + .../TriggerViewer.object-edit.test.tsx | 57 +++++++++++++++++++ frontend/src/components/TriggerViewer.tsx | 6 ++ 4 files changed, 91 insertions(+) diff --git a/frontend/src/components/DefinitionViewer.object-edit.test.tsx b/frontend/src/components/DefinitionViewer.object-edit.test.tsx index fe99485..031eb7f 100644 --- a/frontend/src/components/DefinitionViewer.object-edit.test.tsx +++ b/frontend/src/components/DefinitionViewer.object-edit.test.tsx @@ -87,6 +87,7 @@ describe('DefinitionViewer object edit entry', () => { storeState.addTab.mockReset(); storeState.setActiveContext.mockReset(); storeState.theme = 'light'; + storeState.connections[0].config.type = 'postgres'; backendApp.DBQuery.mockResolvedValue({ success: true, data: [{ view_definition: 'SELECT id, name FROM users' }], @@ -117,6 +118,30 @@ describe('DefinitionViewer object edit entry', () => { expect(storeState.addTab.mock.calls[0][0].query).toContain('SELECT id, name FROM users;'); }); + it('adds CREATE OR REPLACE without duplicating view fragments returned without ddl prefix', async () => { + backendApp.DBQuery.mockResolvedValue({ + success: true, + data: [{ view_definition: 'VIEW reporting.active_users AS\nSELECT id, name FROM users' }], + }); + + let renderer: any; + await act(async () => { + renderer = create(); + await flushPromises(); + }); + + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('对象修改'))[0]; + + await act(async () => { + button.props.onClick(); + }); + + const query = storeState.addTab.mock.calls[0][0].query; + expect(query).toContain('CREATE OR REPLACE VIEW reporting.active_users AS'); + expect(query).toContain('SELECT id, name FROM users;'); + expect(query).not.toContain('AS\nVIEW reporting.active_users AS'); + }); + it('opens an editable query tab for routine definitions', async () => { backendApp.DBQuery.mockResolvedValue({ success: true, diff --git a/frontend/src/components/DefinitionViewer.tsx b/frontend/src/components/DefinitionViewer.tsx index a83ae8a..703de86 100644 --- a/frontend/src/components/DefinitionViewer.tsx +++ b/frontend/src/components/DefinitionViewer.tsx @@ -48,6 +48,9 @@ const buildEditableDefinitionSql = (tab: TabData, definition: string, objectLabe } if (tab.type === 'view-def' && !/^\s*create\b/i.test(normalizedDefinition)) { + if (/^\s*view\b/i.test(normalizedDefinition)) { + return `${header}${ensureSqlStatementTerminator(normalizedDefinition.replace(/^\s*view\b/i, 'CREATE OR REPLACE VIEW'))}`; + } return `${header}CREATE OR REPLACE VIEW ${objectName} AS\n${ensureSqlStatementTerminator(normalizedDefinition)}`; } diff --git a/frontend/src/components/TriggerViewer.object-edit.test.tsx b/frontend/src/components/TriggerViewer.object-edit.test.tsx index 206ec1b..6f64db9 100644 --- a/frontend/src/components/TriggerViewer.object-edit.test.tsx +++ b/frontend/src/components/TriggerViewer.object-edit.test.tsx @@ -85,6 +85,7 @@ describe('TriggerViewer object edit entry', () => { beforeEach(() => { storeState.addTab.mockReset(); storeState.setActiveContext.mockReset(); + storeState.connections[0].config.type = 'postgres'; backendApp.DBQuery.mockResolvedValue({ success: true, data: [{ trigger_definition: 'CREATE TRIGGER users_bi BEFORE INSERT ON audit.users EXECUTE FUNCTION audit.audit_users();' }], @@ -113,4 +114,60 @@ describe('TriggerViewer object edit entry', () => { query: expect.stringContaining('CREATE TRIGGER users_bi BEFORE INSERT'), })); }); + + it('adds CREATE OR REPLACE for trigger source snippets returned without ddl prefix', async () => { + storeState.connections[0].config.type = 'oracle'; + backendApp.DBQuery.mockResolvedValue({ + success: true, + data: [{ + TRIGGER_BODY: 'TRIGGER users_bi\nBEFORE INSERT ON audit.users\nFOR EACH ROW\nBEGIN\n :NEW.created_at := SYSDATE;\nEND;', + }], + }); + + let renderer: any; + await act(async () => { + renderer = create(); + await flushPromises(); + }); + + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('对象修改'))[0]; + + await act(async () => { + button.props.onClick(); + }); + + const query = storeState.addTab.mock.calls[0][0].query; + expect(query).toContain('CREATE OR REPLACE TRIGGER users_bi'); + expect(query).toContain('BEFORE INSERT ON audit.users'); + expect(query).toContain(':NEW.created_at := SYSDATE;'); + expect(query).not.toContain('请补全 CREATE TRIGGER 语句'); + }); + + it('adds trigger name for trigger body snippets returned without ddl header', async () => { + storeState.connections[0].config.type = 'oracle'; + backendApp.DBQuery.mockResolvedValue({ + success: true, + data: [{ + TRIGGER_BODY: 'BEFORE UPDATE ON audit.users\nFOR EACH ROW\nBEGIN\n :NEW.updated_at := SYSDATE;\nEND;', + }], + }); + + let renderer: any; + await act(async () => { + renderer = create(); + await flushPromises(); + }); + + const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('对象修改'))[0]; + + await act(async () => { + button.props.onClick(); + }); + + const query = storeState.addTab.mock.calls[0][0].query; + expect(query).toContain('CREATE OR REPLACE TRIGGER audit.users_bi'); + expect(query).toContain('BEFORE UPDATE ON audit.users'); + expect(query).toContain(':NEW.updated_at := SYSDATE;'); + expect(query).not.toContain('请补全 CREATE TRIGGER 语句'); + }); }); diff --git a/frontend/src/components/TriggerViewer.tsx b/frontend/src/components/TriggerViewer.tsx index 3a281ad..21ace7b 100644 --- a/frontend/src/components/TriggerViewer.tsx +++ b/frontend/src/components/TriggerViewer.tsx @@ -29,6 +29,12 @@ const buildEditableTriggerSql = (triggerName: string, triggerDefinition: string) if (/^\s*create\s+(?:or\s+replace\s+)?trigger\b/i.test(normalizedDefinition)) { return `${header}${ensureSqlStatementTerminator(normalizedDefinition)}`; } + if (/^\s*trigger\b/i.test(normalizedDefinition)) { + return `${header}${ensureSqlStatementTerminator(normalizedDefinition.replace(/^\s*trigger\b/i, 'CREATE OR REPLACE TRIGGER'))}`; + } + if (/^\s*(?:before|after|instead\s+of)\b/i.test(normalizedDefinition)) { + return `${header}${ensureSqlStatementTerminator(`CREATE OR REPLACE TRIGGER ${normalizedName}\n${normalizedDefinition}`)}`; + } return `${header}-- 当前数据源仅返回触发器定义片段,请补全 CREATE TRIGGER 语句后执行\n${ensureSqlStatementTerminator(normalizedDefinition)}`; };