#!/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 侧多出,无害)"
|