Files
group_fqcd_jr/tools/migration_state_check.py
T

162 lines
6.5 KiB
Python
Raw Normal View History

"""迁移状态诊断:比对库实际结构、`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())