// 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) }