mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 09:21:38 +08:00
🐛 fix(oracle): 修复存储过程斜杠分隔执行截断
- 支持 SQL*Plus 斜杠分隔符后的可选分号,避免 Oracle 过程执行出空语句 - 光标落在过程异常尾部或斜杠分隔行时,仍选择完整 PL/SQL 定义执行 - 补充前端语句选择、QueryEditor 执行和后端 split/DBQueryMulti 回归测试
This commit is contained in:
@@ -742,6 +742,134 @@ describe('QueryEditor external SQL save', () => {
|
||||
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';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['affectedRows'], rows: [{ affectedRows: 1 }] }],
|
||||
});
|
||||
const plsql = [
|
||||
'-- 修改函数/存储过程: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',
|
||||
' 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.goodsid IS NOT NULL THEN',
|
||||
' BEGIN',
|
||||
' SELECT COUNT(*) INTO v_ecnt FROM dual;',
|
||||
' EXCEPTION',
|
||||
' WHEN no_data_found THEN',
|
||||
' 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;',
|
||||
'/;',
|
||||
].join('\n');
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ dbName: 'ORCLPDB1', query: plsql, queryMode: 'object-edit' })} />);
|
||||
});
|
||||
|
||||
const tailLine = plsql.split('\n').findIndex((line) => line.includes('p_msg_out := substr')) + 1;
|
||||
editorState.position = { lineNumber: tailLine, column: 5 };
|
||||
editorState.selection = {
|
||||
startLineNumber: tailLine,
|
||||
startColumn: 5,
|
||||
endLineNumber: tailLine,
|
||||
endColumn: 5,
|
||||
positionLineNumber: tailLine,
|
||||
positionColumn: 5,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const executedSql = String(backendApp.DBQueryMulti.mock.calls[0][2]);
|
||||
expect(executedSql).toContain('CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1');
|
||||
expect(executedSql).toContain('p_msg_out OUT NVARCHAR2');
|
||||
expect(executedSql).toContain('p_msg_out := substr');
|
||||
expect(executedSql).not.toBe(plsql.split('\n').slice(tailLine - 1).join('\n'));
|
||||
expect(executedSql).not.toContain('/;');
|
||||
renderer?.unmount();
|
||||
});
|
||||
|
||||
it('runs the preceding Oracle procedure when the cursor is on the SQLPlus slash delimiter', 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 plsql = [
|
||||
'CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1(',
|
||||
' p_sourceid IN VARCHAR2,',
|
||||
' 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');
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ dbName: 'ORCLPDB1', query: plsql, queryMode: 'object-edit' })} />);
|
||||
});
|
||||
|
||||
const slashLine = plsql.split('\n').findIndex((line) => line.startsWith('/')) + 1;
|
||||
editorState.position = { lineNumber: slashLine, column: 1 };
|
||||
editorState.selection = {
|
||||
startLineNumber: slashLine,
|
||||
startColumn: 1,
|
||||
endLineNumber: slashLine,
|
||||
endColumn: 1,
|
||||
positionLineNumber: slashLine,
|
||||
positionColumn: 1,
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
await findButton(renderer!, '运行').props.onClick();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const executedSql = String(backendApp.DBQueryMulti.mock.calls[0][2]);
|
||||
expect(executedSql).toContain('CREATE OR REPLACE PROCEDURE cproc_tzhssr_order2sale_A1');
|
||||
expect(executedSql).toContain('p_msg_out OUT NVARCHAR2');
|
||||
expect(executedSql).toContain('END cproc_tzhssr_order2sale_A1;');
|
||||
expect(executedSql).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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -533,6 +533,56 @@ END;`
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBQueryMultiSkipsOracleSqlPlusSlashDelimiterWithSemicolon(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
t.Cleanup(func() {
|
||||
newDatabaseFunc = originalNewDatabaseFunc
|
||||
})
|
||||
|
||||
fakeDB := &fakeBatchWriteDB{}
|
||||
newDatabaseFunc = func(dbType string) (db.Database, error) {
|
||||
return fakeDB, nil
|
||||
}
|
||||
|
||||
app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
|
||||
config := connection.ConnectionConfig{
|
||||
Type: "oracle",
|
||||
Host: "127.0.0.1",
|
||||
Port: 1521,
|
||||
User: "app",
|
||||
}
|
||||
query := `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;
|
||||
/;`
|
||||
wantExecuted := `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;`
|
||||
|
||||
result := app.DBQueryMulti(config, "ORCLPDB1", query, "oracle-sqlplus-slash-semicolon-test")
|
||||
if !result.Success {
|
||||
t.Fatalf("expected DBQueryMulti success, got failure: %s", result.Message)
|
||||
}
|
||||
if fakeDB.execCalls != 1 || len(fakeDB.execQueries) != 1 {
|
||||
t.Fatalf("expected one sequential exec call, got execCalls=%d queries=%#v", fakeDB.execCalls, fakeDB.execQueries)
|
||||
}
|
||||
if fakeDB.execQueries[0] != wantExecuted {
|
||||
t.Fatalf("expected slash delimiter with semicolon to be skipped, got %q", fakeDB.execQueries[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBQueryMultiKeepsOraclePackageSpecAndBodyTogether(t *testing.T) {
|
||||
originalNewDatabaseFunc := newDatabaseFunc
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -281,10 +281,15 @@ func scanSQLStandaloneSlashLineSuffix(text string, pos int) (lineEnd int, standa
|
||||
if pos < 0 || pos >= len(text) || text[pos] != '/' {
|
||||
return 0, false, true
|
||||
}
|
||||
seenOptionalSemicolon := false
|
||||
for i := pos + 1; i < len(text); i++ {
|
||||
if text[i] == '\n' {
|
||||
return i, true, true
|
||||
}
|
||||
if text[i] == ';' && !seenOptionalSemicolon {
|
||||
seenOptionalSemicolon = true
|
||||
continue
|
||||
}
|
||||
if text[i] == '-' {
|
||||
if i+1 >= len(text) {
|
||||
return len(text), true, false
|
||||
|
||||
@@ -343,6 +343,48 @@ END cproc_tzhssr_order2sale_A1;`,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLStatements_OracleCreateProcedureSkipsSemicolonAfterSqlPlusSlashDelimiter(t *testing.T) {
|
||||
input := `-- 修改函数/存储过程: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;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_ecnt FROM dual;
|
||||
p_msg_out := '';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
p_msg_out := substr('订单核销失败,错误信息:' || SQLERRM || ',错误位置:' ||
|
||||
dbms_utility.format_error_backtrace, 1, 1000);
|
||||
END cproc_tzhssr_order2sale_A1;
|
||||
/;
|
||||
SELECT 1 FROM dual;`
|
||||
got := splitSQLStatements(input)
|
||||
want := []string{
|
||||
`-- 修改函数/存储过程: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;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO v_ecnt FROM dual;
|
||||
p_msg_out := '';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
p_msg_out := substr('订单核销失败,错误信息:' || SQLERRM || ',错误位置:' ||
|
||||
dbms_utility.format_error_backtrace, 1, 1000);
|
||||
END cproc_tzhssr_order2sale_A1;`,
|
||||
"SELECT 1 FROM dual",
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("splitSQLStatements(%q) = %#v, want %#v", input, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitSQLStatements_OraclePackageSpecAndBodyStayWhole(t *testing.T) {
|
||||
input := `CREATE OR REPLACE PACKAGE pkg_order AS
|
||||
PROCEDURE sync_order(p_id IN NUMBER);
|
||||
|
||||
Reference in New Issue
Block a user