| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- // Package dbtest 跨域共用的**测试期**数据库口径工具(非生产代码,不参与业务逻辑)。
- //
- // 依据:unified/03-migration/academic-domain-design.md v0.1.2 §2.4「L1 TINYINT(1) 假阳性裁定」:
- // 取「不动 model,改对账归一」——model 保持 int8/bool 原样,由对账侧把
- //
- // `tinyint(1)`(information_schema 返回的 bool 物理形态,MySQL 8.0.19+ 可能省略显示宽度)
- // 与 DDL 侧书写的 `TINYINT(1)` / `TINYINT`
- //
- // 归一到同一签名后再比对,避免"类型其实一致、只因显示宽度写法不同而误报"。
- //
- // 为什么放共享包(而不是 academic 包内私有函数):
- //
- // org 域的 `t_campus.self_support` / `verified` / `login_allowed` 是同一类 bit(1)→tinyint 迁移列,
- // 将来做真库核对必须复用**同一口径**,否则两域对"类型是否一致"的判定会漂移。
- //
- // 归一规则的顺序敏感性(误用即引入漏检,评审重点):
- // 1. ToUpper + TrimSpace + 空白折叠("tinyint ( 1 )" 不会出现,但多空格出现过);
- // 2. **仅整数族**(TINYINT/SMALLINT/MEDIUMINT/INT/INTEGER/BIGINT)剥离 "(N)" 显示宽度;
- // INTEGER 归一为 INT(同义词);
- // 3. BOOL/BOOLEAN 归一为 TINYINT(MySQL 里就是 tinyint(1) 的别名);
- // 4. **其余类型一律保留参数**:decimal(12,2) / varchar(N) / datetime(3) / double(p,s)
- // 的参数是精度语义,归一掉就再也发现不了 decimal(12,3) 这类真缺陷。
- package dbtest
- import (
- "strings"
- "unicode"
- )
- // integerFamily 可安全剥离显示宽度的整数族(MySQL 中 "(N)" 只影响显示,不影响存储与语义)。
- //
- // 注意:**不包含** DECIMAL/NUMERIC/FLOAT/DOUBLE(那里的括号是精度)、也不包含
- // VARCHAR/CHAR/BINARY(那里的括号是长度)——它们的参数必须参与比对。
- var integerFamily = map[string]struct{}{
- "TINYINT": {},
- "SMALLINT": {},
- "MEDIUMINT": {},
- "INT": {},
- "INTEGER": {},
- "BIGINT": {},
- }
- // NormalizeColumnType 把 DDL/信息模式里的列类型归一为可比较签名。
- //
- // 入参:
- // - raw string:原始列类型(可含大小写/空白差异,如 "tinyint(1)"、"TINYINT"、"boolean"、
- // "decimal(12,2)"、"varchar(26)"、"datetime(3)"、"bigint unsigned")。
- //
- // 出参:
- // - string:归一签名(大写)。空入参 → 空串(由调用方判为不合法,不在此处造错误)。
- //
- // 归一样例:
- //
- // "tinyint(1)" → "TINYINT" "TINYINT" → "TINYINT"
- // "boolean" → "TINYINT" "bool" → "TINYINT"
- // "int(11)" → "INT" "integer" → "INT"
- // "bigint unsigned" → "BIGINT UNSIGNED"(属性保留,只有显示宽度被剥离)
- // "decimal(12,2)" → "DECIMAL(12,2)"(**精度保留**)
- // "varchar(26)" → "VARCHAR(26)"(**长度保留**)
- // "datetime(3)" → "DATETIME(3)"(**小数秒精度保留**)
- func NormalizeColumnType(raw string) string {
- // ① 去空白:information_schema 不会插空格,但手工/日志粘贴的字符串常带
- // " tinyint ( 1 ) " 这种形态,折叠成紧凑形态后再解析,避免无意义的误报。
- compact := make([]rune, 0, len(raw))
- for _, r := range strings.ToUpper(raw) {
- if unicode.IsSpace(r) {
- continue
- }
- compact = append(compact, r)
- }
- s := string(compact)
- if s == "" {
- return ""
- }
- // ② 剥离尾部存储属性(UNSIGNED / ZEROFILL 是存储语义,**不**归一掉,只重排到最后)
- attrs := make([]string, 0, 2)
- for {
- switch {
- case strings.HasSuffix(s, "UNSIGNED"):
- attrs = append([]string{"UNSIGNED"}, attrs...)
- s = strings.TrimSuffix(s, "UNSIGNED")
- continue
- case strings.HasSuffix(s, "ZEROFILL"):
- attrs = append([]string{"ZEROFILL"}, attrs...)
- s = strings.TrimSuffix(s, "ZEROFILL")
- continue
- }
- break
- }
- name := s
- if idx := strings.IndexByte(s, '('); idx >= 0 {
- name = s[:idx] // 用于判别族,参数仍保留在 base 中
- }
- base := s
- if _, ok := integerFamily[name]; ok {
- if name == "INTEGER" {
- name = "INT" // 同义词收敛
- }
- base = name // 整数族:剥离 "(N)" 显示宽度
- } else if name == "BOOL" || name == "BOOLEAN" {
- base = "TINYINT" // MySQL 把 bool/boolean 落成 tinyint(1)
- }
- // 非整数族:base 保持原样(decimal(12,2)/varchar(26)/datetime(3) 的参数是精度/长度语义)
- if len(attrs) > 0 {
- base += " " + strings.Join(attrs, " ")
- }
- return base
- }
- // SameColumnType 判定两个列类型是否等价(两侧各归一后逐字相等)。
- //
- // 入参:a/b string——来自 DDL 与 information_schema 的列类型字符串(顺序无关)。
- // 出参:true 表示等价。
- func SameColumnType(a, b string) bool {
- return NormalizeColumnType(a) == NormalizeColumnType(b)
- }
|