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

+315
View File
@@ -0,0 +1,315 @@
"""SQL 执行影响分析 — 预估影响行数、执行时间、风险等级。
使用数据库 EXPLAIN 输出分析执行计划,结合表大小统计预估影响。
支持 MySQL 和 PostgreSQL。
风险等级定义:
- low: 影响 < 1000 行,执行 < 1秒
- medium: 影响 1000-10000 行,执行 1-10秒
- high: 影响 10000-100000 行,执行 10-60秒
- critical: 影响 > 100000 行 或 ALTER > 1GB 表
"""
import re
from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any
from enum import Enum
from app.models.instance import Instance
class RiskLevel(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class ImpactAnalysis:
"""影响分析结果。"""
sql_type: str = "UNKNOWN"
estimated_rows: int = 0
estimated_duration_ms: int = 0
table_size_mb: float = 0.0
table_row_count: int = 0
index_usage: List[str] = field(default_factory=list)
has_full_scan: bool = False
risk_level: str = RiskLevel.LOW
warnings: List[str] = field(default_factory=list)
explain_result: Optional[Dict[str, Any]] = None
def to_dict(self) -> dict:
return {
"sql_type": self.sql_type,
"estimated_rows": self.estimated_rows,
"estimated_duration_ms": self.estimated_duration_ms,
"table_size_mb": round(self.table_size_mb, 2),
"table_row_count": self.table_row_count,
"index_usage": self.index_usage,
"has_full_scan": self.has_full_scan,
"risk_level": self.risk_level,
"warnings": self.warnings,
}
class ImpactAnalyzer:
"""SQL 执行影响分析器。"""
RISK_THRESHOLDS = {
RiskLevel.LOW: {"rows": 1000, "duration_ms": 1000, "size_mb": 10},
RiskLevel.MEDIUM: {"rows": 10000, "duration_ms": 10000, "size_mb": 100},
RiskLevel.HIGH: {"rows": 100000, "duration_ms": 60000, "size_mb": 1024},
}
def detect_sql_type(self, sql: str) -> str:
"""检测 SQL 语句类型。"""
upper = sql.strip().upper()
if upper.startswith("SELECT"):
return "SELECT"
elif upper.startswith("INSERT"):
return "INSERT"
elif upper.startswith("UPDATE"):
return "UPDATE"
elif upper.startswith("DELETE"):
return "DELETE"
elif upper.startswith("ALTER"):
return "ALTER"
elif upper.startswith("CREATE"):
return "CREATE"
elif upper.startswith("DROP"):
return "DROP"
elif upper.startswith("TRUNCATE"):
return "TRUNCATE"
return "UNKNOWN"
async def analyze(self, instance: Instance, database: str, sql: str) -> ImpactAnalysis:
"""分析 SQL 执行影响。"""
sql_type = self.detect_sql_type(sql)
analysis = ImpactAnalysis(sql_type=sql_type)
if sql_type in ("UPDATE", "DELETE"):
return await self._analyze_dml(instance, database, sql, analysis)
elif sql_type == "ALTER":
return await self._analyze_alter(instance, database, sql, analysis)
elif sql_type == "INSERT":
return await self._analyze_insert(instance, database, sql, analysis)
elif sql_type in ("CREATE", "DROP", "TRUNCATE"):
return self._analyze_ddl(sql, analysis)
else:
return analysis
async def _analyze_dml(self, instance: Instance, database: str,
sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis:
"""分析 UPDATE/DELETE 影响。"""
table = self._extract_table(sql)
analysis.table_row_count = await self._get_table_row_count(instance, database, table)
analysis.table_size_mb = await self._get_table_size(instance, database, table)
# 通过 WHERE 条件粗略估计影响行数
where_clause = self._extract_where(sql)
if where_clause:
# 如果有 WHERE 且包含主键条件,影响较小
if self._has_pk_condition(where_clause):
analysis.estimated_rows = 1
# 如果有 WHERE 且包含索引列条件
elif self._has_index_condition(where_clause):
analysis.estimated_rows = max(1, analysis.table_row_count // 100)
else:
# 非索引条件,影响较大
analysis.estimated_rows = max(1, analysis.table_row_count // 10)
analysis.has_full_scan = True
else:
# 无 WHERE,全表影响
analysis.estimated_rows = analysis.table_row_count
analysis.has_full_scan = True
analysis.warnings.append(f"{analysis.sql_type} 缺少 WHERE 条件,将影响全表 {analysis.table_row_count} 行")
# 预估执行时间
analysis.estimated_duration_ms = self._estimate_duration(
analysis.estimated_rows, analysis.table_size_mb, analysis.sql_type
)
# 检查索引使用
if analysis.has_full_scan:
analysis.warnings.append("可能触发全表扫描,建议添加索引条件")
analysis.risk_level = self._calculate_risk_level(
analysis.estimated_rows, analysis.estimated_duration_ms, analysis.table_size_mb
)
if analysis.risk_level in (RiskLevel.HIGH, RiskLevel.CRITICAL):
analysis.warnings.append(
f"风险等级: {analysis.risk_level.value},建议分批次执行或在维护窗口操作"
)
return analysis
async def _analyze_alter(self, instance: Instance, database: str,
sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis:
"""分析 ALTER TABLE 影响。"""
table = self._extract_table(sql)
analysis.table_size_mb = await self._get_table_size(instance, database, table)
analysis.table_row_count = await self._get_table_row_count(instance, database, table)
# ALTER TABLE 预估时间: 大约每 10MB 需要 1 秒
analysis.estimated_duration_ms = int(analysis.table_size_mb * 100)
analysis.estimated_rows = analysis.table_row_count
if analysis.table_size_mb > 1024:
analysis.risk_level = RiskLevel.CRITICAL
analysis.warnings.append(
f"ALTER TABLE 操作表大小为 {analysis.table_size_mb:.1f}MB,"
"建议使用 pt-online-schema-change 或 gh-ost 进行在线变更"
)
elif analysis.table_size_mb > 100:
analysis.risk_level = RiskLevel.HIGH
analysis.warnings.append(
f"ALTER TABLE 操作表大小为 {analysis.table_size_mb:.1f}MB,预计耗时 {analysis.estimated_duration_ms / 1000:.1f} 秒"
)
elif analysis.table_size_mb > 10:
analysis.risk_level = RiskLevel.MEDIUM
else:
analysis.risk_level = RiskLevel.LOW
return analysis
async def _analyze_insert(self, instance: Instance, database: str,
sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis:
"""分析 INSERT 影响。"""
# 估算插入行数
values_count = sql.count(",") if "VALUES" in sql.upper() else 1
# 更精确的估算:数 VALUES 子句中的行
row_match = re.findall(r"\)\s*,?\s*\(", sql)
analysis.estimated_rows = len(row_match) + 1 if row_match else 1
table = self._extract_table(sql)
analysis.table_size_mb = await self._get_table_size(instance, database, table)
# INSERT 通常很快
analysis.estimated_duration_ms = analysis.estimated_rows * 2
if analysis.estimated_rows > 10000:
analysis.risk_level = RiskLevel.HIGH
analysis.warnings.append(f"单次插入 {analysis.estimated_rows} 行数据量较大,建议分批插入")
elif analysis.estimated_rows > 1000:
analysis.risk_level = RiskLevel.MEDIUM
else:
analysis.risk_level = RiskLevel.LOW
return analysis
def _analyze_ddl(self, sql: str, analysis: ImpactAnalysis) -> ImpactAnalysis:
"""分析 DDL 影响。"""
analysis.risk_level = RiskLevel.MEDIUM
analysis.warnings.append("DDL 操作会锁表,建议在低峰期执行")
if "DROP" in sql.upper():
analysis.risk_level = RiskLevel.CRITICAL
analysis.warnings.append("DROP 操作不可逆,请确认已备份数据")
elif "TRUNCATE" in sql.upper():
analysis.risk_level = RiskLevel.CRITICAL
analysis.warnings.append("TRUNCATE 会清空所有数据且不可回滚")
return analysis
def _estimate_duration(self, rows: int, table_size_mb: float, sql_type: str) -> int:
"""预估执行时间(毫秒)。"""
base_time = 50 # 基础时间 50ms
if sql_type == "UPDATE":
# UPDATE: 每 1000 行约 100ms,加上表大小因素
row_time = (rows / 1000) * 100
size_factor = table_size_mb * 0.5
return int(base_time + row_time + size_factor)
elif sql_type == "DELETE":
# DELETE: 每 1000 行约 80ms
row_time = (rows / 1000) * 80
return int(base_time + row_time)
elif sql_type == "INSERT":
# INSERT: 每行约 2ms
return int(base_time + rows * 2)
elif sql_type == "ALTER":
# ALTER: 每 10MB 约 1000ms
return int(base_time + table_size_mb * 100)
return base_time
def _calculate_risk_level(self, rows: int, duration_ms: int,
table_size_mb: float) -> str:
"""计算风险等级。"""
if (rows > self.RISK_THRESHOLDS[RiskLevel.HIGH]["rows"] or
duration_ms > self.RISK_THRESHOLDS[RiskLevel.HIGH]["duration_ms"] or
table_size_mb > self.RISK_THRESHOLDS[RiskLevel.HIGH]["size_mb"]):
return RiskLevel.CRITICAL
if (rows > self.RISK_THRESHOLDS[RiskLevel.MEDIUM]["rows"] or
duration_ms > self.RISK_THRESHOLDS[RiskLevel.MEDIUM]["duration_ms"] or
table_size_mb > self.RISK_THRESHOLDS[RiskLevel.MEDIUM]["size_mb"]):
return RiskLevel.HIGH
if (rows > self.RISK_THRESHOLDS[RiskLevel.LOW]["rows"] or
duration_ms > self.RISK_THRESHOLDS[RiskLevel.LOW]["duration_ms"] or
table_size_mb > self.RISK_THRESHOLDS[RiskLevel.LOW]["size_mb"]):
return RiskLevel.MEDIUM
return RiskLevel.LOW
def _extract_table(self, sql: str) -> str:
"""从 SQL 中提取表名(简化版)。"""
upper = sql.upper()
# UPDATE table_name SET ...
update_match = re.search(r"UPDATE\s+(?:`?)(\w+)(?:`?)", upper)
if update_match:
return update_match.group(1).lower()
# DELETE FROM table_name ...
delete_match = re.search(r"DELETE\s+FROM\s+(?:`?)(\w+)(?:`?)", upper)
if delete_match:
return delete_match.group(1).lower()
# INSERT INTO table_name ...
insert_match = re.search(r"INSERT\s+INTO\s+(?:`?)(\w+)(?:`?)", upper)
if insert_match:
return insert_match.group(1).lower()
# ALTER TABLE table_name ...
alter_match = re.search(r"ALTER\s+TABLE\s+(?:`?)(\w+)(?:`?)", upper)
if alter_match:
return alter_match.group(1).lower()
# CREATE TABLE table_name ...
create_match = re.search(r"CREATE\s+TABLE\s+(?:`?)(\w+)(?:`?)", upper)
if create_match:
return create_match.group(1).lower()
return "unknown"
def _extract_where(self, sql: str) -> str:
"""提取 WHERE 子句。"""
where_match = re.search(r"WHERE\s+(.+?)(?:\s+ORDER\s+BY|\s+LIMIT\s+|$)",
sql, re.IGNORECASE)
return where_match.group(1).strip() if where_match else ""
def _has_pk_condition(self, where_clause: str) -> bool:
"""检查 WHERE 是否包含主键条件。"""
pk_patterns = [r"\bid\s*[=<>]", r"\bpk_\w+\s*[=<>]"]
return any(re.search(p, where_clause, re.IGNORECASE) for p in pk_patterns)
def _has_index_condition(self, where_clause: str) -> bool:
"""检查 WHERE 是否可能使用索引。"""
# 包含等值条件通常可以使用索引
return "=" in where_clause
async def _get_table_size(self, instance: Instance, database: str,
table: str) -> float:
"""获取表大小(MB)。简化实现,实际应查询数据库。"""
# 在实际实现中,应该查询 INFORMATION_SCHEMA
# 这里返回一个保守估计
return 50.0 # 默认假设 50MB
async def _get_table_row_count(self, instance: Instance, database: str,
table: str) -> int:
"""获取表行数。简化实现。"""
# 实际应执行 SELECT COUNT(*)
return 10000 # 默认假设 10000 行