刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# schema 漂移检查:对照远程 MySQL(事实源)与本地 PG 的表级 + 列级差异
#
# 背景:self_built_tables_pg.sql 是迁移期快照,远端是活跃开发库(L035)——此后
#   远端持续加列/建表。合并 main 带业务表变更、或数据刷新(migrate-data.sh)之前,
#   先跑本脚本;否则漂移会静默溜进交付包(启动/冒烟全过,点到新功能才
#   "column does not exist")。已发生两例:环境监测 31 列(update_lims_huanjing_
#   20260717.sql)、sxt/稳定性 7 列 + 6 表(update_lims_sxt_wendingxing_20260717.sql)。
#
# 用法(凭据经环境变量传入,绝不写进脚本;远程库连接参数见项目内部手册):
#   MYSQL_HOST=<host> MYSQL_PASSWORD=<pw> ./schema-drift-check.sh
#
# 参数(环境变量):
#   MYSQL_HOST      必填
#   MYSQL_PASSWORD  必填
#   MYSQL_PORT=3306  MYSQL_USER=root  MYSQL_DB=jnpf_init
#   MYSQL_CLIENT_IMAGE=mysql:8.0   本地 mysql 客户端是 9.x,走容器客户端(项目约定,L010 utf8mb4)
#   CONTAINER=cx-postgres  PG_DB=jnpf_init
#   EXCLUDE_RE=      按表名正则排除(如 '_bak$|_tmp$'),两侧同时生效
#
# 判定与退出码:
#   MySQL 有而 PG 缺(表/列)=阻断性漂移 → 打印明细含类型/注释(可直接照抄写增量 SQL),exit 1
#   PG 有而 MySQL 缺 =警告(远端已删列/本地保留,删列有数据风险,默认不处理),exit 0
#   两侧列名统一小写比较(PG 未加引号标识符折叠小写,L035 大小写折叠坑)
#
# 发现阻断性漂移后的处置(SOP 详见 docs/docker-compose-guide.md §18):
#   1. 按输出明细写增量 SQL(幂等 ADD COLUMN IF NOT EXISTS)入 docs/pg-migration-ddl/
#   2. 接线 install.sh + assemble-package.sh(照 update_lims_huanjing_20260717.sql 先例)
#   3. 本地 PG 应用 → 重跑本脚本应转绿 → make jars && make up && make smoke
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
 
# ── 退役护栏(2026-07-25)─────────────────────────────────────────────────────
# 本脚本以「远程 MySQL = 事实源」为前提做 schema 对照,该前提自 2026-07-20 起消失:
# 现事实源是腾讯云 CVM 共享 PG,MySQL 停在 07-16 且不再有人改它。
# 脚本本身只读无破坏性,但其结论已失效 —— 会把"PG 比 MySQL 多出来的新表/新列"报成漂移,
# 照着去"补"反而会破坏当前 schema。故默认拒绝,避免有人拿失效结论行动。
# 现在要查 schema 差异,应改为对照「本地 PG ↔ CVM 共享 PG」,而非 MySQL。
if [ "${I_KNOW_MIGRATION_IS_RETIRED:-}" != "1" ]; then
  printf '\033[1;31m[已退役·拒绝执行]\033[0m %s\n' "schema-drift-check.sh 的对照基准(远程 MySQL)已不是事实源。" >&2
  printf '  它会把 PG 侧新增的表/列误报成"缺列漂移",照此修改会破坏当前 schema。\n' >&2
  printf '  查 schema 差异请改为对照 本地 PG ↔ CVM 共享 PG(YOUR_DB_HOST:15432)。\n' >&2
  printf '  仅需与历史 MySQL 做考古对照时:I_KNOW_MIGRATION_IS_RETIRED=1 %s\n' "$0" >&2
  exit 1
fi
 
MYSQL_HOST="${MYSQL_HOST:?必填:远程 MySQL 地址(连接参数见项目内部手册)}"
MYSQL_PASSWORD="${MYSQL_PASSWORD:?必填:远程 MySQL 密码(经环境变量传入,勿写脚本)}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
MYSQL_USER="${MYSQL_USER:-root}"
MYSQL_DB="${MYSQL_DB:-jnpf_init}"
MYSQL_CLIENT_IMAGE="${MYSQL_CLIENT_IMAGE:-mysql:8.0}"
CONTAINER="${CONTAINER:-cx-postgres}"
PG_DB="${PG_DB:-jnpf_init}"
EXCLUDE_RE="${EXCLUDE_RE:-}"
 
log()  { printf '\033[1;34m[drift]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[drift][warn]\033[0m %s\n' "$*"; }
die()  { printf '\033[1;31m[drift][err]\033[0m %s\n' "$*" >&2; exit 1; }
 
TMP="$(mktemp -d)"
cleanup() { rm -rf "$TMP"; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
 
docker exec "$CONTAINER" true >/dev/null 2>&1 || die "PG 容器 ${CONTAINER} 不在运行"
 
# ── 1. 两侧各一次 information_schema 全量导出(只取 BASE TABLE,排除视图/扩展视图) ──
log "导出远程 MySQL schema(${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DB})…"
docker run --rm "$MYSQL_CLIENT_IMAGE" mysql --default-character-set=utf8mb4 \
  -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" \
  information_schema --batch -sN -e "
    SELECT LOWER(c.table_name), LOWER(c.column_name), c.column_type, c.is_nullable, c.column_comment
      FROM columns c JOIN tables t
        ON t.table_schema=c.table_schema AND t.table_name=c.table_name
     WHERE c.table_schema='$MYSQL_DB' AND t.table_type='BASE TABLE'
     ORDER BY 1,2" 2>/dev/null > "$TMP/mysql.tsv" \
  || die "远程 MySQL 导出失败(网络/凭据?)"
 
log "导出本地 PG schema(${CONTAINER}/${PG_DB})…"
docker exec "$CONTAINER" psql -U postgres -d "$PG_DB" -tA -F$'\t' -c "
    SELECT lower(c.table_name), lower(c.column_name)
      FROM information_schema.columns c JOIN information_schema.tables t
        ON t.table_schema=c.table_schema AND t.table_name=c.table_name
     WHERE c.table_schema='public' AND t.table_type='BASE TABLE'
     ORDER BY 1,2" > "$TMP/pg.tsv" \
  || die "本地 PG 导出失败"
 
# 表.列 键集合(可选排除)
mkkeys() { # $1=in tsv  $2=out keys
  if [ -n "$EXCLUDE_RE" ]; then
    awk -F'\t' -v re="$EXCLUDE_RE" '$1 !~ re {print $1"."$2}' "$1" > "$2"
  else
    awk -F'\t' '{print $1"."$2}' "$1" > "$2"
  fi
}
mkkeys "$TMP/mysql.tsv" "$TMP/mysql.keys"
mkkeys "$TMP/pg.tsv"    "$TMP/pg.keys"
cut -d. -f1 "$TMP/mysql.keys" | sort -u > "$TMP/mysql.tables"
cut -d. -f1 "$TMP/pg.keys"    | sort -u > "$TMP/pg.tables"
 
N_MY_T=$(wc -l < "$TMP/mysql.tables" | tr -d ' '); N_PG_T=$(wc -l < "$TMP/pg.tables" | tr -d ' ')
log "MySQL 表 ${N_MY_T} / PG 表 ${N_PG_T}(列键 $(wc -l < "$TMP/mysql.keys" | tr -d ' ') / $(wc -l < "$TMP/pg.keys" | tr -d ' '))"
 
# ── 2. 表级差集 ──────────────────────────────────────────────────────────────
comm -23 "$TMP/mysql.tables" "$TMP/pg.tables" > "$TMP/tables.missing"   # 阻断
comm -13 "$TMP/mysql.tables" "$TMP/pg.tables" > "$TMP/tables.extra"    # 警告
 
# ── 3. 列级差集(只看两侧都有的表,整表缺失已在表级报过) ────────────────────
comm -12 "$TMP/mysql.tables" "$TMP/pg.tables" > "$TMP/tables.common"
grep -F -f <(sed 's/$/./' "$TMP/tables.common") "$TMP/mysql.keys" 2>/dev/null \
  | comm -23 - "$TMP/pg.keys" > "$TMP/cols.missing" || true             # 阻断
grep -F -f <(sed 's/$/./' "$TMP/tables.common") "$TMP/pg.keys" 2>/dev/null \
  | comm -13 "$TMP/mysql.keys" - > "$TMP/cols.extra" || true            # 警告
 
# ── 4. 报告 ──────────────────────────────────────────────────────────────────
BLOCKING=0
echo
if [ -s "$TMP/tables.missing" ]; then
  BLOCKING=1
  printf '\033[1;31m[drift] ✗ MySQL 有而 PG 缺的表(%s 张,阻断):\033[0m\n' "$(wc -l < "$TMP/tables.missing" | tr -d ' ')"
  sed 's/^/    /' "$TMP/tables.missing"
fi
if [ -s "$TMP/cols.missing" ]; then
  BLOCKING=1
  printf '\033[1;31m[drift] ✗ MySQL 有而 PG 缺的列(%s 个,阻断)——含类型/注释,可照抄写增量 SQL:\033[0m\n' "$(wc -l < "$TMP/cols.missing" | tr -d ' ')"
  # 从 mysql.tsv 反查类型与注释
  awk -F'\t' 'NR==FNR{miss[$0]=1; next} (($1"."$2) in miss){printf "    %-40s %-30s %-14s %s %s\n", $1, $2, $3, ($4=="YES"?"NULL":"NOT NULL"), $5}' \
      "$TMP/cols.missing" "$TMP/mysql.tsv"
fi
if [ -s "$TMP/tables.extra" ]; then
  warn "PG 有而 MySQL 缺的表($(wc -l < "$TMP/tables.extra" | tr -d ' ') 张,通常=远端已删或本地自建,默认保留):"
  sed 's/^/    /' "$TMP/tables.extra"
fi
if [ -s "$TMP/cols.extra" ]; then
  warn "PG 有而 MySQL 缺的列($(wc -l < "$TMP/cols.extra" | tr -d ' ') 个,远端已删/本地保留,删列有数据风险,默认不处理):"
  sed 's/^/    /' "$TMP/cols.extra"
fi
 
echo
if [ "$BLOCKING" = 1 ]; then
  printf '\033[1;31m[drift][err]\033[0m 存在阻断性漂移:按上方明细写增量 SQL(照 docs/pg-migration-ddl/update_lims_huanjing_20260717.sql 先例:幂等 ADD COLUMN IF NOT EXISTS + 接线 install.sh/assemble-package.sh),SOP 见 docs/docker-compose-guide.md §18\n' >&2
  exit 1
fi
log "无阻断性漂移 ✓(警告项如上,均为 PG 侧多出,无害)"