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)}`; };