diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index c185b8fa..a5f04ab7 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -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(); + }); + + 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'; diff --git a/frontend/src/utils/queryAutoLimit.test.ts b/frontend/src/utils/queryAutoLimit.test.ts index 89731f5b..fa08d31d 100644 --- a/frontend/src/utils/queryAutoLimit.test.ts +++ b/frontend/src/utils/queryAutoLimit.test.ts @@ -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); diff --git a/frontend/src/utils/queryAutoLimit.ts b/frontend/src/utils/queryAutoLimit.ts index ff5bf468..20e141f9 100644 --- a/frontend/src/utils/queryAutoLimit.ts +++ b/frontend/src/utils/queryAutoLimit.ts @@ -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 }; }