columntype.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. // Package dbtest 跨域共用的**测试期**数据库口径工具(非生产代码,不参与业务逻辑)。
  2. //
  3. // 依据:unified/03-migration/academic-domain-design.md v0.1.2 §2.4「L1 TINYINT(1) 假阳性裁定」:
  4. // 取「不动 model,改对账归一」——model 保持 int8/bool 原样,由对账侧把
  5. //
  6. // `tinyint(1)`(information_schema 返回的 bool 物理形态,MySQL 8.0.19+ 可能省略显示宽度)
  7. // 与 DDL 侧书写的 `TINYINT(1)` / `TINYINT`
  8. //
  9. // 归一到同一签名后再比对,避免"类型其实一致、只因显示宽度写法不同而误报"。
  10. //
  11. // 为什么放共享包(而不是 academic 包内私有函数):
  12. //
  13. // org 域的 `t_campus.self_support` / `verified` / `login_allowed` 是同一类 bit(1)→tinyint 迁移列,
  14. // 将来做真库核对必须复用**同一口径**,否则两域对"类型是否一致"的判定会漂移。
  15. //
  16. // 归一规则的顺序敏感性(误用即引入漏检,评审重点):
  17. // 1. ToUpper + TrimSpace + 空白折叠("tinyint ( 1 )" 不会出现,但多空格出现过);
  18. // 2. **仅整数族**(TINYINT/SMALLINT/MEDIUMINT/INT/INTEGER/BIGINT)剥离 "(N)" 显示宽度;
  19. // INTEGER 归一为 INT(同义词);
  20. // 3. BOOL/BOOLEAN 归一为 TINYINT(MySQL 里就是 tinyint(1) 的别名);
  21. // 4. **其余类型一律保留参数**:decimal(12,2) / varchar(N) / datetime(3) / double(p,s)
  22. // 的参数是精度语义,归一掉就再也发现不了 decimal(12,3) 这类真缺陷。
  23. package dbtest
  24. import (
  25. "strings"
  26. "unicode"
  27. )
  28. // integerFamily 可安全剥离显示宽度的整数族(MySQL 中 "(N)" 只影响显示,不影响存储与语义)。
  29. //
  30. // 注意:**不包含** DECIMAL/NUMERIC/FLOAT/DOUBLE(那里的括号是精度)、也不包含
  31. // VARCHAR/CHAR/BINARY(那里的括号是长度)——它们的参数必须参与比对。
  32. var integerFamily = map[string]struct{}{
  33. "TINYINT": {},
  34. "SMALLINT": {},
  35. "MEDIUMINT": {},
  36. "INT": {},
  37. "INTEGER": {},
  38. "BIGINT": {},
  39. }
  40. // NormalizeColumnType 把 DDL/信息模式里的列类型归一为可比较签名。
  41. //
  42. // 入参:
  43. // - raw string:原始列类型(可含大小写/空白差异,如 "tinyint(1)"、"TINYINT"、"boolean"、
  44. // "decimal(12,2)"、"varchar(26)"、"datetime(3)"、"bigint unsigned")。
  45. //
  46. // 出参:
  47. // - string:归一签名(大写)。空入参 → 空串(由调用方判为不合法,不在此处造错误)。
  48. //
  49. // 归一样例:
  50. //
  51. // "tinyint(1)" → "TINYINT" "TINYINT" → "TINYINT"
  52. // "boolean" → "TINYINT" "bool" → "TINYINT"
  53. // "int(11)" → "INT" "integer" → "INT"
  54. // "bigint unsigned" → "BIGINT UNSIGNED"(属性保留,只有显示宽度被剥离)
  55. // "decimal(12,2)" → "DECIMAL(12,2)"(**精度保留**)
  56. // "varchar(26)" → "VARCHAR(26)"(**长度保留**)
  57. // "datetime(3)" → "DATETIME(3)"(**小数秒精度保留**)
  58. func NormalizeColumnType(raw string) string {
  59. // ① 去空白:information_schema 不会插空格,但手工/日志粘贴的字符串常带
  60. // " tinyint ( 1 ) " 这种形态,折叠成紧凑形态后再解析,避免无意义的误报。
  61. compact := make([]rune, 0, len(raw))
  62. for _, r := range strings.ToUpper(raw) {
  63. if unicode.IsSpace(r) {
  64. continue
  65. }
  66. compact = append(compact, r)
  67. }
  68. s := string(compact)
  69. if s == "" {
  70. return ""
  71. }
  72. // ② 剥离尾部存储属性(UNSIGNED / ZEROFILL 是存储语义,**不**归一掉,只重排到最后)
  73. attrs := make([]string, 0, 2)
  74. for {
  75. switch {
  76. case strings.HasSuffix(s, "UNSIGNED"):
  77. attrs = append([]string{"UNSIGNED"}, attrs...)
  78. s = strings.TrimSuffix(s, "UNSIGNED")
  79. continue
  80. case strings.HasSuffix(s, "ZEROFILL"):
  81. attrs = append([]string{"ZEROFILL"}, attrs...)
  82. s = strings.TrimSuffix(s, "ZEROFILL")
  83. continue
  84. }
  85. break
  86. }
  87. name := s
  88. if idx := strings.IndexByte(s, '('); idx >= 0 {
  89. name = s[:idx] // 用于判别族,参数仍保留在 base 中
  90. }
  91. base := s
  92. if _, ok := integerFamily[name]; ok {
  93. if name == "INTEGER" {
  94. name = "INT" // 同义词收敛
  95. }
  96. base = name // 整数族:剥离 "(N)" 显示宽度
  97. } else if name == "BOOL" || name == "BOOLEAN" {
  98. base = "TINYINT" // MySQL 把 bool/boolean 落成 tinyint(1)
  99. }
  100. // 非整数族:base 保持原样(decimal(12,2)/varchar(26)/datetime(3) 的参数是精度/长度语义)
  101. if len(attrs) > 0 {
  102. base += " " + strings.Join(attrs, " ")
  103. }
  104. return base
  105. }
  106. // SameColumnType 判定两个列类型是否等价(两侧各归一后逐字相等)。
  107. //
  108. // 入参:a/b string——来自 DDL 与 information_schema 的列类型字符串(顺序无关)。
  109. // 出参:true 表示等价。
  110. func SameColumnType(a, b string) bool {
  111. return NormalizeColumnType(a) == NormalizeColumnType(b)
  112. }