compare_dup_tables.py 6.16 KB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
六库重名表结构比对 —— P2 合库前的核心检查。

合库时"同名表直接搬"只在结构一致时成立。不一致就必须改名或建视图过渡,
否则数据会串。本脚本对每组重名表算一个**规范化结构签名**(列 + 类型 + 可空 +
默认值 + 键 + 排序规则 + 索引),签名相同的直接判定可合并,不同的展开差异明细。

只读:仅查 information_schema + SHOW CREATE TABLE(后者只在需要展示差异时调)。
"""

import hashlib
import os
import re
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from legacy_db import load_env, connect  # noqa: E402

DBS = ['zt_sys', 'zt_park', 'zt_order', 'zt_order_n', 'zt_bill', 'zt_charge']


def col_sig(cur, db, table):
    """列定义规范化。忽略列注释/自增值这些不影响数据搬运的差异。"""
    cur.execute(
        "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, IFNULL(COLUMN_DEFAULT,'~'), "
        "       COLUMN_KEY, EXTRA, IFNULL(COLLATION_NAME,'') "
        "FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=%s AND TABLE_NAME=%s "
        "ORDER BY COLUMN_NAME", (db, table))
    return [(r[0].lower(), r[1].lower(), r[2], str(r[3]), r[4],
             re.sub(r'auto_increment', '', r[5]).strip(), r[6]) for r in cur.fetchall()]


def idx_sig(cur, db, table):
    """索引定义规范化(索引名忽略大小写;排除主键的自动命名差异不适用,主键名恒为 PRIMARY)。"""
    cur.execute(
        "SELECT INDEX_NAME, NON_UNIQUE, SEQ_IN_INDEX, COLUMN_NAME, IFNULL(SUB_PART,-1) "
        "FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=%s AND TABLE_NAME=%s "
        "ORDER BY INDEX_NAME, SEQ_IN_INDEX", (db, table))
    return [(r[0].lower(), r[1], r[2], (r[3] or '').lower(), r[4]) for r in cur.fetchall()]


def sig(cur, db, table):
    payload = repr(col_sig(cur, db, table)) + '||' + repr(idx_sig(cur, db, table))
    return hashlib.sha1(payload.encode('utf-8')).hexdigest()[:12], payload


COL_ATTRS = ['列名', '类型', '可空', '默认值', '键', 'EXTRA', '排序规则']


def col_diff(a_cols, b_cols):
    """
    列级差异(a=左库,b=右库)。

    ⚠️ 必须展开到**属性级**:只打 `左 varchar(32) / 右 varchar(32)` 会让人以为"没差别",
    实际差异在默认值 / 排序规则 / EXTRA(on update CURRENT_TIMESTAMP)上——
    这三类差异恰恰是合库时最容易漏、合完才发现数据不对的。
    """
    da = {c[0]: c for c in a_cols}
    db_ = {c[0]: c for c in b_cols}
    out = []
    for name in sorted(set(da) | set(db_)):
        if name not in da:
            out.append('  + %-30s 仅右边有: %s' % (name, db_[name][1]))
        elif name not in db_:
            out.append('  - %-30s 仅左边有: %s' % (name, da[name][1]))
        elif da[name] != db_[name]:
            det = []
            for i, label in enumerate(COL_ATTRS):
                if da[name][i] != db_[name][i]:
                    det.append('%s 左[%s]/右[%s]' % (label, da[name][i], db_[name][i]))
            out.append('  ~ %-30s %s' % (name, '; '.join(det) if det else '未知差异'))
    return out


def idx_diff(a_idx, b_idx):
    """索引差异:按索引名比对,缺索引 / 列顺序不同 / 唯一性不同。"""
    da, db_ = {}, {}
    for r in a_idx:
        da.setdefault(r[0], []).append(r)
    for r in b_idx:
        db_.setdefault(r[0], []).append(r)
    out = []
    for n in sorted(set(da) | set(db_)):
        if n not in da:
            out.append('  + 索引 %s 仅右边有' % n)
        elif n not in db_:
            out.append('  - 索引 %s 仅左边有' % n)
        elif da[n] != db_[n]:
            out.append('  ~ 索引 %s 定义不同: 左%s / 右%s' % (
                n,
                [(x[3], x[1]) for x in da[n]],
                [(x[3], x[1]) for x in db_[n]]))
    return out


def main():
    cfg = load_env(os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                '..', '..', '私密', 'legacy-ro.env'))
    conn = connect(cfg)
    cur = conn.cursor()
    inlist = ','.join("'" + d + "'" for d in DBS)

    cur.execute("SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TABLES "
                "WHERE TABLE_SCHEMA IN (" + inlist + ")")
    where = {}
    for s, t in cur.fetchall():
        where.setdefault(t.lower(), {})[s] = t      # 记原始大小写

    dups = {n: v for n, v in where.items() if len(v) > 1}
    print('跨库重名表 %d 组\n' % len(dups))

    same, diff = [], []
    for name in sorted(dups):
        locs = dups[name]
        sigs, cols, idxs = {}, {}, {}
        for dbname, real in sorted(locs.items()):
            s, _p = sig(cur, dbname, real)
            sigs[dbname] = s
            cols[dbname] = col_sig(cur, dbname, real)
            idxs[dbname] = idx_sig(cur, dbname, real)
        uniq = set(sigs.values())
        if len(uniq) == 1:
            same.append((name, sorted(locs)))
        else:
            diff.append((name, sorted(locs), sigs, cols, idxs))

    print('=' * 78)
    print('【A】结构一致(可直接合并): %d 组' % len(same))
    print('=' * 78)
    for name, locs in same:
        print('  %-40s %s' % (name, '、'.join(locs)))

    print()
    print('=' * 78)
    print('【B】⚠️ 结构不一致(必须处理): %d 组' % len(diff))
    print('=' * 78)
    for name, locs, sigs, cols, idxs in diff:
        print('\n── %s   %s' % (name, '、'.join('%s=%s' % (k, v) for k, v in sigs.items())))
        base = locs[0]
        for other in locs[1:]:
            print('   [%s] vs [%s]' % (base, other))
            d = col_diff(cols[base], cols[other])
            if not d:
                print('     列完全一致 —— 差异只在索引层')
            for line in d[:18]:
                print(line)
            if len(d) > 18:
                print('     ...(另有 %d 处列差异)' % (len(d) - 18))
            di = idx_diff(idxs[base], idxs[other])
            for line in di[:8]:
                print(line)
            if len(di) > 8:
                print('     ...(另有 %d 处索引差异)' % (len(di) - 8))

    conn.close()
    return 0


if __name__ == '__main__':
    sys.exit(main())