rowspill_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. package core
  2. // 本文件验证【值级溢出】: 行内容超 MaxInlineRowData 时, 未建索引的宽字符串列落到堆页,
  3. // 行内只留 6B 堆引用 —— "一页两行"的政策由此从"建表时按声明宽度卡死"变成"写入时按实际
  4. // 行宽动态保住"。要钉住的性质:
  5. //
  6. // 1. 写入: 超限的行不再整行拒绝, 而是真的被缩短到 MaxInlineRowData 以内 (行内出现引用)。
  7. // 2. 读回: 溢出的列解引用后逐字节一致; 同行未溢出的列不受影响。
  8. // 3. 逐行判定: 同一列的【不同行】可以一行内联、一行溢出, 过滤必须按行内 tag 走对分支
  9. // —— 这是最容易错的一处: 只看列类型就会把 6B 页号当成字符串内容去比, 症状是静默漏行。
  10. // 4. 更新: 溢出行改写后仍可读回 (旧堆块复用/释放)。
  11. // 5. 删除: 行与堆块一并消失。
  12. import (
  13. "fmt"
  14. "strings"
  15. "testing"
  16. tsindex "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  17. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  18. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  19. )
  20. func spillColName(i int) string { return fmt.Sprintf("S%d", i) }
  21. // spillNewTable 建一张"宽字符串列都可溢出"的表: 1 个 8B 主键列 + n 个 2048 宽字符串列。
  22. // 列 0 是定长主键 (恒内联, 偏移表基址依赖它), 其余字符串列都没建索引 ⇒ 都可溢出。
  23. func spillNewTable(t *testing.T, db *DB, name string, n int) *Table {
  24. t.Helper()
  25. fields := []types.FieldInfo{{FieldType: types.FieldType_Uint64, FieldName: []byte("id")}}
  26. for i := 0; i < n; i++ {
  27. fields = append(fields, types.FieldInfo{
  28. FieldType: types.FieldType_String,
  29. Flag: types.WidthCodeFor(types.MaxInlineColumnWidth),
  30. FieldName: []byte(spillColName(i)),
  31. })
  32. }
  33. tbl, err := db.CreateTable(name, fields)
  34. if err != nil {
  35. t.Fatalf("建表 %s: %v", name, err)
  36. }
  37. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  38. t.Fatalf("建主键索引: %v", err)
  39. }
  40. return tbl
  41. }
  42. // spillRawRow 直接按 pkID 取回【盘上原始 rowData】(去掉 8B pkID 前缀)。精确到字节, 用来断言
  43. // "行真的被缩短了", 而不是只看插入成不成功 —— 溢出若没生效, 行会变大但仍然插得进去。
  44. func spillRawRow(t *testing.T, tbl *Table, pk uint64) []byte {
  45. t.Helper()
  46. meta, handler, ok := tbl.IndexMeta().rowStoreMeta()
  47. if !ok {
  48. t.Fatal("表上没有行存储索引")
  49. }
  50. ph, ok := handler.(*tsindex.PrimaryHandler)
  51. if !ok {
  52. t.Fatalf("行存储 handler 不是 PrimaryHandler: %T", handler)
  53. }
  54. store, found := ph.SearchByPK(tbl.handle, meta.EntryPage, pk)
  55. if !found {
  56. t.Fatalf("pk=%d 读不到行", pk)
  57. }
  58. return store[8:]
  59. }
  60. func spillValues(pk uint64, vals []string) []types.FieldValue {
  61. fs := make([]types.FieldValue, 0, 1+len(vals))
  62. fs = append(fs, types.FieldValue{FieldName: []byte("id"), Value: pk})
  63. for i, v := range vals {
  64. fs = append(fs, types.FieldValue{FieldName: []byte(spillColName(i)), Value: v})
  65. }
  66. return fs
  67. }
  68. func TestValueSpillRoundTrip(t *testing.T) {
  69. db := fixOpenDB(t)
  70. const n = 5
  71. tbl := spillNewTable(t, db, "SPILL", n)
  72. // 每列 1800 字节 × 5 = 9000 > MaxInlineRowData(8159): 不溢出就装不下。
  73. big := make([]string, n)
  74. for i := range big {
  75. big[i] = strings.Repeat(string(rune('a'+i)), 1800)
  76. }
  77. pkBig := tbl.AllocPKID()
  78. if _, err := tbl.InsertRowWithID(pkBig, spillValues(pkBig, big)); err != nil {
  79. t.Fatalf("插入超限行: %v", err)
  80. }
  81. // 同一批列的第二行故意【不溢出】: 覆盖"同列不同行、内联与溢出并存"的路径。
  82. small := make([]string, n)
  83. for i := range small {
  84. small[i] = strings.Repeat(string(rune('A'+i)), 40)
  85. }
  86. pkSmall := tbl.AllocPKID()
  87. if _, err := tbl.InsertRowWithID(pkSmall, spillValues(pkSmall, small)); err != nil {
  88. t.Fatalf("插入小行: %v", err)
  89. }
  90. // 1. 大行真的被缩短到行内上限以内 (未溢出时长度 ≈ 9022)。
  91. raw := spillRawRow(t, tbl, pkBig)
  92. if len(raw) > types.MaxInlineRowData {
  93. t.Fatalf("行内长度 %d 超过上限 %d: 值级溢出没有生效",
  94. len(raw), types.MaxInlineRowData)
  95. }
  96. // 小行不该被无谓地溢出 (它本来就在限内)。
  97. if rawSmall := spillRawRow(t, tbl, pkSmall); len(rawSmall) > types.MaxInlineRowData {
  98. t.Fatalf("小行长度 %d 超过上限: 不该溢出却溢出了", len(rawSmall))
  99. }
  100. // 2. 两行都读回, 每列逐字节一致 (溢出列解引用 + 内联列并存的错位探针)。
  101. rows, err := tbl.Query().Find()
  102. if err != nil {
  103. t.Fatalf("find: %v", err)
  104. }
  105. if len(rows) != 2 {
  106. t.Fatalf("应读出 2 行, 得到 %d", len(rows))
  107. }
  108. byID := map[uint64][]types.FieldValue{}
  109. for _, r := range rows {
  110. id, ok := newTypeRowVal(t, r, "id").(uint64)
  111. if !ok {
  112. t.Fatalf("id 列不是 uint64: %T", newTypeRowVal(t, r, "id"))
  113. }
  114. byID[id] = r
  115. }
  116. for _, tc := range []struct {
  117. pk uint64
  118. want []string
  119. }{{pkBig, big}, {pkSmall, small}} {
  120. r, ok := byID[tc.pk]
  121. if !ok {
  122. t.Fatalf("pk=%d 没读回来", tc.pk)
  123. }
  124. for i, want := range tc.want {
  125. if got := stringTrim(newTypeRowVal(t, r, spillColName(i))); got != want {
  126. t.Errorf("pk=%d 列 %s 读回长度 %d, 期望 %d (溢出列解引用不一致)",
  127. tc.pk, spillColName(i), len(got), len(want))
  128. }
  129. }
  130. }
  131. // 3. 过滤: 对【未建索引】的列做等值过滤, 走的是行级过滤 (rowFieldForCompare) ——
  132. // 大行该列已溢出 (行内是 6B 引用), 小行该列内联, 两条路径都要给出正确答案。
  133. const probeCol = 2
  134. if got, err := tbl.Query().Where(Eq(spillColName(probeCol), big[probeCol], types.FieldType(0))).Count(); err != nil {
  135. t.Fatalf("按溢出列过滤: %v", err)
  136. } else if got != 1 {
  137. t.Errorf("溢出列等值过滤命中 %d 行, 期望 1 (把 6B 堆引用当成内容比了?)", got)
  138. }
  139. if got, err := tbl.Query().Where(Eq(spillColName(probeCol), small[probeCol], types.FieldType(0))).Count(); err != nil {
  140. t.Fatalf("按内联列过滤: %v", err)
  141. } else if got != 1 {
  142. t.Errorf("内联列等值过滤命中 %d 行, 期望 1", got)
  143. }
  144. // 前缀不该命中 (与内联时的语义一致: 按列宽补零比较)。
  145. if got, _ := tbl.Query().Where(Eq(spillColName(probeCol), big[probeCol][:8], types.FieldType(0))).Count(); got != 0 {
  146. t.Errorf("前缀命中 %d 行, 期望 0", got)
  147. }
  148. // LIKE 走的是另一条行级路径 (LikeCondition.Match), 同样要能解引用。
  149. if got, err := tbl.Query().Where(Like(spillColName(probeCol), big[probeCol][:8]+"%")).Count(); err != nil {
  150. t.Fatalf("LIKE: %v", err)
  151. } else if got != 1 {
  152. t.Errorf("LIKE 命中 %d 行, 期望 1", got)
  153. }
  154. // 4. 更新溢出行 (改短) 后仍可读回。
  155. newVals := make([]string, n)
  156. for i := range newVals {
  157. newVals[i] = strings.Repeat(string(rune('0'+i)), 1500)
  158. }
  159. if err := tbl.UpdateRowByPK(pkBig, spillValues(pkBig, newVals)); err != nil {
  160. t.Fatalf("更新溢出行: %v", err)
  161. }
  162. rows, err = tbl.Query().Find()
  163. if err != nil {
  164. t.Fatalf("find after update: %v", err)
  165. }
  166. for _, r := range rows {
  167. if id, _ := newTypeRowVal(t, r, "id").(uint64); id != pkBig {
  168. continue
  169. }
  170. for i, want := range newVals {
  171. if got := stringTrim(newTypeRowVal(t, r, spillColName(i))); got != want {
  172. t.Errorf("更新后列 %s 长度 %d, 期望 %d", spillColName(i), len(got), len(want))
  173. }
  174. }
  175. }
  176. // 5. 删除: 行与堆块一并消失。
  177. if err := tbl.DeleteRowByPK(pkBig); err != nil {
  178. t.Fatalf("删除: %v", err)
  179. }
  180. if got, _ := tbl.Query().Count(); got != 1 {
  181. t.Errorf("删除后应剩 1 行, 得到 %d", got)
  182. }
  183. }