🐛 fix(sql-editor): 补全对象定义片段修改模板

- 视图定义以 VIEW 片段开头时转换为 CREATE OR REPLACE VIEW,避免重复拼接 VIEW

- 触发器定义以 TRIGGER 或触发时机片段开头时自动补 CREATE OR REPLACE TRIGGER

- 增加视图和触发器对象修改模板回归测试
This commit is contained in:
Syngnat
2026-06-04 16:18:58 +08:00
parent f7217583a3
commit 30d1c080a0
4 changed files with 91 additions and 0 deletions

View File

@@ -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(<DefinitionViewer tab={createTab()} />);
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,

View File

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

View File

@@ -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(<TriggerViewer tab={tab} />);
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(<TriggerViewer tab={tab} />);
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 语句');
});
});

View File

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