🐛 fix(oracle): 修复存储过程斜杠分隔执行截断

- 支持 SQL*Plus 斜杠分隔符后的可选分号,避免 Oracle 过程执行出空语句
- 光标落在过程异常尾部或斜杠分隔行时,仍选择完整 PL/SQL 定义执行
- 补充前端语句选择、QueryEditor 执行和后端 split/DBQueryMulti 回归测试
This commit is contained in:
Syngnat
2026-06-25 17:37:32 +08:00
parent f6556f25d5
commit 9ab31a7614
6 changed files with 372 additions and 0 deletions

View File

@@ -285,6 +285,103 @@ describe('sqlStatementSelection', () => {
});
});
it('keeps large Oracle procedures intact when the cursor is in the exception tail', () => {
const sql = [
'-- 修改函数/存储过程H2.cproc_tzhssr_order2sale_A1',
'-- 请确认语法兼容当前数据库后执行',
'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1(',
' p_sourceid IN VARCHAR2,',
' p_msg_out OUT NVARCHAR2',
') AS',
' v_ecnt NUMBER;',
' CURSOR cur_ware IS',
' SELECT d.goodsid, d.goodsqty',
' FROM t_order_d d',
' ORDER BY CASE',
" WHEN d.goodsqty > 0 THEN '1'",
" ELSE '2'",
' END, d.goodsid;',
'BEGIN',
' FOR row_ware IN cur_ware LOOP',
' IF row_ware.goodsqty > 0 THEN',
' BEGIN',
' SELECT COUNT(*) INTO v_ecnt FROM dual;',
' EXCEPTION',
' WHEN no_data_found THEN',
' v_ecnt := 0;',
' END;',
' ELSE',
' BEGIN',
' v_ecnt := 0;',
' END;',
' END IF;',
' END LOOP;',
" p_msg_out := '';",
'EXCEPTION',
' WHEN OTHERS THEN',
" p_msg_out := substr('订单核销失败,错误信息:' || SQLERRM || ',错误位置:' ||",
' dbms_utility.format_error_backtrace, 1, 1000);',
'END cproc_tzhssr_order2sale_A1;',
'/ -- SQLPlus delimiter from PL/SQL tools',
'SELECT 1 FROM dual;',
].join('\n');
const ranges = findSqlStatementRanges(sql).map((range) => range.text);
expect(ranges).toHaveLength(2);
expect(ranges[0]).toContain('CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1');
expect(ranges[0]).toContain('p_msg_out OUT NVARCHAR2');
expect(ranges[0]).toContain('EXCEPTION');
expect(ranges[0]).toContain('END cproc_tzhssr_order2sale_A1;');
expect(ranges[1]).toBe('SELECT 1 FROM dual');
expect(resolveExecutableSql(sql, sql.indexOf('p_msg_out := substr'))).toEqual({
sql: ranges[0],
source: 'statement',
});
expect(resolveExecutableSql(sql, sql.indexOf('/ -- SQLPlus delimiter'))).toEqual({
sql: ranges[0],
source: 'statement',
});
expect(resolveCurrentSqlStatementRange(sql, sql.indexOf('/ -- SQLPlus delimiter'))?.text).toBe(ranges[0]);
});
it('skips optional semicolons after SQL*Plus slash delimiters', () => {
const sql = [
'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1(',
' p_msg_out OUT NVARCHAR2',
') AS',
'BEGIN',
" p_msg_out := '';",
'EXCEPTION',
' WHEN OTHERS THEN',
' p_msg_out := SQLERRM;',
'END cproc_tzhssr_order2sale_A1;',
'/;',
'SELECT 1 FROM dual;',
].join('\n');
const ranges = findSqlStatementRanges(sql).map((range) => range.text);
expect(ranges).toEqual([
[
'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1(',
' p_msg_out OUT NVARCHAR2',
') AS',
'BEGIN',
" p_msg_out := '';",
'EXCEPTION',
' WHEN OTHERS THEN',
' p_msg_out := SQLERRM;',
'END cproc_tzhssr_order2sale_A1;',
].join('\n'),
'SELECT 1 FROM dual',
]);
expect(resolveExecutableSql(sql, sql.indexOf('/;'))).toEqual({
sql: ranges[0],
source: 'statement',
});
});
it('keeps Oracle PACKAGE specification and body definitions as complete executable statements', () => {
const sql = [
'CREATE OR REPLACE PACKAGE pkg_order AS',

View File

@@ -74,7 +74,13 @@ const resolveStandaloneSqlSlashLineEnd = (text: string, index: number): number |
}
let lineEnd = index + 1;
let seenOptionalSemicolon = false;
while (lineEnd < text.length && text[lineEnd] !== '\n') {
if (text[lineEnd] === ';' && !seenOptionalSemicolon) {
seenOptionalSemicolon = true;
lineEnd += 1;
continue;
}
if (text[lineEnd] === '-' && text[lineEnd + 1] === '-') {
while (lineEnd < text.length && text[lineEnd] !== '\n') {
lineEnd += 1;
@@ -89,6 +95,37 @@ const resolveStandaloneSqlSlashLineEnd = (text: string, index: number): number |
return lineEnd;
};
const resolveStandaloneSqlSlashLineAtOffset = (
text: string,
offset: number,
): { lineStart: number; lineEnd: number; slashIndex: number } | null => {
const lineStart = text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
const nextLineBreak = text.indexOf('\n', lineStart);
const lineEnd = nextLineBreak === -1 ? text.length : nextLineBreak;
let slashIndex = lineStart;
while (slashIndex < lineEnd && isHorizontalWhitespace(text[slashIndex])) {
slashIndex += 1;
}
if (slashIndex >= lineEnd || text[slashIndex] !== '/') {
return null;
}
const resolvedLineEnd = resolveStandaloneSqlSlashLineEnd(text, slashIndex);
if (resolvedLineEnd === null || resolvedLineEnd !== lineEnd) {
return null;
}
return { lineStart, lineEnd, slashIndex };
};
const findPreviousSqlStatementRange = (
ranges: SqlStatementRange[],
offset: number,
): SqlStatementRange | null => (
[...ranges].reverse().find((range) => range.end <= offset) || null
);
const shouldEnterPlsqlBeginBlock = (text: string, tokenEnd: number): boolean => {
const nextChar = nextSqlSignificantChar(text, tokenEnd);
if (!nextChar || nextChar === ';') return false;
@@ -385,6 +422,11 @@ export const resolveCurrentSqlStatementRange = (sql: string, cursorOffset: numbe
return containingRange;
}
const slashLine = resolveStandaloneSqlSlashLineAtOffset(text, offset);
if (slashLine) {
return findPreviousSqlStatementRange(ranges, slashLine.lineStart);
}
const nextRange = ranges.find((range) => offset < range.start);
if (nextRange) {
return nextRange;
@@ -411,6 +453,14 @@ export const resolveExecutableSql = (
return { sql: statement.text, source: 'statement' };
}
const slashLine = resolveStandaloneSqlSlashLineAtOffset(text, offset);
if (slashLine) {
const previousStatement = findPreviousSqlStatementRange(ranges, slashLine.lineStart);
return previousStatement?.text.trim()
? { sql: previousStatement.text, source: 'statement' }
: null;
}
const lineStart = text.lastIndexOf('\n', Math.max(0, offset - 1)) + 1;
const nextLineBreak = text.indexOf('\n', offset);
const lineEnd = nextLineBreak === -1 ? text.length : nextLineBreak;