charset_rewrite_test.sh 6.35 KB
#!/usr/bin/env bash
#
# 10_dump_legacy.sh 里 rewrite_charset / guard_charset 的自测(可重复跑)。
#
# 为什么要有这个文件:这两个函数各栽过一次,而且**第二次是 guard 自己放行的**——
#   · 第一次:顺手补 COLLATE,和原有的 COLLATE 撞成 `COLLATE=..ci COLLATE=..bin`(建表报错)
#   · 第二次:`utf8_bin → utf8mb4_bin` 写进去的字面量被最后那条
#     `s/utf8/utf8mb4/g` 二次替换成 `utf8mb4mb4_bin`;
#     而 guard 的 grep 里没有 `mb4mb4`,于是打印了"guard 通过 ✅"——
#     **一个能通过一切输出的守卫等于没有守卫**(AGENTS.md §1.1)。
#
# 所以本文件有两类断言,缺一不可:
#   A. 正向:好输入必须被改对(含 utf8_bin 必须变 utf8mb4_bin,不能拍成 _ci)
#   B. 反向:guard **必须拦下**坏输出——只测正向的话,把 guard 换成 `true` 也能全绿
#
# 用法:  bash sql/migration/tests/charset_rewrite_test.sh
# 说明:  函数是从 10_dump_legacy.sh **现抠**出来的,不复制一份——
#         否则测的是副本,改坏了真文件测试照样绿。

set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT="$HERE/../10_dump_legacy.sh"
[[ -f "$SCRIPT" ]] || { echo "找不到 $SCRIPT" >&2; exit 2; }

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT

# --- 抠函数:只取函数体,跳过主流程(主流程会连库) -------------------------
{
  sed -n '/^rewrite_charset()/,/^}/p' "$SCRIPT"
  sed -n '/^guard_charset()/,/^}/p'  "$SCRIPT"
} > "$TMP/funcs.sh"
# shellcheck disable=SC1090
. "$TMP/funcs.sh"
command -v rewrite_charset >/dev/null || { echo "没抠到 rewrite_charset" >&2; exit 2; }
command -v guard_charset  >/dev/null || { echo "没抠到 guard_charset"  >&2; exit 2; }

# 真脚本里 die 是 exit(guard 失败就该中断 dump,正确);
# 测试里必须换成 return,否则第一次拦下就把测试进程带走 → 后面的用例根本不执行,
# 表现为"没有输出、退出码 1",看起来像通过。(这个假象本人踩过。)
die() { printf '错误: %s\n' "$*" >&2; return 1; }

FAIL=0
ok()   { printf '  ✅ %s\n' "$1"; }
bad()  { printf '  ❌ %s\n' "$1"; FAIL=1; }

# ===========================================================================
echo "A. 正向:字符集改写结果"
# ===========================================================================
cat > "$TMP/in.sql" <<'EOF'
CREATE TABLE `t` (
  `bin_col`   varchar(20) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
  `ci_col`    varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `uni_col`   varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `mb4bin`    varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
  `l1_bare`   varchar(10) CHARACTER SET latin1 DEFAULT NULL,
  `l1_col`    varchar(10) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
  `bare_utf8` text CHARACTER SET utf8,
  `mb3_bin`   varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin DEFAULT NULL,
  `mb3_ci`    varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
-- 另一种客户端(MySQL 8.0.29+ 的 mysqldump)会把 utf8 输出成 utf8mb3:
CREATE TABLE `t3` (
  `a` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;
EOF
rewrite_charset < "$TMP/in.sql" > "$TMP/out.sql"

assert_has() {  # 期望含有的片段
  if grep -qF -- "$1" "$TMP/out.sql"; then ok "有  $2"; else bad "缺  $2(期望出现:$1)"; fi
}
assert_not() {  # 期望不出现的片段
  if grep -qF -- "$1" "$TMP/out.sql"; then bad "多  $2(不该出现:$1)"; else ok "无  $2"; fi
}

# utf8_bin 必须落成 utf8mb4_bin —— 这是语义保留(大小写敏感),不能拍成 _ci
assert_has "COLLATE utf8mb4_bin"            "utf8_bin → utf8mb4_bin"
assert_not "utf8mb4mb4"                     "无二次替换 utf8mb4mb4"
assert_has "COLLATE utf8mb4_general_ci"     "utf8_general_ci → utf8mb4_general_ci"
assert_has "COLLATE utf8mb4_unicode_ci"     "utf8mb4_unicode_ci 原样保留"
assert_has "CHARACTER SET utf8mb4"          "裸 utf8 → utf8mb4"
assert_has "DEFAULT CHARSET=utf8mb4"        "表级 CHARSET=utf8 → utf8mb4"
assert_not "latin1"                         "latin1 全部转走(含 _swedish_ci)"
assert_not "COLLATE=utf8 "                  "无残留 COLLATE=utf8 "
# 重复 COLLATE 会直接建表失败,是本函数第一版栽的坑
assert_not "COLLATE=utf8mb4_general_ci COLLATE"  "无重复 COLLATE"
# utf8mb3:MySQL 8.0.29+ 的 mysqldump 这么写。不单独处理会变成 utf8mb4mb3
assert_not "mb4mb3"                         "无二次替换 utf8mb4mb3"
assert_not "utf8mb3"                        "utf8mb3 已全部转走"
assert_has "COLLATE utf8mb4_bin"            "utf8mb3_bin → utf8mb4_bin(保持大小写敏感)"

if guard_charset "$TMP/out.sql"; then ok "guard 放行改写结果"; else bad "guard 误报改写结果"; fi

# ===========================================================================
echo "B. 反向:guard 必须拦下坏输出(否则它就是个摆设)"
# ===========================================================================
guard_must_fail() {  # $1=说明 $2=坏内容
  printf '%s\n' "$2" > "$TMP/bad.sql"
  if guard_charset "$TMP/bad.sql" >/dev/null 2>&1; then
    bad "guard 漏判:$1"          # 放行了 → 守卫失效
  else
    ok "guard 拦下:$1"
  fi
}
guard_must_fail "双替换 utf8mb4mb4_bin" 'x varchar(9) CHARACTER SET utf8mb4 COLLATE utf8mb4mb4_bin,'
guard_must_fail "双替换 utf8mb4mb3_bin" 'x varchar(9) CHARACTER SET utf8mb4 COLLATE utf8mb4mb3_bin,'
guard_must_fail "残留 utf8mb3"          'x varchar(9) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin,'
guard_must_fail "残留 utf8_bin"         'x varchar(9) CHARACTER SET utf8mb4 COLLATE utf8_bin,'
guard_must_fail "残留 utf8_general_ci"  'x varchar(9) CHARACTER SET utf8 COLLATE utf8_general_ci,'
guard_must_fail "残留 utf8_unicode_ci"  'x varchar(9) CHARACTER SET utf8 COLLATE utf8_unicode_ci,'
guard_must_fail "残留裸 CHARSET=utf8"   ') ENGINE=InnoDB DEFAULT CHARSET=utf8;'
guard_must_fail "残留 latin1"           'x varchar(9) CHARACTER SET latin1,'

echo
if [[ "$FAIL" == 0 ]]; then
  echo "全部通过 ✅"
else
  echo "有失败项 ❌ —— 先修 10_dump_legacy.sh,别绕过测试直接 dump"
fi
exit "$FAIL"