sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1

- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
logan committed 2026-05-23 14:41:06 +08:00
1 parent 6fcd9465cf
commit 4c9427e546
105 files changed
+23680 -471

No files matched your search

+342
View File
@@ -0,0 +1,342 @@
"""SQL助手 — 人性化辅助功能。
提供便捷的数据操作辅助:
- SQL格式化: 统一SQL代码风格
- SQL语法高亮标记: 标记关键字、表名、字段名
- 表结构速查: 快速获取表列信息
- 智能补全建议: 基于表结构生成字段列表
- SQL Explain解析: 将执行计划转为人类可读格式
- 查询风险提示: 基于规则标记潜在风险
人性化设计原则:
- 减少重复操作
- 降低SQL编写门槛
- 提供即时反馈
- 统一操作体验
"""
import re
from typing import List, Dict, Optional, Any
from dataclasses import dataclass
# SQL关键字(用于语法高亮)
SQL_KEYWORDS = {
"SELECT", "FROM", "WHERE", "INSERT", "UPDATE", "DELETE", "CREATE",
"DROP", "ALTER", "TABLE", "INDEX", "JOIN", "LEFT", "RIGHT", "INNER",
"OUTER", "ON", "AND", "OR", "NOT", "NULL", "IS", "IN", "EXISTS",
"BETWEEN", "LIKE", "GROUP", "BY", "ORDER", "HAVING", "LIMIT", "OFFSET",
"UNION", "ALL", "DISTINCT", "AS", "ASC", "DESC", "COUNT", "SUM",
"AVG", "MAX", "MIN", "IF", "CASE", "WHEN", "THEN", "ELSE", "END",
"SHOW", "DESCRIBE", "EXPLAIN", "USE", "SET", "COMMIT", "ROLLBACK",
}
# 高风险操作标记
RISK_MARKERS = {
"DELETE": {"level": "high", "message": "删除操作不可逆,请确认WHERE条件正确"},
"DROP": {"level": "critical", "message": "DROP操作会永久删除对象,请谨慎操作"},
"TRUNCATE": {"level": "critical", "message": "TRUNCATE会清空全表且不可回滚"},
"UPDATE": {"level": "medium", "message": "UPDATE可能影响多行数据,建议先SELECT验证"},
"ALTER": {"level": "medium", "message": "ALTER TABLE可能锁表,建议在低峰期执行"},
}
@dataclass
class SQLFormatResult:
"""SQL格式化结果。"""
formatted: str
original: str
is_valid: bool
error: Optional[str] = None
@dataclass
class SQLHighlightResult:
"""SQL语法高亮结果。"""
highlighted: str # HTML格式的语法高亮
keywords: List[str] # 提取的关键字
tables: List[str] # 提取的表名
columns: List[str] # 提取的列名
risks: List[Dict[str, str]] # 风险提示
@dataclass
class ExplainResult:
"""Explain解析结果。"""
is_simple: bool
has_index: bool
has_full_scan: bool
estimated_rows: int
warnings: List[str]
human_readable: str # 人类可读的执行计划描述
class SQLAssistant:
"""SQL助手 — 人性化辅助功能。"""
# ── SQL格式化 ──
@staticmethod
def format_sql(sql: str) -> SQLFormatResult:
"""格式化SQL语句。
- 关键字大写
- 合理换行和缩进
- 统一空格
"""
if not sql or not sql.strip():
return SQLFormatResult(formatted="", original=sql, is_valid=False, error="SQL为空")
try:
import sqlparse
formatted = sqlparse.format(
sql,
keyword_case="upper",
identifier_case="lower",
strip_comments=False,
reindent=True,
indent_width=4,
wrap_after=80,
)
return SQLFormatResult(formatted=formatted, original=sql, is_valid=True)
except ImportError:
# 如果没有sqlparse,使用简单格式化
return SQLAssistant._simple_format(sql)
except Exception as e:
return SQLFormatResult(formatted=sql, original=sql, is_valid=False, error=str(e))
@staticmethod
def _simple_format(sql: str) -> SQLFormatResult:
"""简单SQL格式化(不依赖sqlparse)。"""
# 去除多余空格
formatted = " ".join(sql.split())
# 关键字大写
for kw in SQL_KEYWORDS:
pattern = re.compile(r"\b" + kw + r"\b", re.IGNORECASE)
formatted = pattern.sub(kw, formatted)
# 在主要关键字前换行
line_breaks = [" SELECT ", " FROM ", " WHERE ", " GROUP BY ",
" ORDER BY ", " HAVING ", " LIMIT ", " JOIN ",
" LEFT JOIN ", " RIGHT JOIN ", " INNER JOIN ", " UNION "]
for br in line_breaks:
formatted = formatted.replace(br, "\n" + br.strip().ljust(8))
return SQLFormatResult(formatted=formatted, original=sql, is_valid=True)
# ── 语法高亮与风险分析 ──
@staticmethod
def highlight_sql(sql: str) -> SQLHighlightResult:
"""SQL语法高亮和风险分析。
返回HTML格式的语法高亮文本和风险提示。
"""
upper = sql.upper().strip()
# 提取关键字
words = re.findall(r"\b\w+\b", upper)
keywords = [w for w in words if w in SQL_KEYWORDS]
# 提取表名(简单规则)
tables = []
table_patterns = [
r"FROM\s+(\w+)",
r"JOIN\s+(\w+)",
r"INTO\s+(\w+)",
r"UPDATE\s+(\w+)",
r"TABLE\s+(\w+)",
]
for pattern in table_patterns:
matches = re.findall(pattern, upper, re.IGNORECASE)
tables.extend(matches)
tables = list(set(t.lower() for t in tables))
# 风险提示
risks = []
for op, info in RISK_MARKERS.items():
if re.search(r"\b" + op + r"\b", upper):
# 检查是否有WHERE
if op in ("UPDATE", "DELETE") and "WHERE" not in upper:
risks.append({
"level": "critical",
"operation": op,
"message": f"{op} 缺少 WHERE 条件,将操作全表数据!",
})
else:
risks.append({"level": info["level"], "operation": op, "message": info["message"]})
# 构建HTML高亮(简化版)
highlighted = sql
for kw in SQL_KEYWORDS:
highlighted = re.sub(
r"\b" + kw + r"\b",
f'<span class="sql-keyword">{kw}</span>',
highlighted,
flags=re.IGNORECASE,
)
return SQLHighlightResult(
highlighted=highlighted,
keywords=list(set(keywords)),
tables=tables,
columns=[], # 简单实现不提取列
risks=risks,
)
# ── Explain解析 ──
@staticmethod
def parse_explain(explain_rows: List[Dict[str, Any]]) -> ExplainResult:
"""解析MySQL EXPLAIN结果为人类可读格式。
Args:
explain_rows: EXPLAIN输出结果行列表
"""
if not explain_rows:
return ExplainResult(
is_simple=True, has_index=False, has_full_scan=False,
estimated_rows=0, warnings=["EXPLAIN结果为空"],
human_readable="无法解析执行计划",
)
warnings = []
total_rows = 0
has_full_scan = False
has_index = False
parts = []
for i, row in enumerate(explain_rows):
table = row.get("table") or row.get("TABLE", "unknown")
select_type = row.get("select_type") or row.get("SELECT_TYPE", "SIMPLE")
scan_type = row.get("type") or row.get("TYPE", "ALL")
possible_keys = row.get("possible_keys") or row.get("POSSIBLE_KEYS")
key_used = row.get("key") or row.get("KEY")
rows_est = row.get("rows") or row.get("ROWS", 0) or row.get("EST_ROWS", 0)
extra = row.get("Extra") or row.get("EXTRA", "")
total_rows += int(rows_est) if rows_est else 0
if scan_type in ("ALL", "index") and select_type == "SIMPLE":
has_full_scan = True
warnings.append(f"表 '{table}' 使用了全表扫描({scan_type}),建议添加索引")
if key_used:
has_index = True
parts.append(f"表 '{table}': 使用了索引 {key_used},预估扫描 {rows_est} 行")
else:
parts.append(f"表 '{table}': 未使用索引(类型: {scan_type}),预估扫描 {rows_est} 行")
if "Using filesort" in str(extra):
warnings.append(f"表 '{table}' 使用了文件排序,建议优化ORDER BY")
if "Using temporary" in str(extra):
warnings.append(f"表 '{table}' 使用了临时表,建议优化GROUP BY")
human_readable = "\n".join(parts)
if total_rows > 100000:
warnings.append(f"预估扫描总行数 {total_rows} 较大,查询可能较慢")
return ExplainResult(
is_simple=len(explain_rows) == 1,
has_index=has_index,
has_full_scan=has_full_scan,
estimated_rows=total_rows,
warnings=warnings,
human_readable=human_readable,
)
# ── 查询建议 ──
@staticmethod
def get_query_suggestions(sql: str, db_type: str = "mysql") -> List[Dict[str, str]]:
"""基于SQL内容提供人性化建议。
帮助用户写出更好的SQL。
"""
suggestions = []
upper = sql.upper().strip()
# SELECT * 建议
if re.search(r"SELECT\s+\*", upper):
suggestions.append({
"type": "optimization",
"message": "使用 SELECT * 会返回所有列,建议只选择需要的字段以减少网络传输",
})
# 缺少LIMIT
if "SELECT" in upper and "LIMIT" not in upper and db_type in ("mysql", "postgres"):
suggestions.append({
"type": "safety",
"message": "建议添加 LIMIT 子句限制返回行数,避免大数据量查询",
})
# ORDER BY RAND()
if "ORDER BY RAND()" in upper or "ORDER BY RANDOM()" in upper:
suggestions.append({
"type": "optimization",
"message": "ORDER BY RAND() 在大表上性能极差,建议使用其他随机化方案",
})
# LIKE %xxx%
if re.search(r"LIKE\s+'%.", upper):
suggestions.append({
"type": "optimization",
"message": "LIKE '%xxx%' 无法使用索引,如数据量大请考虑全文索引或其他方案",
})
# 多表JOIN
join_count = len(re.findall(r"\bJOIN\b", upper))
if join_count > 3:
suggestions.append({
"type": "optimization",
"message": f"当前有 {join_count} 个 JOIN,建议考虑子查询或视图简化",
})
# 没有WHERE的UPDATE/DELETE(前面已标记为critical)
# 这里给出更详细的建议
if re.search(r"\bUPDATE\b", upper) and "WHERE" not in upper:
suggestions.append({
"type": "danger",
"message": "UPDATE 缺少 WHERE 将修改全表!建议先用 SELECT 验证影响范围",
})
if re.search(r"\bDELETE\b", upper) and "WHERE" not in upper:
suggestions.append({
"type": "danger",
"message": "DELETE 缺少 WHERE 将删除全表!建议先用 SELECT 验证影响范围",
})
# 缺少注释的复杂查询
if len(sql) > 500 and "--" not in sql and "/*" not in sql:
suggestions.append({
"type": "style",
"message": "复杂查询建议添加注释,方便后续维护和理解",
})
return suggestions
# ── 统一辅助入口 ──
@staticmethod
def analyze(sql: str, db_type: str = "mysql") -> Dict[str, Any]:
"""全面分析SQL,返回格式化+高亮+风险+建议的完整结果。
人性化入口,一次调用获取所有辅助信息。
"""
format_result = SQLAssistant.format_sql(sql)
highlight_result = SQLAssistant.highlight_sql(sql)
suggestions = SQLAssistant.get_query_suggestions(sql, db_type)
return {
"formatted": format_result.formatted if format_result.is_valid else sql,
"is_valid": format_result.is_valid,
"format_error": format_result.error,
"highlighted": highlight_result.highlighted,
"keywords": highlight_result.keywords,
"tables": highlight_result.tables,
"risks": highlight_result.risks,
"suggestions": suggestions,
"risk_count": len(highlight_result.risks),
"suggestion_count": len(suggestions),
}