"""SQL助手 API — 人性化辅助功能。""" import time from typing import Optional from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.crypto import decrypt_password from app.core.permissions import check_instance_access, check_instance_permission from app.core.security import get_current_user from app.models.user import User from app.models.instance import Instance from app.services.sql_assistant import SQLAssistant from app.services.explain_service import ExplainService router = APIRouter(prefix="/sql-assistant", tags=["SQL助手"]) class SQLAnalyzeRequest(BaseModel): sql: str db_type: str = "mysql" class SQLFormatRequest(BaseModel): sql: str @router.post("/analyze") async def analyze_sql( req: SQLAnalyzeRequest, current_user: User = Depends(get_current_user), ): """全面分析SQL — 格式化+高亮+风险+建议。 一次调用获取所有辅助信息,帮助用户写出更好的SQL。 """ if not req.sql or not req.sql.strip(): raise HTTPException(status_code=400, detail="SQL内容不能为空") result = SQLAssistant.analyze(req.sql, req.db_type) return result @router.post("/format") async def format_sql( req: SQLFormatRequest, current_user: User = Depends(get_current_user), ): """格式化SQL — 统一代码风格。 - 关键字大写 - 合理换行缩进 """ if not req.sql or not req.sql.strip(): raise HTTPException(status_code=400, detail="SQL内容不能为空") result = SQLAssistant.format_sql(req.sql) return { "formatted": result.formatted, "is_valid": result.is_valid, "error": result.error, } @router.post("/highlight") async def highlight_sql( req: SQLAnalyzeRequest, current_user: User = Depends(get_current_user), ): """SQL语法高亮+风险检测。 返回高亮文本和风险提示列表。 """ if not req.sql or not req.sql.strip(): raise HTTPException(status_code=400, detail="SQL内容不能为空") result = SQLAssistant.highlight_sql(req.sql) return { "highlighted": result.highlighted, "keywords": result.keywords, "tables": result.tables, "risks": result.risks, } @router.post("/suggestions") async def query_suggestions( req: SQLAnalyzeRequest, current_user: User = Depends(get_current_user), ): """获取查询建议 — 优化提示。 基于SQL内容提供人性化的改进建议。 """ if not req.sql or not req.sql.strip(): raise HTTPException(status_code=400, detail="SQL内容不能为空") suggestions = SQLAssistant.get_query_suggestions(req.sql, req.db_type) return {"suggestions": suggestions, "count": len(suggestions)} @router.post("/parse-explain") async def parse_explain( explain_rows: list, current_user: User = Depends(get_current_user), ): """解析MySQL EXPLAIN结果为人类可读格式。""" if not explain_rows: raise HTTPException(status_code=400, detail="EXPLAIN结果不能为空") result = SQLAssistant.parse_explain(explain_rows) return { "is_simple": result.is_simple, "has_index": result.has_index, "has_full_scan": result.has_full_scan, "estimated_rows": result.estimated_rows, "warnings": result.warnings, "human_readable": result.human_readable, } # ===== EXPLAIN 真实执行 API ===== class ExplainRequest(BaseModel): """EXPLAIN 请求体。""" instance_id: int database: str query: str @router.post("/explain") async def execute_explain( req: ExplainRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """连接数据源执行 EXPLAIN 并解析为统一树结构。 根据数据源类型自动构造对应的 EXPLAIN 语句: - MySQL: EXPLAIN FORMAT=JSON - PostgreSQL: EXPLAIN (FORMAT JSON, ANALYZE false) - SQLite: EXPLAIN QUERY PLAN 返回统一的树形执行计划、优化警告和汇总统计。 """ if current_user.role == "guest": raise HTTPException(status_code=403, detail="访客无权执行 EXPLAIN,请联系管理员申请权限") if not req.query or not req.query.strip(): raise HTTPException(status_code=400, detail="查询语句不能为空") # 获取实例 result = await db.execute(select(Instance).where(Instance.id == req.instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在,请确认实例ID正确") # 权限检查 await check_instance_access(current_user, instance, db) await check_instance_permission(current_user, instance, "query:select", db) # 解密密码 instance.password = decrypt_password(instance.password) start_time = time.time() try: explain_result = await ExplainService.execute_explain( instance, req.database, req.query ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"EXPLAIN 执行失败: {e}") elapsed_ms = int((time.time() - start_time) * 1000) explain_result["elapsed_ms"] = elapsed_ms return explain_result