package DB import ( "path/filepath" "testing" ) // TestExplicitTxnSurvivesObjDeleteAndBatchInsert 回归测试: 用户持有显式事务时, // 对象头部 Delete() (行号已知 → deleteByRowID) 与 Table.BatchInsert() 必须复用 // 该事务, 而不是顶掉它、在操作中途替用户全局提交 —— 否则用户随后的 Rollback // 失去原子性: // - 事务内 Delete 的行在 Rollback 后必须"复活"; // - 事务内 BatchInsert 的行在 Rollback 后必须全部消失。 // // 旧实现 (修复前) 在这两处无条件 db.BeginTransaction() + Commit: 用户事务被 // 顶掉且已全局提交, Rollback 回滚不到任何东西, 下面两个断言都会失败。 func TestExplicitTxnSurvivesObjDeleteAndBatchInsert(t *testing.T) { db, err := Open(filepath.Join(t.TempDir(), "txn_hijack.db")) if err != nil { t.Fatalf("打开: %v", err) } defer func() { _ = db.Close() }() db.SetAutoCommitDelay(0) tbl, err := UseTable[biRow](db, "txnhi") if err != nil { t.Fatalf("建表: %v", err) } // 预置 4 行并落盘 for i := 0; i < 4; i++ { if err := tbl.Insert(biMake(i)); err != nil { t.Fatalf("预置 %d: %v", i, err) } } if err := db.Commit(); err != nil { t.Fatalf("提交: %v", err) } // 用户开显式事务, 在事务内做 Delete + BatchInsert tx := db.BeginTransaction() // victim 经 Find 读出, 行号已知 → obj.Delete() 走 deleteByRowID 自动事务路径 victim, err := tbl.Find(2) if err != nil { t.Fatalf("Find(2): %v", err) } if err := victim.Delete(); err != nil { t.Fatalf("事务内 Delete: %v", err) } added := make([]*biRow, 10) for i := range added { added[i] = biMake(100 + i) } if err := tbl.BatchInsert(added); err != nil { t.Fatalf("事务内 BatchInsert: %v", err) } // 中途数据必须在事务内可见 (证明确实写进去了, 而不是被悄悄吞掉) if _, err := tbl.Find(2); err == nil { t.Fatal("事务内行 2 应已被删除") } // 回滚: 删除的行复活, 批插的行全部消失 if err := tx.Rollback(); err != nil { t.Fatalf("Rollback: %v", err) } if _, err := tbl.Find(2); err != nil { t.Fatalf("Rollback 后行 2 应复活: %v", err) } for _, o := range added { if _, err := tbl.Find(o.ID); err == nil { t.Fatalf("Rollback 后批插行 %d 不应存在", o.ID) } } coreTbl, ok := db.CoreDB().FindTable("txnhi") if !ok { t.Fatal("core 找不到表 txnhi") } if got := coreTbl.TotalRows(); got != 4 { t.Fatalf("Rollback 后行数: 期望 4, 实际 %d", got) } // 反向对照: 同样操作走 Commit 必须生效 (证明复用用户事务时提交链没断) tx2 := db.BeginTransaction() victim2, err := tbl.Find(1) if err != nil { t.Fatalf("对照 Find(1): %v", err) } if err := victim2.Delete(); err != nil { t.Fatalf("对照 Delete: %v", err) } kept := make([]*biRow, 5) for i := range kept { kept[i] = biMake(200 + i) } if err := tbl.BatchInsert(kept); err != nil { t.Fatalf("对照 BatchInsert: %v", err) } if err := tx2.Commit(); err != nil { t.Fatalf("对照 Commit: %v", err) } if _, err := tbl.Find(1); err == nil { t.Fatal("Commit 后行 1 应已删除") } for _, o := range kept { if _, err := tbl.Find(o.ID); err != nil { t.Fatalf("Commit 后批插行 %d 应存在: %v", o.ID, err) } } if got := coreTbl.TotalRows(); got != 8 { t.Fatalf("Commit 后行数: 期望 8, 实际 %d", got) } } // TestAutoTxnDeleteStillCommits 回归对照: 没有用户事务时, deleteByRowID 的自开自提 // 必须保持原语义 —— Delete 立即生效 (不依赖后续的 db.Commit)。 func TestAutoTxnDeleteStillCommits(t *testing.T) { db, err := Open(filepath.Join(t.TempDir(), "txn_auto_del.db")) if err != nil { t.Fatalf("打开: %v", err) } defer func() { _ = db.Close() }() db.SetAutoCommitDelay(0) tbl, err := UseTable[biRow](db, "txnad") if err != nil { t.Fatalf("建表: %v", err) } obj := &biRow{Name: "auto", Age: 30, Dept: "d", Score: 7} if err := tbl.Insert(obj); err != nil { t.Fatalf("Insert: %v", err) } if err := obj.Delete(); err != nil { // 无活跃事务 → 自开自提 t.Fatalf("Delete: %v", err) } if _, err := tbl.Find(obj.ID); err == nil { t.Fatal("Delete 后行应已删除") } }