charset_rewrite_test.sh
6.35 KB
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
#!/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"