"""迁移状态诊断:比对库实际结构、`alembic_version` 记录与迁移链 head,给出对齐建议。 方案 A 把 39 张基线表补进 Alembic 链首(`20260909_baseline_schema`)后,存在三类库: 1. **空库**:直接 `alembic upgrade head` 全量建库; 2. **由 Alembic 迁移建起的库**(如本地 `jr`):`alembic_version` 已位于新链上, 直接 `upgrade head` 即可,Alembic 不会重跑祖先迁移; 3. **由 DBA 脚本手工建库、`alembic_version` 为空**的库:直接 `upgrade head` 会尝试重建 已存在的表而失败,**必须先 `alembic stamp <与当前结构相符的版本>`**。 本脚本只读诊断,不执行任何 DDL,也不代替人工执行 stamp。它按"结构特征"由新到旧推断 库实际所处的版本,从而给出可执行的建议命令。 用法: python tools/migration_state_check.py """ from __future__ import annotations import sys from pathlib import Path from sqlalchemy import create_engine, text from sqlalchemy.engine import Connection ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from alembic.config import Config # noqa: E402 from alembic.script import ScriptDirectory # noqa: E402 from app.core.config import get_settings # noqa: E402 # 由新到旧排列:每个版本引入一个可探测的结构特征。 FEATURES: list[tuple[str, str, str]] = [ ( "20260909_memory_active_key", "memory_unit.active_memory_key 生成列", """SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'memory_unit' AND COLUMN_NAME = 'active_memory_key'""", ), ( "20260909_constraint_fix", "fin_market_price 联合唯一键", """SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'fin_market_price' AND INDEX_NAME = 'uk_fin_market_price_product_id_trade_date'""", ), ( "20260909_api_receipt", "api_request_receipt 表", """SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'api_request_receipt'""", ), ( "20260909_session", "svc_conversation_session 表", """SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'svc_conversation_session'""", ), ( "20260909_outbox_delivery", "outbox_delivery 表", """SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'outbox_delivery'""", ), ( "20260909_agent_platform_v31", "agent_run 表", """SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'agent_run'""", ), ( "20260909_baseline_schema", "sys_user 表", """SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sys_user'""", ), ] def satisfied(connection: Connection, statement: str) -> bool: return bool(connection.execute(text(statement)).scalar()) def infer_revision(connection: Connection) -> tuple[str | None, str | None]: """按结构特征推断库所处版本,返回 (revision, 命中的特征描述)。""" for revision, description, statement in FEATURES: if satisfied(connection, statement): return revision, description return None, None def main() -> int: config = Config(str(ROOT / "alembic.ini")) heads = sorted(ScriptDirectory.from_config(config).get_heads()) engine = create_engine(get_settings().mysql_dsn.replace("mysql+asyncmy", "mysql+pymysql")) with engine.connect() as connection: # 空库连 alembic_version 表都还没有,必须先探测再查询,否则报 1146。 has_version_table = bool( connection.execute( text( "SELECT COUNT(*) FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'alembic_version'" ) ).scalar() ) recorded = ( connection.execute(text("SELECT version_num FROM alembic_version")).scalars().all() if has_version_table else [] ) tables = connection.execute( text( "SELECT COUNT(*) FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME <> 'alembic_version'" ) ).scalar() inferred, feature = infer_revision(connection) engine.dispose() print(f"迁移链 head : {', '.join(heads) or '(none)'}") print(f"alembic_version : {', '.join(recorded) if recorded else '(空,未记录任何版本)'}") print(f"业务表数量 : {tables}") print(f"结构推断版本 : {inferred or '无法匹配已知版本'}" + (f"(命中特征:{feature})" if feature else "")) print() if not recorded and not tables: print("判定:空库。") print("建议:alembic upgrade head") return 0 if not recorded and tables: if inferred is None: print("判定:库中有表但无版本记录,且结构不符合任何已知版本。") print("建议:人工核对库结构后再决定 stamp 目标;不要直接 upgrade(会重建已有表而失败)。") return 1 print("判定:库由脚本或人工建起,没有 Alembic 版本记录。") print("直接 upgrade 会尝试重建已存在的表并失败,必须先对齐版本记录:") print(f"建议:alembic stamp {inferred}") print(" alembic upgrade head") return 0 if set(recorded) == set(heads): print("判定:版本记录已位于 head,无需升级。") print("建议:alembic current 复核;结构差异用 tools/audit_constraints.py 检查。") return 0 print("判定:版本记录不在 head。") print("建议:alembic current # 确认当前版本") print(" alembic upgrade head") if inferred is not None and inferred not in recorded: print(f"注意:结构特征显示库可能已处于 {inferred},但版本记录为 {', '.join(recorded)};") print(" 若二者不符,请先核对结构再决定是否 stamp,不要盲目 upgrade。") return 0 if __name__ == "__main__": raise SystemExit(main())