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