txn_ddl_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. package core
  2. // 事务与 DDL 交互的回归测试。
  3. //
  4. // 背景 (txn_crash_chaos 压测实锤): 建表/建索引后首个事务回滚, 二次插入立刻
  5. // "load failed" —— DDL 未提交时, Rollback 按页丢弃缓存, 而 Table.info /
  6. // IndexMeta.metas 是独立于页缓存的 Go 堆副本, 仍指向事务中分配、已被回收
  7. // 的 B+ 树根页; ReloadInfos 读表页失败 (文件还没长到那里) 又静默跳过,
  8. // 内存元数据与磁盘真值永久分叉。
  9. // 修复: DDL 隐式提交 (无事务时) + 事务内 DDL 显式拒绝。
  10. import (
  11. "strings"
  12. "testing"
  13. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  14. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  15. )
  16. func txnDDLTable(t *testing.T, db *DB) *Table {
  17. t.Helper()
  18. tbl, err := db.CreateTable("t", []types.FieldInfo{
  19. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  20. {FieldType: types.FieldType_String, Flag: types.WidthCodeFor(32), FieldName: []byte("v")},
  21. })
  22. if err != nil {
  23. t.Fatalf("建表: %v", err)
  24. }
  25. if _, err := tbl.CreateIndex("pk", page.IndexTypePrimary, []byte{0}); err != nil {
  26. t.Fatalf("建索引: %v", err)
  27. }
  28. return tbl
  29. }
  30. // TestRollbackAfterDDLKeepsIndexUsable 回归: DDL (建表+建索引) 后首个事务回滚,
  31. // 索引元数据必须恢复到已提交的初始状态 (EntryPage=0), 后续插入重建树根, 不迷路。
  32. func TestRollbackAfterDDLKeepsIndexUsable(t *testing.T) {
  33. db := fixOpenDB(t)
  34. tbl := txnDDLTable(t, db)
  35. // 首个事务写入后回滚
  36. tx := db.BeginTransaction()
  37. for i := uint64(1); i <= 5; i++ {
  38. if _, err := tbl.InsertRowWithID(i, []types.FieldValue{
  39. {FieldName: []byte("id"), Value: i},
  40. {FieldName: []byte("v"), Value: "x"},
  41. }); err != nil {
  42. t.Fatalf("insert %d: %v", i, err)
  43. }
  44. }
  45. if err := tx.Rollback(); err != nil {
  46. t.Fatalf("rollback: %v", err)
  47. }
  48. // 回滚后索引元数据必须已是 committed 初值, 插入能正常重建
  49. meta, _, ok := tbl.indexMeta.rowStoreMeta()
  50. if !ok {
  51. t.Fatal("rowstore 丢失")
  52. }
  53. if meta.EntryPage != 0 {
  54. t.Fatalf("回滚后 EntryPage=%d, 想要 0 (指向已回收页 = 元数据分叉)", meta.EntryPage)
  55. }
  56. tx2 := db.BeginTransaction()
  57. if _, err := tbl.InsertRowWithID(1, []types.FieldValue{
  58. {FieldName: []byte("id"), Value: uint64(1)},
  59. {FieldName: []byte("v"), Value: "y"},
  60. }); err != nil {
  61. t.Fatalf("回滚后二次插入: %v", err)
  62. }
  63. if err := tx2.Commit(); err != nil {
  64. t.Fatalf("commit: %v", err)
  65. }
  66. if _, err := tbl.FindByPK(1); err != nil {
  67. t.Fatalf("提交后按主键查询: %v", err)
  68. }
  69. }
  70. // TestDDLInsideTransactionRejected 事务内 DDL 必须显式拒绝: 未提交的 DDL 随
  71. // 回滚消失后无法恢复内存元数据, 宁可拒绝也不让调用方踩进不可恢复的坑。
  72. func TestDDLInsideTransactionRejected(t *testing.T) {
  73. db := fixOpenDB(t)
  74. tbl := txnDDLTable(t, db)
  75. tx := db.BeginTransaction()
  76. defer func() { _ = tx.Rollback() }()
  77. if _, err := db.CreateTable("t2", []types.FieldInfo{
  78. {FieldType: types.FieldType_Uint64, FieldName: []byte("id")},
  79. }); err == nil || !strings.Contains(err.Error(), "DDL") {
  80. t.Fatalf("事务内 CreateTable 必须报 DDL 错误, 实际: %v", err)
  81. }
  82. if _, err := tbl.CreateIndex("idx_v", page.IndexTypeBTree, []byte{1}); err == nil || !strings.Contains(err.Error(), "DDL") {
  83. t.Fatalf("事务内 CreateIndex 必须报 DDL 错误, 实际: %v", err)
  84. }
  85. if err := tbl.AddField(types.FieldInfo{FieldType: types.FieldType_Uint64, FieldName: []byte("w")}); err == nil || !strings.Contains(err.Error(), "DDL") {
  86. t.Fatalf("事务内 AddField 必须报 DDL 错误, 实际: %v", err)
  87. }
  88. if err := tbl.DropField("v"); err == nil || !strings.Contains(err.Error(), "DDL") {
  89. t.Fatalf("事务内 DropField 必须报 DDL 错误, 实际: %v", err)
  90. }
  91. if err := db.DropTable("t"); err == nil || !strings.Contains(err.Error(), "DDL") {
  92. t.Fatalf("事务内 DropTable 必须报 DDL 错误, 实际: %v", err)
  93. }
  94. }
  95. // TestDDLImplicitCommit DDL (无事务时) 必须隐式提交: 建表/建索引后立即对后续
  96. // 事务的回滚可见 —— 回滚只应撤销事务内的数据修改, 不应波及已提交的 DDL。
  97. func TestDDLImplicitCommit(t *testing.T) {
  98. db := fixOpenDB(t)
  99. tbl := txnDDLTable(t, db)
  100. // DDL 之后不开事务直接杀 (模拟): 隐式提交意味着表与索引此刻已在盘上。
  101. // 用新句柄重开验证 (不重开则缓存可能掩盖落盘问题)。
  102. path := db.path
  103. if err := db.Close(); err != nil {
  104. t.Fatalf("close: %v", err)
  105. }
  106. db2, err := Open(path)
  107. if err != nil {
  108. t.Fatalf("重开: %v", err)
  109. }
  110. defer db2.Close()
  111. tbl2, ok := db2.FindTable("t")
  112. if !ok {
  113. t.Fatal("重开后表丢失 —— DDL 未隐式提交")
  114. }
  115. meta, _, ok := tbl2.indexMeta.rowStoreMeta()
  116. if !ok || meta == nil {
  117. t.Fatal("重走后索引元数据丢失 —— CreateIndex 未隐式提交")
  118. }
  119. _ = tbl
  120. }