Files
MyGoNavi/frontend/src/utils/aiSqlLimit.ts
Syngnat 51675f9d05 🐛 fix(ai): 修复多方言执行与 DDL 降级
- SQL 执行:移除 AI 工具和代码块预览中硬编码的 LIMIT 50
- 方言适配:按连接类型和自定义驱动别名生成只读 SQL 预览限流语句
- Oracle 兼容:Oracle、自定义 Oracle 和达梦改用 ROWNUM 语法限制行数
- 权限降级:获取表 DDL 失败时自动降级为字段元数据摘要
- 上下文优化:手动添加表结构上下文时复用同一套 DDL 降级逻辑
- 测试覆盖:新增 AI SQL 限流和表结构降级单元测试
Refs #418
2026-04-28 14:03:48 +08:00

32 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 = ''): string => {
const baseSQL = trimSQLStatement(sql);
const safeLimit = Math.max(0, Math.floor(Number(limit) || 0));
const dialect = resolveSqlDialect(dbType, driver);
if (!baseSQL || safeLimit <= 0 || !isAIReadonlySQL(baseSQL) || hasExistingRowLimit(dialect, baseSQL)) {
return baseSQL;
}
return buildPaginatedSelectSQL(dialect, baseSQL, '', safeLimit, 0);
};