🐛 fix(definition-viewer): 修复对象修改保留完整定义

- 统一对象查看页定义展示,补齐可复制的 CREATE OR REPLACE DDL

- 打开对象修改前清理同标签旧草稿,避免旧草稿覆盖完整注释

- 补充 Oracle 函数、过程和包定义的回归测试
This commit is contained in:
Syngnat
2026-07-02 14:12:22 +08:00
parent 68d4081fcf
commit ffb4d16950
2 changed files with 129 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { TabData } from '../types';
import { I18nProvider } from '../i18n/provider';
import { clearQueryTabDraft, hasQueryTabDraft, setQueryTabDraft } from '../utils/sqlFileTabDrafts';
import DefinitionViewer from './DefinitionViewer';
const storeState = vi.hoisted(() => ({
@@ -102,6 +103,7 @@ describe('DefinitionViewer object edit entry', () => {
storeState.setActiveContext.mockReset();
storeState.theme = 'light';
storeState.connections[0].config.type = 'postgres';
clearQueryTabDraft('routine-def-conn-1-H2-H2.F_GET_BUSPRICE');
backendApp.DBQuery.mockReset();
backendApp.DBQuery.mockResolvedValue({
success: true,
@@ -117,6 +119,9 @@ describe('DefinitionViewer object edit entry', () => {
});
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0];
const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''));
expect(editorText).toContain('CREATE OR REPLACE VIEW reporting.active_users AS');
expect(editorText).toContain('SELECT id, name FROM users;');
await act(async () => {
button.props.onClick();
@@ -367,6 +372,10 @@ describe('DefinitionViewer object edit entry', () => {
await flushPromises();
});
const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''));
expect(editorText).toContain('CREATE OR REPLACE PROCEDURE proc_tally2accept(p_id IN NUMBER)');
expect(editorText).toContain('v_count PLS_INTEGER;');
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0];
await act(async () => {
@@ -379,6 +388,71 @@ describe('DefinitionViewer object edit entry', () => {
expect(query).toContain('SELECT COUNT(*) INTO v_count FROM dual;');
});
it('clears stale same-tab drafts before opening object edit so comments are not replaced', async () => {
storeState.connections[0].config.type = 'oracle';
const tab = createTab({
id: 'routine-def-conn-1-H2-H2.F_GET_BUSPRICE',
title: '函数: H2.F_GET_BUSPRICE',
type: 'routine-def',
dbName: 'H2',
routineName: 'H2.F_GET_BUSPRICE',
routineType: 'FUNCTION',
viewName: undefined,
viewKind: undefined,
});
backendApp.DBQuery.mockResolvedValue({
success: true,
data: [
{ TEXT: 'FUNCTION "F_GET_BUSPRICE"(p_compid t_store_d.compid%TYPE,\n' },
{ TEXT: ' p_busno t_store_d.busno%TYPE,\n' },
{ TEXT: ' p_wareid t_store_d.wareid%TYPE,\n' },
{ TEXT: ' p_type INTEGER)\n' },
{ TEXT: 'RETURN t_ware_saleprice.saleprice%TYPE IS\n\n' },
{ TEXT: 'BEGIN\n' },
{ TEXT: ' /*根据机构跟商品,返回对应的价格信息\n' },
{ TEXT: ' 1: p_compid 企业编码\n' },
{ TEXT: ' 2: p_busno 业务机构\n' },
{ TEXT: ' 3: p_wareid 商品编码\n' },
{ TEXT: ' 4: p_type 类型 1, 当前机构商品毛利率; 2, 当前机构商品毛利; 3, 当前机构商品最新进价; 4, 当前机构商品销售价\n' },
{ TEXT: ' */\n' },
{ TEXT: ' NULL; -- add by txy 1723 20171129 增加库存不等于0的条件\n' },
{ TEXT: 'END;\n' },
],
});
setQueryTabDraft(tab.id, [
'-- 修改函数/存储过程: H2.F_GET_BUSPRICE',
'CREATE OR REPLACE FUNCTION "F_GET_BUSPRICE" AS',
'BEGIN',
' /*根据机构跟商品,返回对应的价格信息',
' 1: p_compid 企业编码',
' */',
'END;',
].join('\n'));
expect(hasQueryTabDraft(tab.id)).toBe(true);
let renderer: any;
await act(async () => {
renderer = create(renderWithI18n(tab));
await flushPromises();
});
const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''));
expect(editorText).toContain('CREATE OR REPLACE FUNCTION "F_GET_BUSPRICE"');
expect(editorText).toContain('4: p_type 类型 1, 当前机构商品毛利率; 2, 当前机构商品毛利; 3, 当前机构商品最新进价; 4, 当前机构商品销售价');
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0];
await act(async () => {
await button.props.onClick();
await flushPromises();
});
const editQuery = String(storeState.addTab.mock.calls[0][0].query || '');
expect(editQuery).toContain('4: p_type 类型 1, 当前机构商品毛利率; 2, 当前机构商品毛利; 3, 当前机构商品最新进价; 4, 当前机构商品销售价');
expect(editQuery).toContain('NULL; -- add by txy 1723 20171129 增加库存不等于0的条件');
expect(hasQueryTabDraft(tab.id)).toBe(false);
});
it('opens an editable query tab for Oracle sequence definitions', async () => {
storeState.connections[0].config.type = 'oracle';
backendApp.DBQuery.mockResolvedValue({
@@ -466,9 +540,9 @@ describe('DefinitionViewer object edit entry', () => {
expect(String(backendApp.DBQuery.mock.calls[0][2] || '')).toContain("TYPE = 'PACKAGE'");
expect(String(backendApp.DBQuery.mock.calls[1][2] || '')).toContain("TYPE = 'PACKAGE BODY'");
const editorText = String(renderer.root.findAll((node: any) => node.props['data-editor'] === 'true')[0].children.join(''));
expect(editorText).toContain('PACKAGE pkg_order AS');
expect(editorText).toContain('CREATE OR REPLACE PACKAGE pkg_order AS');
expect(editorText).toContain('PROCEDURE sync_order(p_id IN NUMBER);');
expect(editorText).toContain('PACKAGE BODY pkg_order AS');
expect(editorText).toContain('CREATE OR REPLACE PACKAGE BODY pkg_order AS');
expect(editorText).toContain('NULL;');
const button = renderer.root.findAll((node: any) => node.type === 'button' && findButtonText(node).includes('Edit object'))[0];

View File

@@ -10,6 +10,7 @@ import { normalizeOceanBaseProtocol } from '../utils/oceanBaseProtocol';
import { useI18n } from '../i18n/provider';
import { splitQualifiedNameLast } from '../utils/qualifiedName';
import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition';
import { clearQueryTabDraft } from '../utils/sqlFileTabDrafts';
interface DefinitionViewerProps {
tab: TabData;
@@ -118,6 +119,46 @@ const buildEditableDefinitionSql = (
return `${header}${ensureSqlStatementTerminator(normalizedDefinition)}`;
};
const buildDisplayDefinitionSql = (
tab: TabData,
definition: string,
objectName: string,
): string => {
const normalizedDefinition = String(definition || '').trim();
if (!normalizedDefinition || isCommentOnlyDefinition(normalizedDefinition)) {
return normalizedDefinition;
}
if (tab.type === 'view-def' && !/^\s*create\b/i.test(normalizedDefinition)) {
if (/^\s*view\b/i.test(normalizedDefinition)) {
return ensureSqlStatementTerminator(normalizedDefinition.replace(/^\s*view\b/i, 'CREATE OR REPLACE VIEW'));
}
return `CREATE OR REPLACE VIEW ${objectName} AS\n${ensureSqlStatementTerminator(normalizedDefinition)}`;
}
if (tab.type === 'sequence-def' && !/^\s*create\b/i.test(normalizedDefinition)) {
return ensureSqlStatementTerminator(`CREATE SEQUENCE ${objectName}\n${normalizedDefinition}`);
}
if (
tab.type === 'package-def'
&& !/^\s*create\b/i.test(normalizedDefinition)
&& /^\s*package\b/i.test(normalizedDefinition)
) {
return withCreateOrReplacePackageHeaders(normalizedDefinition);
}
if (
tab.type === 'routine-def'
&& !/^\s*create\b/i.test(normalizedDefinition)
&& /^\s*(function|procedure)\b/i.test(normalizedDefinition)
) {
return ensureSqlStatementTerminator(`CREATE OR REPLACE ${normalizedDefinition}`);
}
return ensureSqlStatementTerminator(normalizedDefinition);
};
const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -700,6 +741,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
let queries: string[];
let extractFn: (dialect: string, data: any[]) => string;
let resolvedObjectLabel: string;
let resolvedObjectName = '';
if (tab.type === 'view-def') {
const viewName = tab.viewName || '';
@@ -711,6 +753,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
resolvedObjectLabel = tab.viewKind === 'materialized'
? t('definition_viewer.object.materialized_view')
: t('definition_viewer.object.view');
resolvedObjectName = viewName;
} else if (tab.type === 'event-def') {
const eventName = tab.eventName || '';
if (!eventName) {
@@ -719,6 +762,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
queries = buildShowEventQueries(dialect, eventName, dbName);
extractFn = extractEventDefinition;
resolvedObjectLabel = t('definition_viewer.object.event');
resolvedObjectName = eventName;
} else if (tab.type === 'sequence-def') {
const sequenceName = tab.sequenceName || '';
if (!sequenceName) {
@@ -727,6 +771,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
queries = buildShowSequenceQueries(dialect, sequenceName, dbName);
extractFn = extractSequenceDefinition;
resolvedObjectLabel = t('definition_viewer.object.sequence');
resolvedObjectName = sequenceName;
} else if (tab.type === 'package-def') {
const packageName = tab.packageName || '';
if (!packageName) {
@@ -735,6 +780,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
queries = buildShowPackageQueries(dialect, packageName, dbName);
extractFn = extractPackageDefinition;
resolvedObjectLabel = t('definition_viewer.object.package');
resolvedObjectName = packageName;
} else {
const routineName = tab.routineName || '';
const routineType = tab.routineType || 'FUNCTION';
@@ -744,6 +790,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
queries = buildShowRoutineQueries(dialect, routineName, routineType, dbName);
extractFn = extractRoutineDefinition;
resolvedObjectLabel = t('definition_viewer.object.routine');
resolvedObjectName = routineName;
}
if (!queries.length || String(queries[0] || '').startsWith('--')) {
@@ -770,7 +817,11 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
: await runQueryCandidates(config, dbName, queries);
if (result.success && Array.isArray(result.data) && result.data.length > 0) {
return { success: true, definition: extractFn(dialect, result.data) };
const rawDefinition = extractFn(dialect, result.data);
return {
success: true,
definition: buildDisplayDefinitionSql(tab, rawDefinition, resolvedObjectName),
};
}
if (result.success) {
@@ -944,6 +995,7 @@ const DefinitionViewer: React.FC<DefinitionViewerProps> = ({ tab }) => {
setDefinition(latestDefinition);
const query = buildEditableDefinitionSql(tab, latestDefinition, normalizedObjectName, editableDefinitionCopy);
setActiveContext({ connectionId: tab.connectionId, dbName });
clearQueryTabDraft(tab.id);
addTab({
id: tab.id,
title: editTabTitle,