overflow_repro_db_test.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package core
  2. import (
  3. "fmt"
  4. "testing"
  5. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  7. )
  8. // ============================================================================
  9. // DB 层 "节点满员先写再裂" 越界回归 (生产 panic: DockerManager 持续插入
  10. // 宽二级索引时 sbtEntry 越界 ← sbtPromote 右移循环)。
  11. //
  12. // 触发链: 批量删除会走"软删 + 全索引重建" (Deleter.rebuildIndexes), 二级索引由
  13. // SBTBuildFromSorted 批量构建 —— 内部节点被装到物理满员 (128B 键 iel=140,
  14. // 容量 (16384-32)/140 = 116)。之后任何一次让叶分裂的插入, 提升项都会打进这个
  15. // 满员内部节点: 修复前右移循环越过 sbtEntry 切片上界 panic; 修复后 pre-split
  16. // 先裂内部节点再插。本测试复刻该几何, 修复前红 (panic)、修复后绿。
  17. // ============================================================================
  18. // wideKey 生成 128 字节定宽、字节序 == 数值序的字符串键 (零填充十进制),
  19. // 落在 string(128) 列的档位宽度内, SBT 键 = 定长 128B 存储字节。
  20. func wideKey(i uint64) string { return fmt.Sprintf("%0128d", i) }
  21. // wideTable id(uint64 主键) + s(string(128)) + 主键索引 + s 上的 SBT 二级索引。
  22. func wideTable(t *testing.T, db *DB, name string) *Table {
  23. t.Helper()
  24. tbl, err := db.CreateTable(name, []types.FieldInfo{
  25. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  26. {FieldType: types.FieldType_String, FieldName: []byte("s"), Flag: types.WidthCodeFor(128)},
  27. })
  28. if err != nil {
  29. t.Fatalf("建表: %v", err)
  30. }
  31. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  32. t.Fatalf("建主键索引: %v", err)
  33. }
  34. if _, err := tbl.CreateIndex("idx_s", page.IndexTypeBTree, []byte{1}); err != nil {
  35. t.Fatalf("建二级索引: %v", err)
  36. }
  37. return tbl
  38. }
  39. func countScan(t *testing.T, tbl *Table) int64 {
  40. t.Helper()
  41. var n int64
  42. if err := tbl.Query().Scan(func(row []types.FieldValue) bool { n++; return true }); err != nil {
  43. t.Fatalf("全表扫描: %v", err)
  44. }
  45. return n
  46. }
  47. // TestWideKeySBTAfterMassDeleteRebuild 生产形态复刻:
  48. // 批量灌 13921 行 → 批量删 1 行 (全索引重建, SBT 根内部节点恰好物理满员 116 条)
  49. // → 持续单行插入 → 修复前在第一个分裂的叶处 panic。
  50. func TestWideKeySBTAfterMassDeleteRebuild(t *testing.T) {
  51. db := fixOpenDB(t)
  52. tbl := wideTable(t, db, "t")
  53. // 1) 批量灌 13921 行, s 单调递增
  54. const total = 13921
  55. pkIDs := make([]uint64, total)
  56. vals := make([][]types.FieldValue, total)
  57. for i := 0; i < total; i++ {
  58. id := uint64(i + 1)
  59. pkIDs[i] = id
  60. vals[i] = []types.FieldValue{
  61. {FieldName: []byte("id"), Value: id},
  62. {FieldName: []byte("s"), Value: wideKey(id)},
  63. }
  64. }
  65. if _, err := tbl.InsertRowsWithIDs(pkIDs, vals); err != nil {
  66. t.Fatalf("批量插入: %v", err)
  67. }
  68. if err := db.Commit(); err != nil {
  69. t.Fatal(err)
  70. }
  71. // 2) 批量删 1 行 → 全索引重建: 存活 13920 = 116×120 条 → 116 张满叶 →
  72. // SBT 根内部节点恰好 116 条 = 物理满员 (iel=140 容量 116)
  73. if _, err := tbl.Delete().Where(Eq("id", uint64(1), types.FieldType(0))).Exec(); err != nil {
  74. t.Fatalf("批量删除: %v", err)
  75. }
  76. if err := db.Commit(); err != nil {
  77. t.Fatal(err)
  78. }
  79. // 3) 持续单行插入 (生产形态): 每张满叶的分裂都把提升项打进满员内部节点。
  80. // 修复前: sbtPromote 右移循环越界 panic。
  81. const more = 1000
  82. for i := uint64(total + 1); i <= total+more; i++ {
  83. if _, err := tbl.InsertRowWithID(i, []types.FieldValue{
  84. {FieldName: []byte("id"), Value: i},
  85. {FieldName: []byte("s"), Value: wideKey(i)},
  86. }); err != nil {
  87. t.Fatalf("插入 %d: %v", i, err)
  88. }
  89. }
  90. if err := db.Commit(); err != nil {
  91. t.Fatal(err)
  92. }
  93. // 4) 校验: 行数 / 全扫 / 主键回读 / 二级索引等值查
  94. const want = int64(total - 1 + more)
  95. if got := int64(tbl.TotalRows()); got != want {
  96. t.Fatalf("TotalRows=%d, 想要 %d", got, want)
  97. }
  98. if got := countScan(t, tbl); got != want {
  99. t.Fatalf("全表扫描 %d 行, 想要 %d", got, want)
  100. }
  101. if _, err := tbl.FindByPK(1); err == nil {
  102. t.Fatal("id=1 已删除却仍可读")
  103. }
  104. if _, err := tbl.FindByPK(total + 777); err != nil {
  105. t.Fatalf("id=%d 读不到: %v", total+777, err)
  106. }
  107. // 二级索引等值查: 单调键, 每个 s 值恰一行
  108. hit := 0
  109. if err := tbl.Query().Where(Eq("s", wideKey(uint64(total+500)), types.FieldType(0))).
  110. Scan(func(row []types.FieldValue) bool { hit++; return true }); err != nil {
  111. t.Fatalf("二级索引等值查: %v", err)
  112. }
  113. if hit != 1 {
  114. t.Fatalf("s 等值查应命中 1 行, 实际 %d", hit)
  115. }
  116. }
  117. // TestBatchInsertMonotonicAndRangeScan 综合用例: 单调递增键 + 二级索引 +
  118. // 持续批量 (InsertRows 2000 行 × 10 批), 之后范围扫描 / 等值查 / 全扫校验行数。
  119. func TestBatchInsertMonotonicAndRangeScan(t *testing.T) {
  120. db := fixOpenDB(t)
  121. tbl := wideTable(t, db, "t2")
  122. // 10 批 × 2000 行, 主键自增 (pkID=0 由引擎分配), s 与 id 同步单调
  123. const batches = 10
  124. const per = 2000
  125. for b := 0; b < batches; b++ {
  126. pkIDs := make([]uint64, per)
  127. vals := make([][]types.FieldValue, per)
  128. for i := 0; i < per; i++ {
  129. id := uint64(b*per + i + 1) // 仅作 s 的取值来源, 主键走自增
  130. pkIDs[i] = 0
  131. vals[i] = []types.FieldValue{
  132. {FieldName: []byte("id"), Value: id},
  133. {FieldName: []byte("s"), Value: wideKey(id)},
  134. }
  135. }
  136. if _, err := tbl.InsertRowsWithIDs(pkIDs, vals); err != nil {
  137. t.Fatalf("第 %d 批插入: %v", b, err)
  138. }
  139. }
  140. if err := db.Commit(); err != nil {
  141. t.Fatal(err)
  142. }
  143. const want = int64(batches * per)
  144. if got := int64(tbl.TotalRows()); got != want {
  145. t.Fatalf("TotalRows=%d, 想要 %d", got, want)
  146. }
  147. if got := countScan(t, tbl); got != want {
  148. t.Fatalf("全表扫描 %d 行, 想要 %d", got, want)
  149. }
  150. // 范围扫描: id ∈ [5001, 15001) → 10000 行 (走主键索引 RangeScan)
  151. rng := int64(0)
  152. if err := tbl.Query().Where(And(
  153. Ge("id", uint64(5001), types.FieldType(0)),
  154. Lt("id", uint64(15001), types.FieldType(0)),
  155. )).Scan(func(row []types.FieldValue) bool { rng++; return true }); err != nil {
  156. t.Fatalf("范围扫描: %v", err)
  157. }
  158. if rng != 10000 {
  159. t.Fatalf("范围扫描 %d 行, 想要 10000", rng)
  160. }
  161. // 二级索引等值 + 边界行回读
  162. hit := 0
  163. if err := tbl.Query().Where(Eq("s", wideKey(12345), types.FieldType(0))).
  164. Scan(func(row []types.FieldValue) bool { hit++; return true }); err != nil {
  165. t.Fatalf("二级索引等值查: %v", err)
  166. }
  167. if hit != 1 {
  168. t.Fatalf("s=wideKey(12345) 应命中 1 行, 实际 %d", hit)
  169. }
  170. for _, id := range []uint64{1, 5000, 5001, 15000, uint64(want)} {
  171. if _, err := tbl.FindByPK(id); err != nil {
  172. t.Fatalf("id=%d 读不到: %v", id, err)
  173. }
  174. }
  175. }