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())
|