widerow_test.go 8.6 KB

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