mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-10 16:53:35 +08:00
🐛 fix(query-editor): 修复 Oracle 序列查询自动限行报错
- 识别可执行 SQL 中的 NEXTVAL 与 CURRVAL 序列伪列 - Oracle、达梦及 OceanBase Oracle 序列查询跳过 ROWNUM 子查询包装 - 排除字符串和注释中的同名文本误判 - 补充自动限行与真实执行链路回归测试
This commit is contained in:
@@ -8423,6 +8423,37 @@ describe('QueryEditor external SQL save', () => {
|
||||
renderer?.unmount();
|
||||
});
|
||||
|
||||
it('keeps OceanBase Oracle sequence queries out of the ROWNUM auto-limit wrapper', async () => {
|
||||
const sql = 'SELECT IMP_BASICINFO.SEQ_HIS_AZA7.nextval FROM dual';
|
||||
storeState.connections[0].config.type = 'oceanbase';
|
||||
(storeState.connections[0].config as any).oceanBaseProtocol = 'oracle';
|
||||
storeState.connections[0].config.database = 'ORCLPDB1';
|
||||
backendApp.DBQueryMulti.mockResolvedValueOnce({
|
||||
success: true,
|
||||
data: [{ columns: ['NEXTVAL'], rows: [{ NEXTVAL: 42 }] }],
|
||||
});
|
||||
|
||||
let renderer!: ReactTestRenderer;
|
||||
await act(async () => {
|
||||
renderer = create(<QueryEditor tab={createTab({ dbName: 'IMP_BASICINFO', query: sql })} />);
|
||||
});
|
||||
|
||||
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('IMP_BASICINFO.SEQ_HIS_AZA7.nextval');
|
||||
expect(executedSql).not.toContain('SELECT * FROM (');
|
||||
expect(executedSql).not.toMatch(/\bROWNUM\b/i);
|
||||
expect(backendApp.DBQueryMultiTransactional).not.toHaveBeenCalled();
|
||||
renderer?.unmount();
|
||||
});
|
||||
|
||||
it('quotes exact-case OceanBase Oracle lowercase tables for execution while keeping sql logs unchanged', async () => {
|
||||
storeState.connections[0].config.type = 'oceanbase';
|
||||
(storeState.connections[0].config as any).oceanBaseProtocol = 'oracle';
|
||||
|
||||
@@ -113,6 +113,25 @@ describe('applyQueryAutoLimit', () => {
|
||||
.toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['oracle', 'SELECT IMP_BASICINFO.SEQ_HIS_AZA7.nextval FROM dual'],
|
||||
['dameng', 'SELECT "APP"."ORDER_SEQ".CURRVAL FROM dual'],
|
||||
])('does not wrap %s sequence pseudo-column queries', (dbType, sql) => {
|
||||
expect(applyQueryAutoLimit(sql, dbType, 500)).toEqual({
|
||||
sql,
|
||||
applied: false,
|
||||
maxRows: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not mistake sequence pseudo-column text in Oracle strings or comments for executable SQL', () => {
|
||||
const sql = "SELECT 'SEQ.NEXTVAL' AS sample FROM dual /* OTHER_SEQ.CURRVAL */";
|
||||
const result = applyQueryAutoLimit(sql, 'oracle', 500);
|
||||
|
||||
expect(result.applied).toBe(true);
|
||||
expect(result.sql).toContain('WHERE ROWNUM <= 500');
|
||||
});
|
||||
|
||||
it('does not add another SQL Server limit when SQL already uses TOP', () => {
|
||||
expect(applyQueryAutoLimit('SELECT TOP 10 * FROM users', 'sqlserver', 500).applied)
|
||||
.toBe(false);
|
||||
|
||||
@@ -283,6 +283,75 @@ export const findTopLevelKeyword = (sql: string, keyword: string): number => {
|
||||
return -1;
|
||||
};
|
||||
|
||||
const hasOracleSequencePseudoColumn = (sql: string): boolean => {
|
||||
const text = sql || '';
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let inBacktick = false;
|
||||
let inLineComment = false;
|
||||
let inBlockComment = false;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
const next = i + 1 < text.length ? text[i + 1] : '';
|
||||
|
||||
if (inLineComment) {
|
||||
if (ch === '\n') inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (ch === '*' && next === '/') {
|
||||
i++;
|
||||
inBlockComment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && !inBacktick) {
|
||||
if (ch === '-' && next === '-') {
|
||||
i++;
|
||||
inLineComment = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '/' && next === '*') {
|
||||
i++;
|
||||
inBlockComment = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inDouble && !inBacktick && ch === "'") {
|
||||
if (inSingle && next === "'") {
|
||||
i++;
|
||||
} else {
|
||||
inSingle = !inSingle;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inBacktick && ch === '"') {
|
||||
if (inDouble && next === '"') {
|
||||
i++;
|
||||
} else {
|
||||
inDouble = !inDouble;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && ch === '`') {
|
||||
inBacktick = !inBacktick;
|
||||
continue;
|
||||
}
|
||||
if (inSingle || inDouble || inBacktick || ch !== '.') continue;
|
||||
|
||||
let tokenStart = i + 1;
|
||||
while (tokenStart < text.length && isWS(text[tokenStart])) tokenStart++;
|
||||
let tokenEnd = tokenStart;
|
||||
while (tokenEnd < text.length && isWord(text[tokenEnd])) tokenEnd++;
|
||||
const token = text.slice(tokenStart, tokenEnd).toLowerCase();
|
||||
if (token === 'nextval' || token === 'currval') return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const applyQueryAutoLimit = (
|
||||
sql: string,
|
||||
dbType: string,
|
||||
@@ -323,6 +392,9 @@ export const applyQueryAutoLimit = (
|
||||
if (offsetPos >= 0 && (fromPos < 0 || offsetPos > fromPos)) return { sql, applied: false, maxRows };
|
||||
const forPos = findTopLevelKeyword(main, 'for');
|
||||
if (forPos >= 0 && (fromPos < 0 || forPos > fromPos)) return { sql, applied: false, maxRows };
|
||||
// Oracle-compatible databases reject NEXTVAL/CURRVAL when the ROWNUM cap
|
||||
// moves the original SELECT into a subquery.
|
||||
if (hasOracleSequencePseudoColumn(main)) return { sql, applied: false, maxRows };
|
||||
return { sql: `${buildPaginatedSelectSQL(normalizedType, main, '', maxRows, 0)}${tail}`, applied: true, maxRows };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user