safety-eval-service/docs/db-diff/compare_schema.py

160 lines
6.0 KiB
Python

# -*- coding: utf-8 -*-
"""
只读对比:本地库 vs 线上库 结构差异,并生成升级 SQL。
本脚本仅对 information_schema 执行 SELECT 查询,绝不修改线上数据库。
"""
import json
import pymysql
SCHEMA = "safety-eval-service"
LOCAL = dict(host="192.168.20.100", port=33080,
user="root", password="Mysql@zcloud33080")
ONLINE = dict(host="nlb-kd2xz70qhllfet2koj.cn-beijing.nlb.aliyuncsslb.com",
port=33068, user="root", password="5tS3owZ7w8Uk1egv")
COL_SQL = """
SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_TYPE, IS_NULLABLE,
COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA=%s
ORDER BY TABLE_NAME, ORDINAL_POSITION
"""
TBL_SQL = """
SELECT TABLE_NAME, ENGINE, TABLE_COLLATION, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA=%s AND TABLE_TYPE='BASE TABLE'
ORDER BY TABLE_NAME
"""
IDX_SQL = """
SELECT TABLE_NAME, INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME, INDEX_TYPE
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA=%s
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
"""
def fetch(conn_args):
conn = pymysql.connect(charset="utf8mb4", **conn_args)
try:
with conn.cursor(pymysql.cursors.DictCursor) as cur:
cur.execute(TBL_SQL, (SCHEMA,))
tables = {r["TABLE_NAME"]: r for r in cur.fetchall()}
cur.execute(COL_SQL, (SCHEMA,))
cols = {}
for r in cur.fetchall():
cols.setdefault(r["TABLE_NAME"], {})[r["COLUMN_NAME"]] = r
cur.execute(IDX_SQL, (SCHEMA,))
idx = {}
for r in cur.fetchall():
idx.setdefault(r["TABLE_NAME"], {}).setdefault(
r["INDEX_NAME"], []).append(r)
finally:
conn.close()
return tables, cols, idx
def q(name):
return "`%s`" % name
def col_def(c):
"""根据本地列定义拼出 DDL 片段。"""
parts = [q(c["COLUMN_NAME"]), c["COLUMN_TYPE"]]
if c["CHARACTER_SET_NAME"] and c["COLLATION_NAME"]:
# 保持与表默认字符集一致,通常无需显式声明,这里省略以免冲突
pass
parts.append("NOT NULL" if c["IS_NULLABLE"] == "NO" else "NULL")
default = c["COLUMN_DEFAULT"]
extra = (c["EXTRA"] or "")
if default is not None:
up = str(default).upper()
if up in ("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP()") or \
up.startswith("CURRENT_TIMESTAMP(") or extra.upper().startswith("DEFAULT_GENERATED"):
parts.append("DEFAULT %s" % default)
else:
parts.append("DEFAULT '%s'" % str(default).replace("'", "''"))
elif c["IS_NULLABLE"] == "YES":
parts.append("DEFAULT NULL")
extra_clean = extra.replace("DEFAULT_GENERATED", "").strip()
if extra_clean:
parts.append(extra_clean.upper())
if c["COLUMN_COMMENT"]:
parts.append("COMMENT '%s'" % c["COLUMN_COMMENT"].replace("'", "''"))
return " ".join(parts)
def main():
l_tbl, l_col, l_idx = fetch(LOCAL)
o_tbl, o_col, o_idx = fetch(ONLINE)
report = {
"local_table_count": len(l_tbl),
"online_table_count": len(o_tbl),
"missing_tables": sorted(set(l_tbl) - set(o_tbl)),
"extra_tables": sorted(set(o_tbl) - set(l_tbl)),
"missing_columns": [],
"extra_columns": [],
"diff_columns": [],
"missing_indexes": [],
}
for t in sorted(set(l_tbl) & set(o_tbl)):
lc, oc = l_col.get(t, {}), o_col.get(t, {})
for name in lc:
if name not in oc:
report["missing_columns"].append(
{"table": t, "column": name, "def": col_def(lc[name]),
"pos": lc[name]["ORDINAL_POSITION"]})
for name in oc:
if name not in lc:
report["extra_columns"].append({"table": t, "column": name,
"type": oc[name]["COLUMN_TYPE"]})
for name in lc:
if name not in oc:
continue
a, b = lc[name], oc[name]
diffs = {}
if a["COLUMN_TYPE"] != b["COLUMN_TYPE"]:
diffs["type"] = [b["COLUMN_TYPE"], a["COLUMN_TYPE"]]
if a["IS_NULLABLE"] != b["IS_NULLABLE"]:
diffs["nullable"] = [b["IS_NULLABLE"], a["IS_NULLABLE"]]
if (a["COLUMN_DEFAULT"] or "") != (b["COLUMN_DEFAULT"] or ""):
diffs["default"] = [b["COLUMN_DEFAULT"], a["COLUMN_DEFAULT"]]
if (a["EXTRA"] or "") != (b["EXTRA"] or ""):
diffs["extra"] = [b["EXTRA"], a["EXTRA"]]
if (a["COLUMN_COMMENT"] or "") != (b["COLUMN_COMMENT"] or ""):
diffs["comment"] = [b["COLUMN_COMMENT"], a["COLUMN_COMMENT"]]
if diffs:
report["diff_columns"].append(
{"table": t, "column": name, "diffs": diffs,
"def": col_def(a)})
li, oi = l_idx.get(t, {}), o_idx.get(t, {})
for iname, rows in li.items():
if iname == "PRIMARY":
continue
if iname not in oi:
report["missing_indexes"].append({
"table": t, "index": iname,
"unique": rows[0]["NON_UNIQUE"] == 0,
"columns": [r["COLUMN_NAME"] for r in rows]})
with open("schema_diff.json", "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2, default=str)
print("本地表数:", report["local_table_count"],
" 线上表数:", report["online_table_count"])
print("线上缺表:", len(report["missing_tables"]), report["missing_tables"])
print("线上多表:", len(report["extra_tables"]), report["extra_tables"])
print("线上缺字段:", len(report["missing_columns"]))
print("线上多字段:", len(report["extra_columns"]))
print("字段定义不一致:", len(report["diff_columns"]))
print("线上缺索引:", len(report["missing_indexes"]))
if __name__ == "__main__":
main()