widerow_test.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. package migrate
  2. import (
  3. "TsDB2/DB/core/page"
  4. "TsDB2/DB/core/types"
  5. "bytes"
  6. "fmt"
  7. "path/filepath"
  8. "strings"
  9. "testing"
  10. )
  11. // wideColumn 造一个"源库声明了 n 字节容量"的字符串列。
  12. //
  13. // 走 bucketColumn (读端把 SQL 长度收编成列的那条路径) 而不是手写类型 + 宽度码:
  14. // 测试要验的正是"读端读出来的列, 到行宽闸门这里怎么处理", 手写一份等于把被测的映射
  15. // 又抄一遍 —— 映射改了测试不会跟着变。
  16. func wideColumn(name string, n int) Column {
  17. ft, code := bucketColumn(n)
  18. return Column{Name: name, Type: ft, Param: code}
  19. }
  20. // wideSchema 造一张行宽超限的表, 与线上撞过闸门的那张同构: 几列 2048 / 1024 顶到上限。
  21. //
  22. // 末两列是边界样本: resAlias(224) 与 resName(256) 都在 256 这条线以内, 必须留在定长档 ——
  23. // 一起降掉就等于把"可索引、可定长比较"的短列白白拖进堆。
  24. func wideSchema() *TableSchema {
  25. cols := []Column{{Name: "_id", Type: types.FieldType_8byte}}
  26. for _, c := range []struct {
  27. name string
  28. size int
  29. }{
  30. {"resOnlinePatch", 2048},
  31. {"resChatCrowd", 2048},
  32. {"resExtendParam1", 1024},
  33. {"resExtendParam2", 1024},
  34. {"resExtendParam3", 1024},
  35. {"resExtendParam4", 1024},
  36. {"resExtendParam5", 1024},
  37. {"resExtendParam6", 1024},
  38. {"resExtendParam7", 1024},
  39. {"resAppID", 320}, // 320 > 256: 降
  40. {"resAlias", 224}, // <= 256: 不降
  41. {"resName", 256}, // 边界: 也不降
  42. } {
  43. cols = append(cols, wideColumn(c.name, c.size))
  44. }
  45. return &TableSchema{Name: "mesh_resource_info", Columns: cols, PKColumn: "_id"}
  46. }
  47. func TestFitRowWidthDemotesWideColumns(t *testing.T) {
  48. sc := wideSchema()
  49. if err := types.CheckRowSize(vrtxFields(sc)); err == nil {
  50. t.Fatal("样本表本来就该超限, 否则这个用例什么都没验")
  51. }
  52. n, err := fitRowWidthForVRTX(sc)
  53. if err != nil {
  54. t.Fatalf("可降级的表不该报错: %v", err)
  55. }
  56. if want := 10; n != want {
  57. t.Errorf("降级列数 = %d, 期望 %d (宽度 > 256 的那些)", n, want)
  58. }
  59. if err := types.CheckRowSize(vrtxFields(sc)); err != nil {
  60. t.Fatalf("降级后行宽应当装得下: %v", err)
  61. }
  62. byName := make(map[string]Column, len(sc.Columns))
  63. for _, c := range sc.Columns {
  64. byName[c.Name] = c
  65. }
  66. // 宽列: 变成动态列, 且宽度码必须清零 —— 留着原宽度码 FieldSize 仍按定长算, 行宽又变回去。
  67. for _, name := range []string{"resOnlinePatch", "resChatCrowd", "resExtendParam1", "resAppID"} {
  68. c, ok := byName[name]
  69. if !ok {
  70. t.Fatalf("列 %s 丢了", name)
  71. }
  72. if !c.IsDynamic() {
  73. t.Errorf("列 %s 应当降为动态列, 实际 %v", name, c.Type)
  74. }
  75. if c.Param != 0 {
  76. t.Errorf("列 %s 的宽度码应当清零, 实际 %d", name, c.Param)
  77. }
  78. if c.Size() != types.DynamicRefSize {
  79. t.Errorf("列 %s 的行内宽度 = %d, 期望堆引用 %d", name, c.Size(), types.DynamicRefSize)
  80. }
  81. }
  82. // 窄列与行主键: 一个都不许动。
  83. for name, want := range map[string]int{"resAlias": 224, "resName": 256, "_id": 8} {
  84. c := byName[name]
  85. if c.IsDynamic() {
  86. t.Errorf("列 %s (行内 %d 字节) 不该降为动态列", name, c.Size())
  87. }
  88. if c.Size() != want {
  89. t.Errorf("列 %s 的行内宽度 = %d, 期望 %d", name, c.Size(), want)
  90. }
  91. }
  92. // 降级是改类型, 必须报出来 (runTable 靠它汇进迁移报告)。
  93. if len(sc.DegradedColumns) != n {
  94. t.Fatalf("DegradedColumns = %d 条, 期望 %d 条", len(sc.DegradedColumns), n)
  95. }
  96. for _, note := range sc.DegradedColumns {
  97. if !strings.Contains(note, "动态列") || !strings.Contains(note, "索引键") {
  98. t.Errorf("降级说明没讲清后果: %q", note)
  99. }
  100. }
  101. if !strings.Contains(strings.Join(sc.DegradedColumns, "|"), "resChatCrowd") {
  102. t.Errorf("降级说明里应当点名具体列: %v", sc.DegradedColumns)
  103. }
  104. }
  105. // 没有可降的列 (全是 <= 256 的定长列) 时, 只能按引擎的口径拒绝 —— 降无可降, 硬装就是骗人。
  106. func TestFitRowWidthRejectsWhenNothingToDemote(t *testing.T) {
  107. sc := &TableSchema{Name: "flat_wide", Columns: []Column{{Name: "_id", Type: types.FieldType_8byte}}}
  108. for i := 0; i < 40; i++ {
  109. sc.Columns = append(sc.Columns, wideColumn(fmt.Sprintf("c%02d", i), 256))
  110. }
  111. sc.PKColumn = "_id"
  112. n, err := fitRowWidthForVRTX(sc)
  113. if err == nil {
  114. t.Fatal("降无可降时必须报错, 否则会建出一张每行都写不进去的表")
  115. }
  116. if n != 0 {
  117. t.Errorf("降级列数 = %d, 期望 0 (这些列都在 256 以内)", n)
  118. }
  119. if !strings.Contains(err.Error(), "行宽") {
  120. t.Errorf("报错应当保留引擎的口径 (行宽/上限/各列宽度): %v", err)
  121. }
  122. if len(sc.DegradedColumns) != 0 {
  123. t.Errorf("没降就不该有降级说明: %v", sc.DegradedColumns)
  124. }
  125. }
  126. // 幂等: 同一个 schema 跑第二遍不能再降一次 (否则 DegradedColumns 会重复记账)。
  127. func TestFitRowWidthIsIdempotent(t *testing.T) {
  128. sc := wideSchema()
  129. if _, err := fitRowWidthForVRTX(sc); err != nil {
  130. t.Fatalf("第一次降级: %v", err)
  131. }
  132. notes := len(sc.DegradedColumns)
  133. n, err := fitRowWidthForVRTX(sc)
  134. if err != nil {
  135. t.Fatalf("第二次不该报错: %v", err)
  136. }
  137. if n != 0 {
  138. t.Errorf("第二次降级列数 = %d, 期望 0 (已经装得下了)", n)
  139. }
  140. if len(sc.DegradedColumns) != notes {
  141. t.Errorf("降级说明重复记账: %d != %d", len(sc.DegradedColumns), notes)
  142. }
  143. }
  144. // 端到端: 降级后的 schema 必须真的能在 VRTX 里立起来并接受大值 ——
  145. // 降级前这张表连 CreateTable 都过不去, 这正是要解决的问题。
  146. func TestDemotedSchemaBuildsAndAcceptsWideValues(t *testing.T) {
  147. sc := wideSchema()
  148. if _, err := fitRowWidthForVRTX(sc); err != nil {
  149. t.Fatalf("行宽闸门: %v", err)
  150. }
  151. // 被降级的列上原有的索引: 动态列不能做索引键, 该索引应当被跳过并报进 SkippedIndexes,
  152. // 而不是让整张表失败 —— 索引丢了能补, 数据没迁才是真丢。
  153. sc.Indexes = append(sc.Indexes, IndexSchema{
  154. Name: "idx_resChatCrowd", Type: page.IndexTypeBTree, Columns: []string{"resChatCrowd"},
  155. })
  156. v, err := OpenVRTX(filepath.Join(t.TempDir(), "wide.db"), quiet())
  157. if err != nil {
  158. t.Fatalf("开库: %v", err)
  159. }
  160. defer v.Close()
  161. if err := v.CreateTable(sc, true); err != nil {
  162. t.Fatalf("降级后建表: %v", err)
  163. }
  164. if len(sc.SkippedIndexes) == 0 {
  165. t.Error("降级列上的索引应当被跳过并记进 SkippedIndexes")
  166. }
  167. tbl, ok := v.DB().FindTable("mesh_resource_info")
  168. if !ok {
  169. t.Fatal("建完表却找不到")
  170. }
  171. // 2000 字节 (超过 2048 档装不下、但动态列装得下) 的值必须原样进得去。
  172. big := bytes.Repeat([]byte{'x'}, 2000)
  173. vals := make([]types.FieldValue, 0, len(sc.Columns))
  174. for _, c := range sc.Columns {
  175. var val any
  176. switch c.Name {
  177. case "_id":
  178. val = uint64(1)
  179. case "resChatCrowd":
  180. val = big
  181. case "resAlias", "resName":
  182. val = []byte(c.Name)
  183. default:
  184. val = []byte(nil)
  185. }
  186. vals = append(vals, types.FieldValue{FieldName: []byte(c.Name), Value: val})
  187. }
  188. if _, err := tbl.InsertRowWithID(1, vals); err != nil {
  189. t.Fatalf("往降级后的列写 2000 字节: %v", err)
  190. }
  191. got, err := tbl.FindByPK(1)
  192. if err != nil {
  193. t.Fatalf("回读: %v", err)
  194. }
  195. for _, fv := range got {
  196. if string(fv.FieldName) != "resChatCrowd" {
  197. continue
  198. }
  199. raw, ok := fv.Value.([]byte)
  200. if !ok {
  201. t.Fatalf("动态列回读值类型 = %T, 期望 []byte", fv.Value)
  202. }
  203. if !bytes.Equal(raw, big) {
  204. t.Errorf("动态列回读 %d 字节, 与写入的 %d 字节不一致", len(raw), len(big))
  205. }
  206. return
  207. }
  208. t.Error("回读结果里没有 resChatCrowd")
  209. }