mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 01:11:31 +08:00
🐛 fix(oracle): 接入对象跳转并修复过程修改执行
- SQL 编辑器补齐 Oracle 序列和存储包元数据、hover 提示与 Ctrl/Cmd 点击跳转 - 对象编辑 SQL 保留 SQLPlus 斜杠分隔符,避免生成 /; 导致 ORA-00900 - 补充导航、对象编辑执行和多语言目录回归测试
This commit is contained in:
45
docs/需求追踪/需求进度追踪-Oracle对象跳转与存储过程修改执行-20260625.md
Normal file
45
docs/需求追踪/需求进度追踪-Oracle对象跳转与存储过程修改执行-20260625.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 需求进度追踪 - Oracle对象跳转与存储过程修改执行
|
||||
|
||||
## 1. 需求摘要
|
||||
- 需求名称:Oracle对象跳转与存储过程修改执行
|
||||
- 提出日期:2026-06-25
|
||||
- 负责人:Codex
|
||||
- 目标:SQL编辑器支持 Oracle 序列、存储包 Ctrl/Cmd 点击跳转;对象编辑执行 Oracle 存储过程时不再生成非法第二条语句。
|
||||
- 非目标:不调整 Oracle 连接驱动、不改数据库 schema、不重构侧栏对象树。
|
||||
|
||||
## 2. 范围与验收
|
||||
- 范围:QueryEditor 对象元数据、hover/跳转逻辑、DefinitionViewer 对象编辑 SQL 生成、相关 i18n 和回归测试。
|
||||
- 验收标准:序列/存储包在 SQL 编辑器可出现链接提示并打开定义页;带 SQLPlus `/` 的 Oracle PL/SQL 编辑 SQL 不生成 `/;`,执行时保留完整定义交给后端拆分。
|
||||
- 依赖与约束:复用现有侧栏 sequence/package tab 类型;不混入 `frontend/wailsjs/go/models.ts` 既有未提交改动。
|
||||
|
||||
## 3. 里程碑与进度
|
||||
- [x] 阶段 1(需求澄清):确认序列/存储包侧栏已可打开,但 SQL 编辑器跳转缺失;存储过程修改仍报 ORA-00900。
|
||||
- [x] 阶段 2(影响分析):影响前端 QueryEditor、DefinitionViewer、i18n、测试;后端拆分已有基础保护。
|
||||
- [x] 阶段 3(方案设计):补齐元数据与跳转类型;对象编辑 SQL 保留 SQLPlus `/` 语义。
|
||||
- [x] 阶段 4(实施计划):先补导航闭环,再修 PL/SQL 生成/执行,最后跑定向测试。
|
||||
- [x] 阶段 5(实现与自检):已完成定向测试、QueryEditor 全文件测试、前端 build、后端 Oracle 拆分测试。
|
||||
- [ ] 阶段 6(评审与交付):待提交推送。
|
||||
- [ ] 阶段 7(发布与观察):发布后验证 Oracle 对象编辑与 SQL 编辑器跳转。
|
||||
|
||||
## 4. 变更清单
|
||||
- 已完成:补齐 QueryEditor sequence/package 元数据、hover、点击打开定义页;修复对象编辑 SQLPlus `/;`;执行路径兼容旧编辑页 `/;`。
|
||||
- 进行中:提交并推送。
|
||||
- 待处理:发布后验证 Oracle 实库。
|
||||
|
||||
## 5. 风险与阻塞
|
||||
- 风险:三段名称 `schema.package.proc` 与 `db.schema.table` 存在歧义。
|
||||
- 阻塞:无。
|
||||
- 缓解措施:仅当第一段不是可见数据库时,将三段名称按 Oracle schema-qualified sequence/package 解析。
|
||||
|
||||
## 6. 决策记录
|
||||
- 决策 1:序列和存储包复用侧栏现有 `sequence-def` / `package-def` tab 结构。
|
||||
- 决策 2:Oracle PL/SQL 定义执行时保留 SQLPlus `/` 分隔符,由后端拆分器去掉 delimiter,避免前端用分号重组造成 ORA-00900。
|
||||
|
||||
## 7. 验证记录
|
||||
- 验证项:QueryEditor 导航回归测试、DefinitionViewer 对象编辑测试、SQL 语句拆分测试、i18n catalog 测试、前端 build、后端 Oracle 拆分测试。
|
||||
- 结果:通过。
|
||||
- 证据(日志/截图/链接):`npm test -- src/components/QueryEditor.external-sql-save.test.tsx`;`npm test -- src/components/DefinitionViewer.object-edit.test.tsx src/i18n/catalog.test.ts src/utils/sqlStatementSelection.test.ts`;`go test ./internal/app -run 'TestDBQueryMultiKeepsOracle|TestDBQueryMultiSkipsOracle'`;`npm run build`。
|
||||
|
||||
## 8. 下一步
|
||||
- 下一步行动:提交并推送到 dev。
|
||||
- 负责人:Codex
|
||||
@@ -419,6 +419,55 @@ describe('DefinitionViewer object edit entry', () => {
|
||||
expect(editQuery).toContain('END sync_order;');
|
||||
});
|
||||
|
||||
it('keeps Oracle routine SQLPlus slash delimiters executable when opening object edit', async () => {
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
backendApp.DBQuery
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{ TEXT: 'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1 AS\n' },
|
||||
{ TEXT: 'BEGIN\n' },
|
||||
{ TEXT: ' NULL;\n' },
|
||||
{ TEXT: 'END cproc_tzhssr_order2sale_A1;\n' },
|
||||
{ TEXT: '/\n' },
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [
|
||||
{ TEXT: 'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1 AS\n' },
|
||||
{ TEXT: 'BEGIN\n' },
|
||||
{ TEXT: ' NULL;\n' },
|
||||
{ TEXT: 'END cproc_tzhssr_order2sale_A1;\n' },
|
||||
{ TEXT: '/\n' },
|
||||
],
|
||||
});
|
||||
|
||||
let renderer: any;
|
||||
await act(async () => {
|
||||
renderer = create(renderWithI18n(createTab({
|
||||
id: 'routine-def-conn-1-H2-H2.CPROC_TZHSSR_ORDER2SALE_A1',
|
||||
title: 'Procedure: H2.CPROC_TZHSSR_ORDER2SALE_A1',
|
||||
type: 'routine-def',
|
||||
routineName: 'H2.CPROC_TZHSSR_ORDER2SALE_A1',
|
||||
routineType: 'PROCEDURE',
|
||||
viewName: undefined,
|
||||
viewKind: undefined,
|
||||
})));
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
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('END cproc_tzhssr_order2sale_A1;\n/');
|
||||
expect(editQuery).not.toContain('/;');
|
||||
});
|
||||
|
||||
it('reloads the latest object definition before opening object edit', async () => {
|
||||
backendApp.DBQuery
|
||||
.mockResolvedValueOnce({
|
||||
|
||||
@@ -32,9 +32,18 @@ const normalizeMySQLViewDDL = (rawDefinition: unknown): string => {
|
||||
return `${normalized};`;
|
||||
};
|
||||
|
||||
const normalizeSqlPlusSlashTerminator = (sql: string): string => (
|
||||
String(sql || '').trim().replace(/(^|\n)([ \t]*\/[ \t]*);+([ \t]*(?:--[^\n]*)?)\s*$/i, '$1$2$3')
|
||||
);
|
||||
|
||||
const hasStandaloneSqlPlusSlashTerminator = (sql: string): boolean => (
|
||||
/(?:^|\n)[ \t]*\/[ \t]*(?:--[^\n]*)?\s*$/i.test(String(sql || '').trim())
|
||||
);
|
||||
|
||||
const ensureSqlStatementTerminator = (sql: string): string => {
|
||||
const normalized = String(sql || '').trim();
|
||||
const normalized = normalizeSqlPlusSlashTerminator(sql);
|
||||
if (!normalized) return '';
|
||||
if (hasStandaloneSqlPlusSlashTerminator(normalized)) return normalized;
|
||||
return /;\s*$/.test(normalized) ? normalized : `${normalized};`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1481,55 +1481,73 @@ describe('QueryEditor external SQL save', () => {
|
||||
const routines = [
|
||||
{ dbName: 'main', routineName: 'reporting.refresh_stats', routineType: 'PROCEDURE', schemaName: 'reporting' },
|
||||
];
|
||||
const sequences = [
|
||||
{ dbName: 'main', sequenceName: 'billing.order_seq', schemaName: 'billing' },
|
||||
];
|
||||
const packages = [
|
||||
{ dbName: 'main', packageName: 'billing.pkg_order', schemaName: 'billing' },
|
||||
];
|
||||
|
||||
expect(resolveQueryEditorNavigationTarget('select * from analytics.events', 31, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('select * from analytics.events', 31, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'table',
|
||||
dbName: 'analytics',
|
||||
tableName: 'events',
|
||||
schemaName: undefined,
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from dbo.orders', 21, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('select * from dbo.orders', 21, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'table',
|
||||
dbName: 'main',
|
||||
tableName: 'dbo.orders',
|
||||
schemaName: 'dbo',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('use analytics', 6, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('use analytics', 6, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'database',
|
||||
dbName: 'analytics',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from users', 18, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('select * from users', 18, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'table',
|
||||
dbName: 'main',
|
||||
tableName: 'users',
|
||||
schemaName: undefined,
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from reporting.active_users', 31, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('select * from reporting.active_users', 31, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'view',
|
||||
dbName: 'main',
|
||||
viewName: 'reporting.active_users',
|
||||
schemaName: 'reporting',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select * from analytics.mv_daily_stats', 37, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('select * from analytics.mv_daily_stats', 37, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'materialized-view',
|
||||
dbName: 'analytics',
|
||||
viewName: 'mv_daily_stats',
|
||||
schemaName: undefined,
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('call audit.users_bi()', 18, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('call audit.users_bi()', 18, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'trigger',
|
||||
dbName: 'main',
|
||||
triggerName: 'audit.users_bi',
|
||||
tableName: 'audit.users',
|
||||
schemaName: 'audit',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('call reporting.refresh_stats()', 21, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines)).toEqual({
|
||||
expect(resolveQueryEditorNavigationTarget('call reporting.refresh_stats()', 21, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'routine',
|
||||
dbName: 'main',
|
||||
routineName: 'reporting.refresh_stats',
|
||||
routineType: 'PROCEDURE',
|
||||
schemaName: 'reporting',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('select billing.order_seq.nextval from dual', 18, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'sequence',
|
||||
dbName: 'main',
|
||||
sequenceName: 'billing.order_seq',
|
||||
schemaName: 'billing',
|
||||
});
|
||||
expect(resolveQueryEditorNavigationTarget('begin billing.pkg_order.sync_order(1); end;', 16, 'main', ['main', 'analytics'], tables, views, materializedViews, triggers, routines, sequences, packages)).toEqual({
|
||||
type: 'package',
|
||||
dbName: 'main',
|
||||
packageName: 'billing.pkg_order',
|
||||
schemaName: 'billing',
|
||||
});
|
||||
});
|
||||
|
||||
it('prefers the unique schema-qualified view target when metadata also contains a bare view name', () => {
|
||||
@@ -1772,6 +1790,12 @@ describe('QueryEditor external SQL save', () => {
|
||||
{ dbName: 'main', routineName: 'reporting.refresh_stats', routineType: 'PROCEDURE', schemaName: 'reporting' },
|
||||
{ dbName: 'main', routineName: 'reporting.score_user', routineType: 'FUNCTION', schemaName: 'reporting' },
|
||||
];
|
||||
const sequences = [
|
||||
{ dbName: 'main', sequenceName: 'billing.order_seq', schemaName: 'billing' },
|
||||
];
|
||||
const packages = [
|
||||
{ dbName: 'main', packageName: 'billing.pkg_order', schemaName: 'billing' },
|
||||
];
|
||||
|
||||
const cases = [
|
||||
{ lineContent: 'use analytics', column: 6, expected: 'Ctrl + click to switch to this database' },
|
||||
@@ -1781,6 +1805,8 @@ describe('QueryEditor external SQL save', () => {
|
||||
{ lineContent: 'call audit.users_bi()', column: 18, expected: 'Ctrl + click to open this trigger' },
|
||||
{ lineContent: 'call reporting.refresh_stats()', column: 21, expected: 'Ctrl + click to open this stored procedure' },
|
||||
{ lineContent: 'select reporting.score_user()', column: 21, expected: 'Ctrl + click to open this function' },
|
||||
{ lineContent: 'select billing.order_seq.nextval from dual', column: 18, expected: 'Ctrl + click to open this sequence' },
|
||||
{ lineContent: 'begin billing.pkg_order.sync_order(1); end;', column: 16, expected: 'Ctrl + click to open this package' },
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
@@ -1794,6 +1820,8 @@ describe('QueryEditor external SQL save', () => {
|
||||
materializedViews,
|
||||
triggers,
|
||||
routines,
|
||||
sequences,
|
||||
packages,
|
||||
'Ctrl',
|
||||
);
|
||||
|
||||
@@ -2098,7 +2126,9 @@ describe('QueryEditor external SQL save', () => {
|
||||
|
||||
it('localizes Monaco action labels for the active language', async () => {
|
||||
setCurrentLanguage('en-US');
|
||||
storeState.shortcutOptions.runQuery.mac = { enabled: true, combo: 'Meta+Q' };
|
||||
storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+Q' };
|
||||
storeState.shortcutOptions.selectCurrentStatement.mac = { enabled: true, combo: 'Meta+Q' };
|
||||
storeState.shortcutOptions.selectCurrentStatement.windows = { enabled: true, combo: 'Ctrl+Q' };
|
||||
|
||||
await act(async () => {
|
||||
@@ -2120,7 +2150,9 @@ describe('QueryEditor external SQL save', () => {
|
||||
});
|
||||
|
||||
it('refreshes Monaco action labels when languagePreference changes after mount', async () => {
|
||||
storeState.shortcutOptions.runQuery.mac = { enabled: true, combo: 'Meta+Q' };
|
||||
storeState.shortcutOptions.runQuery.windows = { enabled: true, combo: 'Ctrl+Q' };
|
||||
storeState.shortcutOptions.selectCurrentStatement.mac = { enabled: true, combo: 'Meta+Q' };
|
||||
storeState.shortcutOptions.selectCurrentStatement.windows = { enabled: true, combo: 'Ctrl+Q' };
|
||||
|
||||
await act(async () => {
|
||||
@@ -2289,6 +2321,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
it('shows "No selectable SQL statement." in English when selecting the current statement without selectable SQL', async () => {
|
||||
storeState.languagePreference = 'en-US';
|
||||
setCurrentLanguage('en-US');
|
||||
storeState.shortcutOptions.selectCurrentStatement.mac = { enabled: true, combo: 'Meta+Q' };
|
||||
storeState.shortcutOptions.selectCurrentStatement.windows = { enabled: true, combo: 'Ctrl+Q' };
|
||||
messageApi.info.mockReset();
|
||||
|
||||
@@ -2308,6 +2341,7 @@ describe('QueryEditor external SQL save', () => {
|
||||
});
|
||||
|
||||
it('selects only the current SQL statement when the editor content uses CRLF line endings', async () => {
|
||||
storeState.shortcutOptions.selectCurrentStatement.mac = { enabled: true, combo: 'Meta+Q' };
|
||||
storeState.shortcutOptions.selectCurrentStatement.windows = { enabled: true, combo: 'Ctrl+Q' };
|
||||
const sql = [
|
||||
'SELECT * FROM first_table;',
|
||||
@@ -3724,6 +3758,99 @@ describe('QueryEditor external SQL save', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('opens sequence and package tabs on ctrl left click inside the editor', async () => {
|
||||
editorState.value = 'select billing.order_seq.nextval from dual; begin billing.pkg_order.sync_order(1); end;';
|
||||
autoFetchState.visible = true;
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
storeState.connections[0].config.database = 'main';
|
||||
backendApp.DBGetDatabases.mockResolvedValueOnce({ success: true, data: [{ Database: 'main' }] });
|
||||
backendApp.DBGetTables.mockResolvedValueOnce({ success: true, data: [] });
|
||||
backendApp.DBGetAllColumns.mockResolvedValueOnce({ success: true, data: [] });
|
||||
backendApp.DBQuery.mockImplementation(async (_config: any, _dbName: string, sql: string) => {
|
||||
if (sql.includes('ALL_SEQUENCES') || sql.includes('USER_SEQUENCES')) {
|
||||
return { success: true, data: [{ sequence_name: 'order_seq', schema_name: 'billing' }] };
|
||||
}
|
||||
if (sql.includes('ALL_OBJECTS') && sql.includes("OBJECT_TYPE = 'PACKAGE'")) {
|
||||
return { success: true, data: [{ package_name: 'pkg_order', schema_name: 'billing' }] };
|
||||
}
|
||||
return { success: true, data: [] };
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
create(<QueryEditor tab={createTab({ query: editorState.value, dbName: 'main' })} />);
|
||||
});
|
||||
await act(async () => {
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 18 } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
editorState.mouseDownListeners[0]?.({
|
||||
target: { position: { lineNumber: 1, column: 59 } },
|
||||
event: {
|
||||
leftButton: true,
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
preventDefault: vi.fn(),
|
||||
stopPropagation: vi.fn(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
expect(storeState.addTab).toHaveBeenCalledWith({
|
||||
id: 'sequence-def-conn-1-main-billing.order_seq',
|
||||
title: '序列:billing.order_seq',
|
||||
type: 'sequence-def',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
sequenceName: 'billing.order_seq',
|
||||
schemaName: 'billing',
|
||||
sidebarLocateKey: 'sequence-def-conn-1-main-billing.order_seq',
|
||||
});
|
||||
expect(storeState.addTab).toHaveBeenCalledWith({
|
||||
id: 'package-def-conn-1-main-billing.pkg_order',
|
||||
title: '存储包:billing.pkg_order',
|
||||
type: 'package-def',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'main',
|
||||
packageName: 'billing.pkg_order',
|
||||
schemaName: 'billing',
|
||||
sidebarLocateKey: 'package-def-conn-1-main-billing.pkg_order',
|
||||
});
|
||||
expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'gonavi:locate-sidebar-object',
|
||||
detail: expect.objectContaining({
|
||||
tabId: 'sequence-def-conn-1-main-billing.order_seq',
|
||||
sequenceName: 'billing.order_seq',
|
||||
schemaName: 'billing',
|
||||
objectGroup: 'sequences',
|
||||
}),
|
||||
}));
|
||||
expect((window as any).dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'gonavi:locate-sidebar-object',
|
||||
detail: expect.objectContaining({
|
||||
tabId: 'package-def-conn-1-main-billing.pkg_order',
|
||||
packageName: 'billing.pkg_order',
|
||||
schemaName: 'billing',
|
||||
objectGroup: 'packages',
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
describe('object navigation tab title localization', () => {
|
||||
it('uses the English catalog title for view definition tabs', async () => {
|
||||
storeState.languagePreference = 'en-US';
|
||||
@@ -5618,6 +5745,50 @@ describe('QueryEditor external SQL save', () => {
|
||||
renderer?.unmount();
|
||||
});
|
||||
|
||||
it('preserves Oracle SQLPlus slash delimiters for selected object-edit PL/SQL definitions', async () => {
|
||||
storeState.connections[0].config.type = 'oracle';
|
||||
storeState.connections[0].config.database = 'ORCLPDB1';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 1 }] }],
|
||||
});
|
||||
const expectedPlsql = [
|
||||
'-- 修改函数/存储过程:H2.cproc_tzhssr_order2sale_A1',
|
||||
'-- 请确认语法兼容当前数据库后执行',
|
||||
'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1 AS',
|
||||
'BEGIN',
|
||||
' NULL;',
|
||||
'END cproc_tzhssr_order2sale_A1;',
|
||||
'/',
|
||||
].join('\n');
|
||||
const legacyEditorPlsql = expectedPlsql.replace(/\n\/$/, '\n/;');
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ dbName: 'ORCLPDB1', query: legacyEditorPlsql, queryMode: 'object-edit' })} />);
|
||||
});
|
||||
editorState.selection = {
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 7,
|
||||
endColumn: 3,
|
||||
positionLineNumber: 7,
|
||||
positionColumn: 3,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(backendApp.DBQueryMulti).toHaveBeenCalledWith(expect.anything(), 'ORCLPDB1', expectedPlsql, 'query-1');
|
||||
expect(String(backendApp.DBQueryMulti.mock.calls[0][2])).not.toContain('/;');
|
||||
renderer?.unmount();
|
||||
});
|
||||
|
||||
it('renders result grid for sqlserver exec statements that return rows', async () => {
|
||||
storeState.connections[0].config.type = 'sqlserver';
|
||||
storeState.connections[0].config.database = 'master';
|
||||
|
||||
@@ -57,7 +57,9 @@ import QueryEditorToolbar from './QueryEditorToolbar';
|
||||
import { useSqlEditorTransactionController } from './useSqlEditorTransactionController';
|
||||
import {
|
||||
type CompletionColumnMeta,
|
||||
type CompletionPackageMeta,
|
||||
type CompletionRoutineMeta,
|
||||
type CompletionSequenceMeta,
|
||||
type CompletionTableMeta,
|
||||
type CompletionTriggerMeta,
|
||||
type CompletionViewMeta,
|
||||
@@ -74,6 +76,8 @@ import {
|
||||
buildCompletionDocumentation,
|
||||
buildCompletionFunctionsMetadataQuerySpecs,
|
||||
buildCompletionMaterializedViewsMetadataQuerySpecs,
|
||||
buildCompletionPackagesMetadataQuerySpecs,
|
||||
buildCompletionSequencesMetadataQuerySpecs,
|
||||
buildCompletionTableCommentSQL,
|
||||
buildCompletionTriggersMetadataQuerySpecs,
|
||||
buildCompletionViewsMetadataQuerySpecs,
|
||||
@@ -170,6 +174,8 @@ let sharedViewsData: CompletionViewMeta[] = [];
|
||||
let sharedMaterializedViewsData: CompletionViewMeta[] = [];
|
||||
let sharedTriggersData: CompletionTriggerMeta[] = [];
|
||||
let sharedRoutinesData: CompletionRoutineMeta[] = [];
|
||||
let sharedSequencesData: CompletionSequenceMeta[] = [];
|
||||
let sharedPackagesData: CompletionPackageMeta[] = [];
|
||||
let sharedColumnsCacheData: Record<string, any[]> = {};
|
||||
const QUERY_EDITOR_LAZY_VISIBLE_DB_COMPLETION_LIMIT = 10;
|
||||
const sharedLazyTablesCache: Record<string, CompletionTableMeta[] | undefined> = {};
|
||||
@@ -190,6 +196,8 @@ const resetSharedQueryEditorMetadata = () => {
|
||||
sharedMaterializedViewsData = [];
|
||||
sharedTriggersData = [];
|
||||
sharedRoutinesData = [];
|
||||
sharedSequencesData = [];
|
||||
sharedPackagesData = [];
|
||||
sharedColumnsCacheData = {};
|
||||
clearRecord(sharedLazyTablesCache);
|
||||
clearRecord(sharedLazyTablesInFlight);
|
||||
@@ -258,6 +266,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const materializedViewsRef = useRef<CompletionViewMeta[]>([]);
|
||||
const triggersRef = useRef<CompletionTriggerMeta[]>([]);
|
||||
const routinesRef = useRef<CompletionRoutineMeta[]>([]);
|
||||
const sequencesRef = useRef<CompletionSequenceMeta[]>([]);
|
||||
const packagesRef = useRef<CompletionPackageMeta[]>([]);
|
||||
const visibleDbsRef = useRef<string[]>([]); // Store visible databases for cross-db intellisense
|
||||
const metadataFetchKeyRef = useRef<string>('');
|
||||
const metadataContextKeyRef = useRef<string>('');
|
||||
@@ -530,6 +540,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
sharedMaterializedViewsData = materializedViewsRef.current;
|
||||
sharedTriggersData = triggersRef.current;
|
||||
sharedRoutinesData = routinesRef.current;
|
||||
sharedSequencesData = sequencesRef.current;
|
||||
sharedPackagesData = packagesRef.current;
|
||||
sharedColumnsCacheData = columnsCacheRef.current;
|
||||
}, [isActive, currentDb, currentConnectionId, connections]);
|
||||
|
||||
@@ -573,6 +585,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
materializedViewsRef.current,
|
||||
triggersRef.current,
|
||||
routinesRef.current,
|
||||
sequencesRef.current,
|
||||
packagesRef.current,
|
||||
);
|
||||
if (!hoverTarget) continue;
|
||||
|
||||
@@ -620,6 +634,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
materializedViewsRef.current,
|
||||
triggersRef.current,
|
||||
routinesRef.current,
|
||||
sequencesRef.current,
|
||||
packagesRef.current,
|
||||
);
|
||||
if (!hoverTarget) {
|
||||
return false;
|
||||
@@ -1018,6 +1034,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
const allMaterializedViews: CompletionViewMeta[] = [];
|
||||
const allTriggers: CompletionTriggerMeta[] = [];
|
||||
const allRoutines: CompletionRoutineMeta[] = [];
|
||||
const allSequences: CompletionSequenceMeta[] = [];
|
||||
const allPackages: CompletionPackageMeta[] = [];
|
||||
const metadataDialect = normalizeMetadataDialect(conn);
|
||||
const syncMetadataSnapshot = () => {
|
||||
if (cancelled) {
|
||||
@@ -1029,6 +1047,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
materializedViewsRef.current = [...allMaterializedViews];
|
||||
triggersRef.current = [...allTriggers];
|
||||
routinesRef.current = [...allRoutines];
|
||||
sequencesRef.current = [...allSequences];
|
||||
packagesRef.current = [...allPackages];
|
||||
if (isActive) {
|
||||
sharedTablesData = tablesRef.current;
|
||||
sharedAllColumnsData = allColumnsRef.current;
|
||||
@@ -1036,6 +1056,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
sharedMaterializedViewsData = materializedViewsRef.current;
|
||||
sharedTriggersData = triggersRef.current;
|
||||
sharedRoutinesData = routinesRef.current;
|
||||
sharedSequencesData = sequencesRef.current;
|
||||
sharedPackagesData = packagesRef.current;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -1211,6 +1233,64 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
});
|
||||
});
|
||||
if (!syncMetadataSnapshot()) return;
|
||||
|
||||
const sequenceResults = await queryCompletionMetadataRowsBySpecs(
|
||||
config,
|
||||
dbName,
|
||||
buildCompletionSequencesMetadataQuerySpecs(metadataDialect, dbName),
|
||||
);
|
||||
if (cancelled) return;
|
||||
const seenSequences = new Set<string>();
|
||||
sequenceResults.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rawSequenceName = String(getCaseInsensitiveValue(row, ['sequence_name', 'name']) || '').trim() || getFirstRowValue(row);
|
||||
if (!rawSequenceName) return;
|
||||
const schemaName = String(getCaseInsensitiveValue(row, ['schema_name', 'sequence_owner', 'owner', 'db', 'database']) || '').trim();
|
||||
const sequenceParts = splitSidebarQualifiedName(rawSequenceName);
|
||||
const resolvedSchemaName = String(schemaName || sequenceParts.schemaName || '').trim();
|
||||
const resolvedSequenceName = String(sequenceParts.objectName || rawSequenceName).trim();
|
||||
const qualifiedSequenceName = buildQualifiedCompletionName(resolvedSchemaName, resolvedSequenceName);
|
||||
if (!qualifiedSequenceName) return;
|
||||
const uniqueKey = `${dbName.toLowerCase()}@@${qualifiedSequenceName.toLowerCase()}`;
|
||||
if (seenSequences.has(uniqueKey)) return;
|
||||
seenSequences.add(uniqueKey);
|
||||
allSequences.push({
|
||||
dbName,
|
||||
sequenceName: qualifiedSequenceName,
|
||||
schemaName: resolvedSchemaName || splitSidebarQualifiedName(qualifiedSequenceName).schemaName || undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
if (!syncMetadataSnapshot()) return;
|
||||
|
||||
const packageResults = await queryCompletionMetadataRowsBySpecs(
|
||||
config,
|
||||
dbName,
|
||||
buildCompletionPackagesMetadataQuerySpecs(metadataDialect, dbName),
|
||||
);
|
||||
if (cancelled) return;
|
||||
const seenPackages = new Set<string>();
|
||||
packageResults.forEach((queryResult) => {
|
||||
queryResult.rows.forEach((row) => {
|
||||
const rawPackageName = String(getCaseInsensitiveValue(row, ['package_name', 'object_name', 'name']) || '').trim() || getFirstRowValue(row);
|
||||
if (!rawPackageName) return;
|
||||
const schemaName = String(getCaseInsensitiveValue(row, ['schema_name', 'owner', 'db', 'database']) || '').trim();
|
||||
const packageParts = splitSidebarQualifiedName(rawPackageName);
|
||||
const resolvedSchemaName = String(schemaName || packageParts.schemaName || '').trim();
|
||||
const resolvedPackageName = String(packageParts.objectName || rawPackageName).trim();
|
||||
const qualifiedPackageName = buildQualifiedCompletionName(resolvedSchemaName, resolvedPackageName);
|
||||
if (!qualifiedPackageName) return;
|
||||
const uniqueKey = `${dbName.toLowerCase()}@@${qualifiedPackageName.toLowerCase()}`;
|
||||
if (seenPackages.has(uniqueKey)) return;
|
||||
seenPackages.add(uniqueKey);
|
||||
allPackages.push({
|
||||
dbName,
|
||||
packageName: qualifiedPackageName,
|
||||
schemaName: resolvedSchemaName || splitSidebarQualifiedName(qualifiedPackageName).schemaName || undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
if (!syncMetadataSnapshot()) return;
|
||||
}
|
||||
|
||||
if (!syncMetadataSnapshot()) return;
|
||||
@@ -1350,6 +1430,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
materializedViewsRef.current,
|
||||
triggersRef.current,
|
||||
routinesRef.current,
|
||||
sequencesRef.current,
|
||||
packagesRef.current,
|
||||
primaryShortcutModifierLabel,
|
||||
);
|
||||
if (decorations.length === 0) {
|
||||
@@ -1603,6 +1685,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
materializedViewsRef.current,
|
||||
triggersRef.current,
|
||||
routinesRef.current,
|
||||
sequencesRef.current,
|
||||
packagesRef.current,
|
||||
);
|
||||
if (!navigationTarget) {
|
||||
return;
|
||||
@@ -1719,6 +1803,60 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigationTarget.type === 'sequence') {
|
||||
const targetSequenceName = String(navigationTarget.sequenceName || '').trim();
|
||||
if (!targetSequenceName) return;
|
||||
const targetSchemaName = String(navigationTarget.schemaName || '').trim();
|
||||
const sidebarLocateKey = `sequence-def-${connectionId}-${targetDbName}-${targetSequenceName}`;
|
||||
addTab({
|
||||
id: `sequence-def-${connectionId}-${targetDbName}-${targetSequenceName}`,
|
||||
title: translate('sidebar.tab.sequence_definition', { name: targetSequenceName }),
|
||||
type: 'sequence-def',
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
sequenceName: targetSequenceName,
|
||||
schemaName: targetSchemaName || undefined,
|
||||
sidebarLocateKey,
|
||||
});
|
||||
dispatchQueryEditorSidebarLocate({
|
||||
tabId: sidebarLocateKey,
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
sequenceName: targetSequenceName,
|
||||
tableName: targetSequenceName,
|
||||
schemaName: targetSchemaName,
|
||||
objectGroup: 'sequences',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigationTarget.type === 'package') {
|
||||
const targetPackageName = String(navigationTarget.packageName || '').trim();
|
||||
if (!targetPackageName) return;
|
||||
const targetSchemaName = String(navigationTarget.schemaName || '').trim();
|
||||
const sidebarLocateKey = `package-def-${connectionId}-${targetDbName}-${targetPackageName}`;
|
||||
addTab({
|
||||
id: `package-def-${connectionId}-${targetDbName}-${targetPackageName}`,
|
||||
title: translate('sidebar.tab.package_definition', { name: targetPackageName }),
|
||||
type: 'package-def',
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
packageName: targetPackageName,
|
||||
schemaName: targetSchemaName || undefined,
|
||||
sidebarLocateKey,
|
||||
});
|
||||
dispatchQueryEditorSidebarLocate({
|
||||
tabId: sidebarLocateKey,
|
||||
connectionId,
|
||||
dbName: targetDbName,
|
||||
packageName: targetPackageName,
|
||||
tableName: targetPackageName,
|
||||
schemaName: targetSchemaName,
|
||||
objectGroup: 'packages',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const targetRoutineName = String(navigationTarget.routineName || '').trim();
|
||||
if (!targetRoutineName) return;
|
||||
const routineTypeLabel = navigationTarget.routineType === 'PROCEDURE'
|
||||
@@ -1874,6 +2012,8 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
sharedMaterializedViewsData,
|
||||
sharedTriggersData,
|
||||
sharedRoutinesData,
|
||||
sharedSequencesData,
|
||||
sharedPackagesData,
|
||||
);
|
||||
if (!hoverTarget) {
|
||||
return null;
|
||||
@@ -2695,6 +2835,14 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return findSqlStatementRanges(sql).map((range) => range.text);
|
||||
};
|
||||
|
||||
const containsOraclePlsqlDefinition = (statements: string[]): boolean => (
|
||||
statements.some((statement) => /^\s*(?:(?:--[^\n]*|\/\*[\s\S]*?\*\/)\s*)*CREATE\s+(?:OR\s+REPLACE\s+)?(?:EDITIONABLE\s+|NONEDITIONABLE\s+)?(?:PROCEDURE|FUNCTION|PACKAGE|TRIGGER)\b/i.test(statement))
|
||||
);
|
||||
|
||||
const normalizeOracleSqlPlusSlashTerminators = (sql: string): string => (
|
||||
String(sql || '').replace(/(^|\n)([ \t]*\/[ \t]*);+([ \t]*(?:--[^\n]*)?)(?=\n|$)/g, '$1$2$3')
|
||||
);
|
||||
|
||||
const getSelectedSQL = (): string => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return '';
|
||||
@@ -3454,7 +3602,10 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
|
||||
return { ...plan, executedSql: result.sql };
|
||||
});
|
||||
const executableStatements = executablePlans.map((plan) => plan.executedSql);
|
||||
const fullSQL = executableStatements.join(';\n');
|
||||
const shouldPreserveOraclePlsqlBatch = isOracleLikeDialect(normalizedDbType) && containsOraclePlsqlDefinition(sourceStatements);
|
||||
const fullSQL = shouldPreserveOraclePlsqlBatch
|
||||
? normalizeOracleSqlPlusSlashTerminators(normalizedRawSQL)
|
||||
: executableStatements.join(';\n');
|
||||
|
||||
const startTime = Date.now();
|
||||
let queryId: string;
|
||||
|
||||
@@ -23,6 +23,8 @@ export type CompletionColumnMeta = {dbName: string, tableName: string, name: str
|
||||
export type CompletionViewMeta = {dbName: string, viewName: string, schemaName?: string};
|
||||
export type CompletionTriggerMeta = {dbName: string, triggerName: string, tableName: string, schemaName?: string};
|
||||
export type CompletionRoutineMeta = {dbName: string, routineName: string, routineType: string, schemaName?: string};
|
||||
export type CompletionSequenceMeta = {dbName: string, sequenceName: string, schemaName?: string};
|
||||
export type CompletionPackageMeta = {dbName: string, packageName: string, schemaName?: string};
|
||||
|
||||
export const QUERY_LOCATOR_ALIAS_PREFIX = '__gonavi_locator_';
|
||||
const QUERY_LOCATOR_METADATA_TIMEOUT_MS = 1500;
|
||||
@@ -946,6 +948,42 @@ export const buildCompletionFunctionsMetadataQuerySpecs = (dialect: string, dbNa
|
||||
}
|
||||
};
|
||||
|
||||
export const buildCompletionSequencesMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeMetadataSqlLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
case 'dameng':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT SEQUENCE_OWNER AS schema_name, SEQUENCE_NAME AS sequence_name FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = '${safeDbName.toUpperCase()}' ORDER BY SEQUENCE_NAME`
|
||||
: `SELECT SEQUENCE_NAME AS sequence_name FROM USER_SEQUENCES ORDER BY SEQUENCE_NAME`,
|
||||
},
|
||||
]);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const buildCompletionPackagesMetadataQuerySpecs = (dialect: string, dbName: string): MetadataQuerySpec[] => {
|
||||
const safeDbName = escapeMetadataSqlLiteral(dbName);
|
||||
switch (dialect) {
|
||||
case 'oracle':
|
||||
case 'dm':
|
||||
case 'dameng':
|
||||
return normalizeMetadataQuerySpecs([
|
||||
{
|
||||
sql: safeDbName
|
||||
? `SELECT OWNER AS schema_name, OBJECT_NAME AS package_name FROM ALL_OBJECTS WHERE OWNER = '${safeDbName.toUpperCase()}' AND OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME`
|
||||
: `SELECT OBJECT_NAME AS package_name FROM USER_OBJECTS WHERE OBJECT_TYPE = 'PACKAGE' ORDER BY OBJECT_NAME`,
|
||||
},
|
||||
]);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const queryCompletionMetadataRowsBySpecs = async (
|
||||
config: Record<string, any>,
|
||||
dbName: string,
|
||||
@@ -979,7 +1017,9 @@ export type QueryEditorNavigationTarget =
|
||||
| { type: 'view'; dbName: string; viewName: string; schemaName?: string }
|
||||
| { type: 'materialized-view'; dbName: string; viewName: string; schemaName?: string }
|
||||
| { type: 'trigger'; dbName: string; triggerName: string; tableName: string; schemaName?: string }
|
||||
| { type: 'routine'; dbName: string; routineName: string; routineType: string; schemaName?: string };
|
||||
| { type: 'routine'; dbName: string; routineName: string; routineType: string; schemaName?: string }
|
||||
| { type: 'sequence'; dbName: string; sequenceName: string; schemaName?: string }
|
||||
| { type: 'package'; dbName: string; packageName: string; schemaName?: string };
|
||||
|
||||
export type QueryEditorHoverTarget =
|
||||
| { kind: 'database'; dbName: string; range: { startColumn: number; endColumn: number } }
|
||||
@@ -988,6 +1028,8 @@ export type QueryEditorHoverTarget =
|
||||
| { kind: 'materialized-view'; dbName: string; viewName: string; schemaName?: string; range: { startColumn: number; endColumn: number } }
|
||||
| { kind: 'trigger'; dbName: string; triggerName: string; tableName: string; schemaName?: string; range: { startColumn: number; endColumn: number } }
|
||||
| { kind: 'routine'; dbName: string; routineName: string; routineType: string; schemaName?: string; range: { startColumn: number; endColumn: number } }
|
||||
| { kind: 'sequence'; dbName: string; sequenceName: string; schemaName?: string; range: { startColumn: number; endColumn: number } }
|
||||
| { kind: 'package'; dbName: string; packageName: string; schemaName?: string; range: { startColumn: number; endColumn: number } }
|
||||
| { kind: 'column'; dbName: string; tableName: string; columnName: string; type?: string; comment?: string; schemaName?: string; range: { startColumn: number; endColumn: number } };
|
||||
|
||||
export const QUERY_EDITOR_IDENTIFIER_CHAR_REGEX = /[A-Za-z0-9_$`"\[\].]/;
|
||||
@@ -1438,6 +1480,10 @@ export const buildQueryEditorHoverMarkdown = (target: QueryEditorHoverTarget): s
|
||||
return `${buildObjectInfoTitle('trigger_viewer.field.trigger', target.triggerName)}\n\n${buildObjectInfoLabel('query_editor.object_info.label.database', target.dbName)}\n\n${buildObjectInfoLabel('query_editor.object_info.label.table', target.tableName)}${target.schemaName ? `\n\n${buildObjectInfoLabel('query_editor.object_info.label.schema', target.schemaName)}` : ''}`;
|
||||
case 'routine':
|
||||
return `${buildObjectInfoTitle(target.routineType === 'PROCEDURE' ? 'sidebar.object.procedure' : 'sidebar.object.function', target.routineName)}\n\n${buildObjectInfoLabel('query_editor.object_info.label.database', target.dbName)}${target.schemaName ? `\n\n${buildObjectInfoLabel('query_editor.object_info.label.schema', target.schemaName)}` : ''}`;
|
||||
case 'sequence':
|
||||
return `${buildObjectInfoTitle('definition_viewer.object.sequence', target.sequenceName)}\n\n${buildObjectInfoLabel('query_editor.object_info.label.database', target.dbName)}${target.schemaName ? `\n\n${buildObjectInfoLabel('query_editor.object_info.label.schema', target.schemaName)}` : ''}`;
|
||||
case 'package':
|
||||
return `${buildObjectInfoTitle('definition_viewer.object.package', target.packageName)}\n\n${buildObjectInfoLabel('query_editor.object_info.label.database', target.dbName)}${target.schemaName ? `\n\n${buildObjectInfoLabel('query_editor.object_info.label.schema', target.schemaName)}` : ''}`;
|
||||
case 'column':
|
||||
return `${buildObjectInfoTitle('query_editor.object_info.column', target.columnName)}${target.type ? `\n\n${buildObjectInfoLabel('query_editor.object_info.label.type', target.type)}` : ''}\n\n${buildObjectInfoLabel('query_editor.object_info.label.table', target.tableName)}\n\n${buildObjectInfoLabel('query_editor.object_info.label.database', target.dbName)}${target.schemaName ? `\n\n${buildObjectInfoLabel('query_editor.object_info.label.schema', target.schemaName)}` : ''}${appendComment(target.comment)}`;
|
||||
default:
|
||||
@@ -1540,6 +1586,8 @@ export const resolveQueryEditorNavigationTarget = (
|
||||
materializedViews: CompletionViewMeta[] = [],
|
||||
triggers: CompletionTriggerMeta[] = [],
|
||||
routines: CompletionRoutineMeta[] = [],
|
||||
sequences: CompletionSequenceMeta[] = [],
|
||||
packages: CompletionPackageMeta[] = [],
|
||||
): QueryEditorNavigationTarget | null => {
|
||||
const text = String(lineContent || '');
|
||||
if (!text) return null;
|
||||
@@ -1601,6 +1649,8 @@ export const resolveQueryEditorNavigationTarget = (
|
||||
...buildObjectNameMeta(routine.dbName, routine.routineName, routine.schemaName),
|
||||
routineType: String(routine.routineType || 'FUNCTION').trim().toUpperCase() || 'FUNCTION',
|
||||
}));
|
||||
const sequenceMetas = sequences.map((sequence) => buildObjectNameMeta(sequence.dbName, sequence.sequenceName, sequence.schemaName));
|
||||
const packageMetas = packages.map((pkg) => buildObjectNameMeta(pkg.dbName, pkg.packageName, pkg.schemaName));
|
||||
|
||||
const findTable = (candidateDbName: string, candidateTableName: string, schemaName = ''): QueryEditorNavigationTarget | null => {
|
||||
const normalizedDbName = String(candidateDbName || '').trim().toLowerCase();
|
||||
@@ -1728,12 +1778,36 @@ export const resolveQueryEditorNavigationTarget = (
|
||||
};
|
||||
};
|
||||
|
||||
const findSequence = (candidateDbName: string, candidateSequenceName: string, schemaName = ''): QueryEditorNavigationTarget | null => {
|
||||
const matched = findNamedObject(sequenceMetas, candidateDbName, candidateSequenceName, schemaName);
|
||||
if (!matched) return null;
|
||||
return {
|
||||
type: 'sequence',
|
||||
dbName: matched.dbName,
|
||||
sequenceName: matched.rawObjectName,
|
||||
schemaName: matched.schemaName || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const findPackage = (candidateDbName: string, candidatePackageName: string, schemaName = ''): QueryEditorNavigationTarget | null => {
|
||||
const matched = findNamedObject(packageMetas, candidateDbName, candidatePackageName, schemaName);
|
||||
if (!matched) return null;
|
||||
return {
|
||||
type: 'package',
|
||||
dbName: matched.dbName,
|
||||
packageName: matched.rawObjectName,
|
||||
schemaName: matched.schemaName || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const findObjectInPriorityOrder = (candidateDbName: string, candidateObjectName: string, schemaName = ''): QueryEditorNavigationTarget | null => (
|
||||
findTable(candidateDbName, candidateObjectName, schemaName)
|
||||
|| findView(candidateDbName, candidateObjectName, schemaName)
|
||||
|| findMaterializedView(candidateDbName, candidateObjectName, schemaName)
|
||||
|| findTrigger(candidateDbName, candidateObjectName, schemaName)
|
||||
|| findRoutine(candidateDbName, candidateObjectName, schemaName)
|
||||
|| findSequence(candidateDbName, candidateObjectName, schemaName)
|
||||
|| findPackage(candidateDbName, candidateObjectName, schemaName)
|
||||
);
|
||||
|
||||
if (parts.length === 1) {
|
||||
@@ -1755,6 +1829,14 @@ export const resolveQueryEditorNavigationTarget = (
|
||||
|
||||
const [dbName, schemaName, tableName] = parts;
|
||||
if (!visibleDbSet.has(dbName.toLowerCase())) {
|
||||
const schemaQualifiedSequence = findSequence(currentDbName, schemaName, dbName);
|
||||
if (schemaQualifiedSequence && ['nextval', 'currval'].includes(tableName.toLowerCase())) {
|
||||
return schemaQualifiedSequence;
|
||||
}
|
||||
const schemaQualifiedPackage = findPackage(currentDbName, schemaName, dbName);
|
||||
if (schemaQualifiedPackage) {
|
||||
return schemaQualifiedPackage;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return findObjectInPriorityOrder(dbName, tableName, schemaName);
|
||||
@@ -1772,6 +1854,8 @@ export const resolveQueryEditorHoverTarget = (
|
||||
materializedViews: CompletionViewMeta[] = [],
|
||||
triggers: CompletionTriggerMeta[] = [],
|
||||
routines: CompletionRoutineMeta[] = [],
|
||||
sequences: CompletionSequenceMeta[] = [],
|
||||
packages: CompletionPackageMeta[] = [],
|
||||
): QueryEditorHoverTarget | null => {
|
||||
const text = String(lineContent || '');
|
||||
if (!text) return null;
|
||||
@@ -1820,6 +1904,8 @@ export const resolveQueryEditorHoverTarget = (
|
||||
materializedViews,
|
||||
triggers,
|
||||
routines,
|
||||
sequences,
|
||||
packages,
|
||||
);
|
||||
if (navigationTarget) {
|
||||
if (navigationTarget.type === 'database') {
|
||||
@@ -1845,7 +1931,13 @@ export const resolveQueryEditorHoverTarget = (
|
||||
if (navigationTarget.type === 'trigger') {
|
||||
return { kind: 'trigger', dbName: navigationTarget.dbName, triggerName: navigationTarget.triggerName, tableName: navigationTarget.tableName, schemaName: navigationTarget.schemaName, range };
|
||||
}
|
||||
return { kind: 'routine', dbName: navigationTarget.dbName, routineName: navigationTarget.routineName, routineType: navigationTarget.routineType, schemaName: navigationTarget.schemaName, range };
|
||||
if (navigationTarget.type === 'routine') {
|
||||
return { kind: 'routine', dbName: navigationTarget.dbName, routineName: navigationTarget.routineName, routineType: navigationTarget.routineType, schemaName: navigationTarget.schemaName, range };
|
||||
}
|
||||
if (navigationTarget.type === 'sequence') {
|
||||
return { kind: 'sequence', dbName: navigationTarget.dbName, sequenceName: navigationTarget.sequenceName, schemaName: navigationTarget.schemaName, range };
|
||||
}
|
||||
return { kind: 'package', dbName: navigationTarget.dbName, packageName: navigationTarget.packageName, schemaName: navigationTarget.schemaName, range };
|
||||
}
|
||||
|
||||
const findColumnTarget = (dbName: string, tableName: string, columnName: string): QueryEditorHoverTarget | null => {
|
||||
@@ -1930,6 +2022,8 @@ export const resolveQueryEditorNavigationDecorations = (
|
||||
materializedViews: CompletionViewMeta[] = [],
|
||||
triggers: CompletionTriggerMeta[] = [],
|
||||
routines: CompletionRoutineMeta[] = [],
|
||||
sequences: CompletionSequenceMeta[] = [],
|
||||
packages: CompletionPackageMeta[] = [],
|
||||
shortcutModifierLabel = 'Ctrl/Cmd',
|
||||
): Array<{ startColumn: number; endColumn: number; hoverMessage: string }> => {
|
||||
const text = String(lineContent || '');
|
||||
@@ -1948,6 +2042,8 @@ export const resolveQueryEditorNavigationDecorations = (
|
||||
materializedViews,
|
||||
triggers,
|
||||
routines,
|
||||
sequences,
|
||||
packages,
|
||||
);
|
||||
if (!navigationTarget) return [];
|
||||
|
||||
@@ -1977,6 +2073,16 @@ export const resolveQueryEditorNavigationDecorations = (
|
||||
shortcut: shortcutModifierLabel,
|
||||
});
|
||||
}
|
||||
if (navigationTarget.type === 'sequence') {
|
||||
return translate('query_editor.hover.open_sequence_with_shortcut', {
|
||||
shortcut: shortcutModifierLabel,
|
||||
});
|
||||
}
|
||||
if (navigationTarget.type === 'package') {
|
||||
return translate('query_editor.hover.open_package_with_shortcut', {
|
||||
shortcut: shortcutModifierLabel,
|
||||
});
|
||||
}
|
||||
return navigationTarget.routineType === 'PROCEDURE'
|
||||
? translate('query_editor.hover.open_procedure_with_shortcut', {
|
||||
shortcut: shortcutModifierLabel,
|
||||
|
||||
@@ -1191,6 +1191,8 @@ describe("i18n catalog", () => {
|
||||
"query_editor.hover.open_trigger_with_shortcut",
|
||||
"query_editor.hover.open_procedure_with_shortcut",
|
||||
"query_editor.hover.open_function_with_shortcut",
|
||||
"query_editor.hover.open_sequence_with_shortcut",
|
||||
"query_editor.hover.open_package_with_shortcut",
|
||||
] as const;
|
||||
const source = readQueryEditorHelpersSource();
|
||||
const hoverMessageSource = sliceBetween(
|
||||
@@ -1929,7 +1931,7 @@ describe("i18n catalog", () => {
|
||||
const oracleRowIdFallbackSource = sliceBetween(
|
||||
source,
|
||||
" if (executableAppendExpressions.length > 0 && isOracleLikeDialect(dbType) && selectInfo.selectsBareAll) {",
|
||||
" plan.executedSql = appendQuerySelectExpressions(statement, executableAppendExpressions);",
|
||||
" plan.executedSql = appendQuerySelectExpressions(executableStatement, executableAppendExpressions);",
|
||||
);
|
||||
|
||||
for (const language of SUPPORTED_LANGUAGES) {
|
||||
|
||||
@@ -6109,7 +6109,9 @@
|
||||
"query_editor.format.snippet_settings": "Snippet-Einstellungen...",
|
||||
"query_editor.hover.open_function_with_shortcut": "{{shortcut}} + klicken, um diese Funktion zu öffnen",
|
||||
"query_editor.hover.open_materialized_view_with_shortcut": "{{shortcut}} + klicken, um diese materialisierte Ansicht zu öffnen",
|
||||
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + klicken, um dieses Paket zu öffnen",
|
||||
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + klicken, um diese gespeicherte Prozedur zu öffnen",
|
||||
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + klicken, um diese Sequenz zu öffnen",
|
||||
"query_editor.hover.open_table_with_shortcut": "{{shortcut}} + klicken, um diese Tabelle zu öffnen",
|
||||
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + klicken, um diesen Trigger zu öffnen",
|
||||
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + klicken, um diese Ansicht zu öffnen",
|
||||
|
||||
@@ -6109,7 +6109,9 @@
|
||||
"query_editor.format.snippet_settings": "Snippet settings...",
|
||||
"query_editor.hover.open_function_with_shortcut": "{{shortcut}} + click to open this function",
|
||||
"query_editor.hover.open_materialized_view_with_shortcut": "{{shortcut}} + click to open this materialized view",
|
||||
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + click to open this package",
|
||||
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + click to open this stored procedure",
|
||||
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + click to open this sequence",
|
||||
"query_editor.hover.open_table_with_shortcut": "{{shortcut}} + click to open this table",
|
||||
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + click to open this trigger",
|
||||
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + click to open this view",
|
||||
|
||||
@@ -6109,7 +6109,9 @@
|
||||
"query_editor.format.snippet_settings": "スニペット設定...",
|
||||
"query_editor.hover.open_function_with_shortcut": "{{shortcut}} + クリックでこの関数を開く",
|
||||
"query_editor.hover.open_materialized_view_with_shortcut": "{{shortcut}} + クリックでこのマテリアライズドビューを開く",
|
||||
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + クリックでこのパッケージを開く",
|
||||
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + クリックでこのストアドプロシージャを開く",
|
||||
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + クリックでこのシーケンスを開く",
|
||||
"query_editor.hover.open_table_with_shortcut": "{{shortcut}} + クリックでこのテーブルを開く",
|
||||
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + クリックでこのトリガーを開く",
|
||||
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + クリックでこのビューを開く",
|
||||
|
||||
@@ -6109,7 +6109,9 @@
|
||||
"query_editor.format.snippet_settings": "Настройки сниппетов...",
|
||||
"query_editor.hover.open_function_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть эту функцию",
|
||||
"query_editor.hover.open_materialized_view_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть это материализованное представление",
|
||||
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть этот пакет",
|
||||
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть эту хранимую процедуру",
|
||||
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть эту последовательность",
|
||||
"query_editor.hover.open_table_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть эту таблицу",
|
||||
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть этот триггер",
|
||||
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + щелчок, чтобы открыть это представление",
|
||||
|
||||
@@ -6109,7 +6109,9 @@
|
||||
"query_editor.format.snippet_settings": "代码片段管理...",
|
||||
"query_editor.hover.open_function_with_shortcut": "{{shortcut}} + 点击打开该函数",
|
||||
"query_editor.hover.open_materialized_view_with_shortcut": "{{shortcut}} + 点击打开该物化视图",
|
||||
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + 点击打开该存储包",
|
||||
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + 点击打开该存储过程",
|
||||
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + 点击打开该序列",
|
||||
"query_editor.hover.open_table_with_shortcut": "{{shortcut}} + 点击打开该表",
|
||||
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + 点击打开该触发器",
|
||||
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + 点击打开该视图",
|
||||
|
||||
@@ -6109,7 +6109,9 @@
|
||||
"query_editor.format.snippet_settings": "程式碼片段管理...",
|
||||
"query_editor.hover.open_function_with_shortcut": "{{shortcut}} + 點擊開啟該函式",
|
||||
"query_editor.hover.open_materialized_view_with_shortcut": "{{shortcut}} + 點擊開啟該實體化檢視",
|
||||
"query_editor.hover.open_package_with_shortcut": "{{shortcut}} + 點擊開啟該套件",
|
||||
"query_editor.hover.open_procedure_with_shortcut": "{{shortcut}} + 點擊開啟該預存程序",
|
||||
"query_editor.hover.open_sequence_with_shortcut": "{{shortcut}} + 點擊開啟該序列",
|
||||
"query_editor.hover.open_table_with_shortcut": "{{shortcut}} + 點擊開啟該資料表",
|
||||
"query_editor.hover.open_trigger_with_shortcut": "{{shortcut}} + 點擊開啟該觸發器",
|
||||
"query_editor.hover.open_view_with_shortcut": "{{shortcut}} + 點擊開啟該檢視",
|
||||
|
||||
Reference in New Issue
Block a user