🐛 fix(query-editor): 修复 Oracle 序列查询自动限行报错

- 识别可执行 SQL 中的 NEXTVAL 与 CURRVAL 序列伪列
- Oracle、达梦及 OceanBase Oracle 序列查询跳过 ROWNUM 子查询包装
- 排除字符串和注释中的同名文本误判
- 补充自动限行与真实执行链路回归测试
This commit is contained in:
Syngnat
2026-07-13 12:54:43 +08:00
parent 4bee3cefa6
commit b23e768973
3 changed files with 122 additions and 0 deletions

View File

@@ -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';

View File

@@ -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);

View File

@@ -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 };
}