"""回滚 SQL API 路由 — 提供回滚 SQL 生成和预览功能。""" from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.security import get_current_user, require_role from app.core.crypto import decrypt_password from app.models.user import User from app.models.instance import Instance from app.models.ticket import Ticket from app.schemas.rollback import ( RollbackPreviewRequest, RollbackPreviewResponse, RollbackGenerateResponse, RollbackGenerateRequest, ) from app.services.rollback_generator import RollbackGenerator, RollbackGeneratorError router = APIRouter(tags=["回滚 SQL"]) async def _get_ticket(db: AsyncSession, ticket_id: int) -> Ticket: """获取工单并检查是否存在。 Args: db: 数据库会话 ticket_id: 工单ID Returns: 工单对象 Raises: HTTPException: 工单不存在 """ result = await db.execute(select(Ticket).where(Ticket.id == ticket_id)) ticket = result.scalar_one_or_none() if not ticket: raise HTTPException(status_code=404, detail="工单不存在") return ticket async def _get_instance(db: AsyncSession, instance_id: int) -> Instance: """获取实例并检查是否存在。 Args: db: 数据库会话 instance_id: 实例ID Returns: 实例对象 Raises: HTTPException: 实例不存在 """ result = await db.execute(select(Instance).where(Instance.id == instance_id)) instance = result.scalar_one_or_none() if not instance: raise HTTPException(status_code=404, detail="数据源不存在") return instance @router.post("/tickets/{ticket_id}/generate-rollback", response_model=RollbackGenerateResponse) async def generate_rollback_sql( ticket_id: int, data: RollbackGenerateRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin", "dba")), ): """为工单生成回滚 SQL。 根据工单中的 SQL 内容,自动生成对应的回滚 SQL。 支持 INSERT/UPDATE/DELETE 三种 DML 操作。 Args: ticket_id: 工单ID data: 生成选项(是否自动保存) db: 数据库会话 current_user: 当前用户 Returns: 生成的回滚 SQL 信息 Raises: HTTPException: 工单不存在、无权访问或生成失败 """ ticket = await _get_ticket(db, ticket_id) # 获取关联的数据源实例 instance = await _get_instance(db, ticket.instance_id) instance.password = decrypt_password(instance.password) # 生成回滚 SQL generator = RollbackGenerator() try: rollback_sql = await generator.generate( instance, ticket.target_database, ticket.sql_content ) except RollbackGeneratorError as e: raise HTTPException(status_code=400, detail=str(e)) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"回滚 SQL 生成失败: {str(e)}") # 自动保存到工单 saved = False if data.auto_save: ticket.rollback_sql = rollback_sql await db.commit() saved = True # 检测 SQL 类型 from app.services.sql_analyzer import SQLAnalyzer analyzer = SQLAnalyzer() sql_type = analyzer._detect_sql_type(ticket.sql_content) return RollbackGenerateResponse( ticket_id=ticket_id, original_sql=ticket.sql_content, rollback_sql=rollback_sql, sql_type=sql_type.value if sql_type else None, saved=saved, ) @router.post("/rollback/preview", response_model=RollbackPreviewResponse) async def preview_rollback_sql( req: RollbackPreviewRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """预览回滚 SQL(不保存)。 为给定的 SQL 语句生成回滚 SQL 预览,不执行实际数据库操作, 也不保存到任何工单。 Args: req: 预览请求(实例ID、数据库、SQL) db: 数据库会话 current_user: 当前用户 Returns: 回滚 SQL 预览 Raises: HTTPException: 实例不存在或无权限 """ instance = await _get_instance(db, req.instance_id) instance.password = decrypt_password(instance.password) generator = RollbackGenerator() try: rollback_sql = await generator.preview(instance, req.database, req.sql) except Exception as e: raise HTTPException(status_code=500, detail=f"回滚 SQL 预览失败: {str(e)}") # 检测 SQL 类型 from app.services.sql_analyzer import SQLAnalyzer analyzer = SQLAnalyzer() sql_type = analyzer._detect_sql_type(req.sql) warning = None if rollback_sql.startswith("--"): # 提取警告信息 lines = rollback_sql.split("\n") warning_lines = [l for l in lines if l.startswith("--") and "无法" in l] if warning_lines: warning = warning_lines[0].replace("-- ", "") return RollbackPreviewResponse( original_sql=req.sql, rollback_sql=rollback_sql, sql_type=sql_type.value if sql_type else None, warning=warning, )