#!/usr/bin/env python3 # -*- coding: utf-8 -*- """DDL 迁移基座执行器(T01/T02):幂等导入 models JSON 生成的 DDL 至 MariaDB hrs 库。 特性: - CREATE TABLE IF NOT EXISTS(可重跑); - 缺索引自动补建(先查 information_schema.STATISTICS); - 迁移台账写入 hrs.schema_migration(version/表数/时间/校验和),重复执行只记一条最新; - 依赖 mysql 客户端或 pymysql,自动探测。 用法: python3 scripts/apply_ddl.py [--host 127.0.0.1] [--user test] [--password test123] [--db hrs] [--models models] [--version T01] """ import argparse, glob, hashlib, json, os, shutil, subprocess, sys BASE = os.path.join(os.path.dirname(__file__), "..") sys.path.insert(0, os.path.join(BASE, "scripts")) from json2ddl import gen_table_ddl, gen_index_guard_sql # noqa: E402 def run_sql_cli(sql, args): cmd = ["mysql", f"-h{args.host}", f"-P{args.port}", f"-u{args.user}", f"-p{args.password}", args.db] p = subprocess.run(cmd, input=sql.encode("utf-8"), capture_output=True) if p.returncode != 0: raise RuntimeError(p.stderr.decode("utf-8", "ignore")) return p.stdout.decode("utf-8", "ignore") def exec_sql(sql, args): try: import pymysql conn = pymysql.connect(host=args.host, port=args.port, user=args.user, password=args.password, database=args.db, charset="utf8mb4") try: with conn.cursor() as cur: cur.execute(sql) conn.commit() return None finally: conn.close() except ImportError: run_sql_cli(sql, args) return None def query_sql(sql, args): if shutil.which("mysql"): out = run_sql_cli(sql + "\n", args) return [ln.split("\t") for ln in out.strip().splitlines() if ln] import pymysql conn = pymysql.connect(host=args.host, port=args.port, user=args.user, password=args.password, database=args.db, charset="utf8mb4") try: with conn.cursor() as cur: cur.execute(sql) return [list(r) for r in cur.fetchall()] finally: conn.close() def main(): ap = argparse.ArgumentParser() ap.add_argument("--host", default="127.0.0.1") ap.add_argument("--port", default=3306, type=int) ap.add_argument("--user", default="test") ap.add_argument("--password", default="test123") ap.add_argument("--db", default="hrs") ap.add_argument("--models", default=os.path.join(BASE, "models")) ap.add_argument("--version", default="T01") args = ap.parse_args() # 0) 迁移台账表 exec_sql("""CREATE TABLE IF NOT EXISTS `schema_migration` ( `version` VARCHAR(32) NOT NULL COMMENT '迁移版本号', `applied_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '执行时间', `table_count` INT NOT NULL DEFAULT 0 COMMENT '本次管理表数', `checksum` VARCHAR(64) DEFAULT NULL COMMENT 'models JSON 合并校验和', `operator` VARCHAR(64) DEFAULT 'agent.develop' COMMENT '执行人', PRIMARY KEY (`version`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='DDL迁移台账(幂等可重跑)';""", args) # 1) 收集全部 models JSON,逐表建表 + 补索引 created, index_added, total = [], [], 0 h = hashlib.sha256() jobs = [] for moddir in sorted(glob.glob(os.path.join(args.models, "*"))): if not os.path.isdir(moddir): continue for p in sorted(glob.glob(os.path.join(moddir, "*.json"))): doc = json.load(open(p, encoding="utf-8")) h.update(open(p, "rb").read()) table, ddl = gen_table_ddl(p) jobs.append((os.path.basename(moddir), table, ddl, doc)) total += 1 for mod, table, ddl, doc in jobs: before = query_sql(f"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='{args.db}' AND TABLE_NAME='{table}'", args) n_exist = int(before[0][0]) if before else 0 exec_sql(ddl, args) if not n_exist: created.append(table) # 索引补建(幂等) rows = query_sql(f"SELECT INDEX_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA='{args.db}' AND TABLE_NAME='{table}'", args) existing_idx = {r[0] for r in rows} for idx in doc.get("indexes", []): if idx["name"] not in existing_idx: exec_sql(gen_index_guard_sql(table, idx), args) index_added.append(f"{table}.{idx['name']}") checksum = h.hexdigest()[:16] # 2) 登记迁移台账(幂等 upsert) exec_sql(f"""INSERT INTO schema_migration (version, table_count, checksum) VALUES ('{args.version}', {total}, '{checksum}') ON DUPLICATE KEY UPDATE applied_at=CURRENT_TIMESTAMP, table_count={total}, checksum='{checksum}';""", args) # 3) 校验 rows = query_sql(f"SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='{args.db}'", args) print(f"[apply_ddl] 管理业务表 {total} 张;新建 {len(created)} 张;补索引 {len(index_added)} 个") print(f"[apply_ddl] 新建表: {', '.join(created) if created else '(无,全部已存在,幂等重跑)'}") print(f"[apply_ddl] schema_migration version={args.version} table_count={total} checksum={checksum}") print(f"[apply_ddl] 库 {args.db} 当前表总数: {rows[0][0]}") if __name__ == "__main__": main()