#!/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"