From 299dceb01c3ebc8b1e49e5f3359599a204f6b290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=9B=BD=E9=94=8B?= Date: Wed, 18 Mar 2026 21:02:54 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20fix(QueryEditor):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E6=9C=80=E5=A4=A7=E8=BF=94=E5=9B=9E=E8=A1=8C=E6=95=B0?= =?UTF-8?q?=E5=AF=B9=20SQL=20Server=20=E7=AD=89=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E4=B8=8D=E7=94=9F=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启用 applyAutoLimit 在 SQL 层面自动注入行数限制 - SQL Server 使用 TOP N,Oracle/Dameng 使用 FETCH FIRST N ROWS ONLY - 已有 LIMIT/TOP/FETCH/ROWNUM 时自动跳过,不重复注入 - 移除相关 DEBT 标记 - refs #236 --- frontend/src/components/QueryEditor.tsx | 57 ++++++++++++++++++++----- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/QueryEditor.tsx b/frontend/src/components/QueryEditor.tsx index 2b290b6..f54534b 100644 --- a/frontend/src/components/QueryEditor.tsx +++ b/frontend/src/components/QueryEditor.tsx @@ -940,9 +940,6 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { return statements; }; - // DEBT: 改用 DBQueryMulti 后前端不再逐条处理语句,此函数暂时未使用。 - // 当恢复前端自动行数限制功能时需要启用。 - // eslint-disable-next-line @typescript-eslint/no-unused-vars const getLeadingKeyword = (sql: string): string => { const text = (sql || '').replace(/\r\n/g, '\n'); const isWS = (ch: string) => ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r'; @@ -1235,24 +1232,53 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { return -1; }; - // DEBT: 改用 DBQueryMulti 后前端不再逐条处理语句,此函数暂时未使用。 - // 当恢复前端自动行数限制功能时需要启用。 - // eslint-disable-next-line @typescript-eslint/no-unused-vars const applyAutoLimit = (sql: string, dbType: string, maxRows: number): { sql: string; applied: boolean; maxRows: number } => { - const normalizedType = (dbType || 'mysql').toLowerCase(); - const supportsLimit = normalizedType === 'mysql' || normalizedType === 'mariadb' || normalizedType === 'diros' || normalizedType === 'sphinx' || normalizedType === 'postgres' || normalizedType === 'kingbase' || normalizedType === 'sqlite' || normalizedType === 'duckdb' || normalizedType === 'tdengine' || normalizedType === 'clickhouse' || normalizedType === ''; - if (!supportsLimit) return { sql, applied: false, maxRows }; if (!Number.isFinite(maxRows) || maxRows <= 0) return { sql, applied: false, maxRows }; + const normalizedType = (dbType || 'mysql').toLowerCase(); + + // 只对 SELECT 语句自动加限制 + const keyword = getLeadingKeyword(sql); + if (keyword !== 'SELECT') return { sql, applied: false, maxRows }; const { main, tail } = splitSqlTail(sql); if (!main.trim()) return { sql, applied: false, maxRows }; const fromPos = findTopLevelKeyword(main, 'from'); const limitPos = findTopLevelKeyword(main, 'limit'); + // 已有 LIMIT → 不注入 if (limitPos >= 0 && (fromPos < 0 || limitPos > fromPos)) return { sql, applied: false, maxRows }; const fetchPos = findTopLevelKeyword(main, 'fetch'); + // 已有 FETCH → 不注入 if (fetchPos >= 0 && (fromPos < 0 || fetchPos > fromPos)) return { sql, applied: false, maxRows }; + // SQL Server / mssql: 检查是否已有 TOP,未有则注入 SELECT TOP N + if (normalizedType === 'sqlserver' || normalizedType === 'mssql') { + const topPos = findTopLevelKeyword(main, 'top'); + if (topPos >= 0) return { sql, applied: false, maxRows }; // 已有 TOP + // 在 SELECT 关键字之后插入 TOP N + const selectPos = findTopLevelKeyword(main, 'select'); + if (selectPos < 0) return { sql, applied: false, maxRows }; + const afterSelect = selectPos + 'SELECT'.length; + // 处理 SELECT DISTINCT 的情况 + const restAfterSelect = main.slice(afterSelect); + const distinctMatch = restAfterSelect.match(/^(\s+DISTINCT\b)/i); + const insertOffset = distinctMatch ? afterSelect + distinctMatch[1].length : afterSelect; + const nextMain = main.slice(0, insertOffset) + ` TOP ${maxRows}` + main.slice(insertOffset); + return { sql: nextMain + tail, applied: true, maxRows }; + } + + // Oracle / Dameng: 使用 FETCH FIRST N ROWS ONLY(Oracle 12c+ 标准语法) + if (normalizedType === 'oracle' || normalizedType === 'dameng') { + // 检查是否已有 ROWNUM 限制 + const rownumPos = findTopLevelKeyword(main, 'rownum'); + if (rownumPos >= 0) return { sql, applied: false, maxRows }; + const offsetPos = findTopLevelKeyword(main, 'offset'); + if (offsetPos >= 0 && (fromPos < 0 || offsetPos > fromPos)) return { sql, applied: false, maxRows }; + const nextMain = main.trimEnd() + ` FETCH FIRST ${maxRows} ROWS ONLY`; + return { sql: nextMain + tail, applied: true, maxRows }; + } + + // 通用 LIMIT 语法(MySQL, PostgreSQL, SQLite, ClickHouse, DuckDB 等) const offsetPos = findTopLevelKeyword(main, 'offset'); const forPos = findTopLevelKeyword(main, 'for'); const lockPos = findTopLevelKeyword(main, 'lock'); @@ -1447,7 +1473,7 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { } } else { // 非 MongoDB:使用 DBQueryMulti 一次性执行多条 SQL,后端返回多结果集 - const fullSQL = normalizedRawSQL; + let fullSQL = normalizedRawSQL; if (!fullSQL.trim()) { message.info('没有可执行的 SQL。'); setResultSets([]); @@ -1455,6 +1481,17 @@ const QueryEditor: React.FC<{ tab: TabData }> = ({ tab }) => { return; } + // 自动给 SELECT 语句注入行数限制(防止大结果集卡死) + const maxRowsForLimit = Number(queryOptions?.maxRows) || 0; + if (Number.isFinite(maxRowsForLimit) && maxRowsForLimit > 0) { + const stmts = splitSQLStatements(fullSQL); + const limitedStmts = stmts.map(s => { + const result = applyAutoLimit(s, normalizedDbType, maxRowsForLimit); + return result.sql; + }); + fullSQL = limitedStmts.join(';\n'); + } + const startTime = Date.now(); let queryId: string; try {