From 9275e934216bffaa3af948a5161c9ccb9d59cb8b Mon Sep 17 00:00:00 2001 From: Syngnat Date: Fri, 24 Jul 2026 12:22:37 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-editor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20Oracle=20=E9=95=BF=E8=84=9A=E6=9C=AC=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 线性屏蔽 SQL 注释和字符串,避免 PL/SQL 定义检测发生正则灾难性回溯 - 仅为顶层 SELECT 解析结果表定位,避免匿名块内部查询触发元数据加载 - 新增长注释匿名块与多结果回归,验证 Oracle 过程和 SQLPlus 分隔符兼容性 --- .../QueryEditor.results-and-drop.test.tsx | 57 +++++++++++++++++++ frontend/src/components/QueryEditor.tsx | 5 +- .../queryEditor/QueryEditorHelpers.ts | 4 ++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/QueryEditor.results-and-drop.test.tsx b/frontend/src/components/QueryEditor.results-and-drop.test.tsx index 40672c3d..0bdd0fdf 100644 --- a/frontend/src/components/QueryEditor.results-and-drop.test.tsx +++ b/frontend/src/components/QueryEditor.results-and-drop.test.tsx @@ -916,6 +916,63 @@ describe('QueryEditor external SQL save', () => { renderer?.unmount(); }); + it('executes a long commented Oracle anonymous block without blocking the UI thread', async () => { + storeState.appearance.uiVersion = 'v2'; + storeState.connections[0].config.type = 'oracle'; + storeState.connections[0].config.database = 'ORCLPDB1'; + const columns = Array.from( + { length: 42 }, + (_, index) => ` column_${index + 1} VARCHAR2(100) DEFAULT 'value_${index + 1}'`, + ).join(',\n'); + const sql = [ + '-- ------------------------------------------------------------', + '-- Long Oracle anonymous setup block', + '-- ------------------------------------------------------------', + 'DECLARE', + ' v_cnt NUMBER;', + 'BEGIN', + ' SELECT COUNT(1) INTO v_cnt', + ' FROM user_tables', + " WHERE table_name = 'GONAVI_REPRO_TABLE';", + ' IF v_cnt = 0 THEN', + " EXECUTE IMMEDIATE '\n CREATE TABLE gonavi_repro_table (\n" + columns + "\n )\n ';", + ' END IF;', + 'END;', + '/', + ].join('\n'); + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: Array.from({ length: 52 }, (_, index) => ({ + statementIndex: index + 1, + columns: ['affectedRows'], + rows: [{ affectedRows: 0 }], + })), + }); + + let renderer!: ReactTestRenderer; + await act(async () => { + renderer = create(); + }); + + const runButton = findByClassName(renderer, 'gn-v2-query-toolbar-run-action'); + await act(async () => { + await runButton.props.onClick(); + await Promise.resolve(); + await Promise.resolve(); + }); + const resultTabs = renderer.root.findAll((node) => + node.type === 'button' && String(node.props?.['data-tab-key'] || '').startsWith('result-'), + ); + + expect(backendApp.DBQueryMulti).toHaveBeenCalledOnce(); + expect(backendApp.DBGetColumns).not.toHaveBeenCalled(); + expect(backendApp.DBGetIndexes).not.toHaveBeenCalled(); + expect(resultTabs).toHaveLength(52); + await act(async () => { + renderer.unmount(); + }); + }); + it('runs the whole Oracle procedure when the cursor is in the exception tail', async () => { storeState.connections[0].config.type = 'oracle'; storeState.connections[0].config.database = 'ORCLPDB1'; diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 01c10a7c..5aa12b56 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -167,6 +167,7 @@ import { queryCompletionMetadataRowsBySpecs, readSidebarSqlDropText, matchLeadingSelectTableReference, + maskQueryEditorSqlLiteralsAndComments, materializeBoundedQueryEditorCompletionBatches, resolveNewQueryDefaultTemplate, resolveEventTargetNode, @@ -6253,7 +6254,9 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc }; 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)) + statements.some((statement) => /^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:EDITIONABLE\s+|NONEDITIONABLE\s+)?(?:PROCEDURE|FUNCTION|PACKAGE|TRIGGER)\b/i.test( + maskQueryEditorSqlLiteralsAndComments(statement), + )) ); const normalizeOracleSqlPlusSlashTerminators = (sql: string): string => ( diff --git a/frontend/src/components/queryEditor/QueryEditorHelpers.ts b/frontend/src/components/queryEditor/QueryEditorHelpers.ts index 19b177ea..4935019f 100644 --- a/frontend/src/components/queryEditor/QueryEditorHelpers.ts +++ b/frontend/src/components/queryEditor/QueryEditorHelpers.ts @@ -15,6 +15,7 @@ import { type EditRowLocator, } from '../../utils/rowLocator'; import { getQueryTabDraft, hasQueryTabDraft } from '../../utils/sqlFileTabDrafts'; +import { resolveSqlEditorOperationKeyword } from '../../utils/sqlEditorTransaction'; import { getColumnDefinitionKey, getColumnDefinitionName } from '../../utils/columnDefinition'; import { resolveUniqueKeyGroupsFromIndexes } from '../dataGridCopyInsert'; import { t as translate } from '../../i18n'; @@ -2709,6 +2710,9 @@ export const resolveQueryLocatorPlan = async ({ executedSql: statement, pkColumns: [], }; + if (resolveSqlEditorOperationKeyword(statement) !== 'select') { + return plan; + } const defaultSchema = isOracleLikeDialect(dbType) ? resolveOracleLikeExecutionSchemaName(config, currentDb) : '';