blob_orm_tx_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. package DB
  2. import (
  3. "bytes"
  4. "path/filepath"
  5. "testing"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core"
  7. )
  8. // TestBlobColumnORMTxRollback 显式事务内 ORM Insert 写 blob, 回滚后:
  9. // 1. 行确实没了 (FindByPK 报 not found);
  10. // 2. blob 链页整链归还空闲链 (Insert 注释承诺: 用户已开事务时链页经 NewPage
  11. // 登记进该事务, Rollback 整链回收 —— blob_stream demo 在 core 层验过,
  12. // ORM 路径必须同样成立, 否则回滚一次就泄漏一批链页)。
  13. func TestBlobColumnORMTxRollback(t *testing.T) {
  14. dbPath := filepath.Join(t.TempDir(), "blob_tx_rb.db")
  15. db, err := Open(dbPath)
  16. if err != nil {
  17. t.Fatalf("打开: %v", err)
  18. }
  19. defer func() { _ = db.Close() }()
  20. db.SetAutoCommitDelay(0)
  21. tbl, err := UseTable[blobOrmRow](db, "blobtx")
  22. if err != nil {
  23. t.Fatalf("建表: %v", err)
  24. }
  25. freeBefore := db.CoreDB().DebugFreePageCount()
  26. tx := db.BeginTransaction()
  27. big := make([]byte, core.MaxBlobSize*3+9) // 跨 3+ 个链页, 回滚归还才有可观测量
  28. for i := range big {
  29. big[i] = byte(i % 199)
  30. }
  31. obj := &blobOrmRow{Name: "tx-rb", Body: big}
  32. if err := tbl.Insert(obj); err != nil {
  33. t.Fatalf("事务内 Insert: %v", err)
  34. }
  35. if err := tx.Rollback(); err != nil {
  36. t.Fatalf("Rollback: %v", err)
  37. }
  38. // 行已不存在。
  39. if _, err := tbl.Find(obj.ID); err == nil {
  40. t.Fatal("回滚后行仍可读出")
  41. }
  42. // 链页归还: 空闲页数不少于回滚前 (归还的链页挂回空闲链)。
  43. freeAfter := db.CoreDB().DebugFreePageCount()
  44. if freeAfter < freeBefore {
  45. t.Fatalf("回滚后空闲页 %d < 回滚前 %d: blob 链页未归还", freeAfter, freeBefore)
  46. }
  47. t.Logf("空闲页: 回滚前=%d 回滚后=%d (归还 %d 页)", freeBefore, freeAfter, freeAfter-freeBefore)
  48. }
  49. // TestBlobColumnORMNullAndDelete
  50. // - NULL/nil/空 blob: nil 切片按"空 blob"语义 (见 convert.go: 只认指针/接口,
  51. // nil 切片是空值不是 NULL) 存空 ref, 读回必须是空内容而不是报错;
  52. // - Delete 行后, 该行 blob 链页必须归还空闲链 —— 只删行不还链是慢性泄漏,
  53. // 大对象场景下几轮删插就把文件撑爆。
  54. func TestBlobColumnORMNullAndDelete(t *testing.T) {
  55. dbPath := filepath.Join(t.TempDir(), "blob_null_del.db")
  56. db, err := Open(dbPath)
  57. if err != nil {
  58. t.Fatalf("打开: %v", err)
  59. }
  60. defer func() { _ = db.Close() }()
  61. db.SetAutoCommitDelay(0)
  62. tbl, err := UseTable[blobOrmRow](db, "blobnd")
  63. if err != nil {
  64. t.Fatalf("建表: %v", err)
  65. }
  66. // nil / 空 blob 正常往返。
  67. nul := &blobOrmRow{Name: "nil-body", Body: nil}
  68. empty := &blobOrmRow{Name: "empty-body", Body: []byte{}}
  69. for _, o := range []*blobOrmRow{nul, empty} {
  70. if err := tbl.Insert(o); err != nil {
  71. t.Fatalf("Insert(%s): %v", o.Name, err)
  72. }
  73. got, err := tbl.Find(o.ID)
  74. if err != nil {
  75. t.Fatalf("Find(%s): %v", o.Name, err)
  76. }
  77. if len(got.Body) != 0 {
  78. t.Fatalf("%s: 读回 %d 字节, 期望空", o.Name, len(got.Body))
  79. }
  80. }
  81. if err := db.Commit(); err != nil {
  82. t.Fatalf("提交: %v", err)
  83. }
  84. // Delete 后链页归还: 先插一条大 blob, 记空闲页数, 删行提交, 空闲页应回升。
  85. big := make([]byte, core.MaxBlobSize*2+11)
  86. for i := range big {
  87. big[i] = byte(i % 253)
  88. }
  89. obj := &blobOrmRow{Name: "to-delete", Body: big}
  90. if err := tbl.Insert(obj); err != nil {
  91. t.Fatalf("Insert: %v", err)
  92. }
  93. if err := db.Commit(); err != nil {
  94. t.Fatalf("提交: %v", err)
  95. }
  96. freeBefore := db.CoreDB().DebugFreePageCount()
  97. if err := tbl.Delete(obj.ID); err != nil {
  98. t.Fatalf("Delete: %v", err)
  99. }
  100. if err := db.Commit(); err != nil {
  101. t.Fatalf("提交: %v", err)
  102. }
  103. if _, err := tbl.Find(obj.ID); err == nil {
  104. t.Fatal("删除后行仍可读出")
  105. }
  106. freeAfter := db.CoreDB().DebugFreePageCount()
  107. if freeAfter <= freeBefore {
  108. t.Fatalf("删除带大 blob 的行后空闲页 %d 未回升 (删除前 %d): blob 链页泄漏",
  109. freeAfter, freeBefore)
  110. }
  111. t.Logf("空闲页: 删前=%d 删后=%d (归还 %d 页)", freeBefore, freeAfter, freeAfter-freeBefore)
  112. // 其余行不受牵连。
  113. got, err := tbl.Find(nul.ID)
  114. if err != nil {
  115. t.Fatalf("删除牵连 Find(nil-body): %v", err)
  116. }
  117. if len(got.Body) != 0 || got.Name != "nil-body" {
  118. t.Fatalf("删除牵连邻行: %+v", got)
  119. }
  120. // 重开后内容依旧 (链上字节没被误归还污染)。
  121. if err := db.Close(); err != nil {
  122. t.Fatalf("关库: %v", err)
  123. }
  124. db2, err := Open(dbPath)
  125. if err != nil {
  126. t.Fatalf("重开: %v", err)
  127. }
  128. defer func() { _ = db2.Close() }()
  129. db2.SetAutoCommitDelay(0)
  130. tbl2, err := UseTable[blobOrmRow](db2, "blobnd")
  131. if err != nil {
  132. t.Fatalf("重开建表: %v", err)
  133. }
  134. got2, err := tbl2.Find(empty.ID)
  135. if err != nil {
  136. t.Fatalf("重开 Find: %v", err)
  137. }
  138. if !bytes.Equal(got2.Body, []byte{}) || got2.Name != "empty-body" {
  139. t.Fatalf("重开后数据异常: %+v", got2)
  140. }
  141. }