diff --git a/docs/需求追踪/需求进度追踪-Oracle对象跳转与存储过程修改执行-20260625.md b/docs/需求追踪/需求进度追踪-Oracle对象跳转与存储过程修改执行-20260625.md new file mode 100644 index 0000000..752fec7 --- /dev/null +++ b/docs/需求追踪/需求进度追踪-Oracle对象跳转与存储过程修改执行-20260625.md @@ -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 diff --git a/frontend/src/components/DefinitionViewer.object-edit.test.tsx b/frontend/src/components/DefinitionViewer.object-edit.test.tsx index a896453..42528b4 100644 --- a/frontend/src/components/DefinitionViewer.object-edit.test.tsx +++ b/frontend/src/components/DefinitionViewer.object-edit.test.tsx @@ -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({ diff --git a/frontend/src/components/DefinitionViewer.tsx b/frontend/src/components/DefinitionViewer.tsx index 3724307..29b8136 100644 --- a/frontend/src/components/DefinitionViewer.tsx +++ b/frontend/src/components/DefinitionViewer.tsx @@ -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};`; }; diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index c580dc4..06cd937 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -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(); + }); + 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(); + }); + 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'; diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index f34d737..0cb70fb 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -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 = {}; const QUERY_EDITOR_LAZY_VISIBLE_DB_COMPLETION_LIMIT = 10; const sharedLazyTablesCache: Record = {}; @@ -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([]); const triggersRef = useRef([]); const routinesRef = useRef([]); + const sequencesRef = useRef([]); + const packagesRef = useRef([]); const visibleDbsRef = useRef([]); // Store visible databases for cross-db intellisense const metadataFetchKeyRef = useRef(''); const metadataContextKeyRef = useRef(''); @@ -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(); + 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(); + 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; diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts index 7f2770d..9e5f8a4 100644 --- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts @@ -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, 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, diff --git a/frontend/src/i18n/catalog.test.ts b/frontend/src/i18n/catalog.test.ts index d609084..4ab3bbf 100644 --- a/frontend/src/i18n/catalog.test.ts +++ b/frontend/src/i18n/catalog.test.ts @@ -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) { diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 4907167..113e1b2 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -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", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index a534872..dbe8da7 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -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", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 7efc07f..e0d93bc 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -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}} + クリックでこのビューを開く", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index 4e1756d..7c60819 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -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}} + щелчок, чтобы открыть это представление", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 9c2371c..48f1aef 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -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}} + 点击打开该视图", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 643a5a5..a6d2de1 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -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}} + 點擊開啟該檢視",