Files
MyGoNavi/frontend/src/utils/aiSqlLimit.ts
Syngnat 3e140c1bc6 🐛 fix(ai-safety): 修正完全模式执行口径与本地工具失败判定
- 修正完全模式下 DML 与过程调用的安全提示和限制说明
- 区分连接探针失败与可恢复 SQL 执行错误,避免数据探针被误终止
- 修复本地 execute_sql 写语句结果返回 affectedRows
- 补充 AI 安全、本地工具执行与 SQL 限制回归测试
2026-06-17 09:49:59 +08:00

38 lines
1.4 KiB
TypeScript

import { buildPaginatedSelectSQL } from './sql';
import { resolveSqlDialect } from './sqlDialect';
const AI_READONLY_SQL_KEYWORDS = new Set(['select', 'show', 'describe', 'desc', 'explain', 'with', 'pragma', 'values']);
const trimSQLStatement = (sql: string): string => String(sql || '').trim().replace(/;\s*$/, '').trim();
const isAIReadonlySQL = (sql: string): boolean => {
const firstWord = trimSQLStatement(sql).trimStart().split(/\s+/)[0]?.toLowerCase() || '';
return AI_READONLY_SQL_KEYWORDS.has(firstWord);
};
const hasExistingRowLimit = (dialect: string, sql: string): boolean => {
const text = trimSQLStatement(sql).toLowerCase();
if (!text) return false;
if (/\blimit\s+\d+\b/.test(text)) return true;
if (/\bfetch\s+(first|next)\s+\d+\s+rows?\b/.test(text)) return true;
if (/\btop\s*\(?\s*\d+\s*\)?\b/.test(text)) return true;
return (dialect === 'oracle' || dialect === 'dameng') && /\brownum\b/.test(text);
};
export const buildAIReadonlyPreviewSQL = (
dbType: string,
sql: string,
limit = 50,
driver = '',
options?: { oceanBaseProtocol?: unknown },
): string => {
const baseSQL = trimSQLStatement(sql);
const safeLimit = Math.max(0, Math.floor(Number(limit) || 0));
const dialect = resolveSqlDialect(dbType, driver, options);
if (!baseSQL || safeLimit <= 0 || !isAIReadonlySQL(baseSQL) || hasExistingRowLimit(dialect, baseSQL)) {
return baseSQL;
}
return buildPaginatedSelectSQL(dialect, baseSQL, '', safeLimit, 0);
};