Files
group_fqcd_jr/tools/migration_state_check.py
T

162 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""迁移状态诊断:比对库实际结构、`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())