package DB import ( "bytes" "path/filepath" "testing" "git.ycl.pub/yl32940/TsDB2/DB/core" ) // TestBlobColumnORMTxRollback 显式事务内 ORM Insert 写 blob, 回滚后: // 1. 行确实没了 (FindByPK 报 not found); // 2. blob 链页整链归还空闲链 (Insert 注释承诺: 用户已开事务时链页经 NewPage // 登记进该事务, Rollback 整链回收 —— blob_stream demo 在 core 层验过, // ORM 路径必须同样成立, 否则回滚一次就泄漏一批链页)。 func TestBlobColumnORMTxRollback(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "blob_tx_rb.db") db, err := Open(dbPath) if err != nil { t.Fatalf("打开: %v", err) } defer func() { _ = db.Close() }() db.SetAutoCommitDelay(0) tbl, err := UseTable[blobOrmRow](db, "blobtx") if err != nil { t.Fatalf("建表: %v", err) } freeBefore := db.CoreDB().DebugFreePageCount() tx := db.BeginTransaction() big := make([]byte, core.MaxBlobSize*3+9) // 跨 3+ 个链页, 回滚归还才有可观测量 for i := range big { big[i] = byte(i % 199) } obj := &blobOrmRow{Name: "tx-rb", Body: big} if err := tbl.Insert(obj); err != nil { t.Fatalf("事务内 Insert: %v", err) } if err := tx.Rollback(); err != nil { t.Fatalf("Rollback: %v", err) } // 行已不存在。 if _, err := tbl.Find(obj.ID); err == nil { t.Fatal("回滚后行仍可读出") } // 链页归还: 空闲页数不少于回滚前 (归还的链页挂回空闲链)。 freeAfter := db.CoreDB().DebugFreePageCount() if freeAfter < freeBefore { t.Fatalf("回滚后空闲页 %d < 回滚前 %d: blob 链页未归还", freeAfter, freeBefore) } t.Logf("空闲页: 回滚前=%d 回滚后=%d (归还 %d 页)", freeBefore, freeAfter, freeAfter-freeBefore) } // TestBlobColumnORMNullAndDelete // - NULL/nil/空 blob: nil 切片按"空 blob"语义 (见 convert.go: 只认指针/接口, // nil 切片是空值不是 NULL) 存空 ref, 读回必须是空内容而不是报错; // - Delete 行后, 该行 blob 链页必须归还空闲链 —— 只删行不还链是慢性泄漏, // 大对象场景下几轮删插就把文件撑爆。 func TestBlobColumnORMNullAndDelete(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "blob_null_del.db") db, err := Open(dbPath) if err != nil { t.Fatalf("打开: %v", err) } defer func() { _ = db.Close() }() db.SetAutoCommitDelay(0) tbl, err := UseTable[blobOrmRow](db, "blobnd") if err != nil { t.Fatalf("建表: %v", err) } // nil / 空 blob 正常往返。 nul := &blobOrmRow{Name: "nil-body", Body: nil} empty := &blobOrmRow{Name: "empty-body", Body: []byte{}} for _, o := range []*blobOrmRow{nul, empty} { if err := tbl.Insert(o); err != nil { t.Fatalf("Insert(%s): %v", o.Name, err) } got, err := tbl.Find(o.ID) if err != nil { t.Fatalf("Find(%s): %v", o.Name, err) } if len(got.Body) != 0 { t.Fatalf("%s: 读回 %d 字节, 期望空", o.Name, len(got.Body)) } } if err := db.Commit(); err != nil { t.Fatalf("提交: %v", err) } // Delete 后链页归还: 先插一条大 blob, 记空闲页数, 删行提交, 空闲页应回升。 big := make([]byte, core.MaxBlobSize*2+11) for i := range big { big[i] = byte(i % 253) } obj := &blobOrmRow{Name: "to-delete", Body: big} if err := tbl.Insert(obj); err != nil { t.Fatalf("Insert: %v", err) } if err := db.Commit(); err != nil { t.Fatalf("提交: %v", err) } freeBefore := db.CoreDB().DebugFreePageCount() if err := tbl.Delete(obj.ID); err != nil { t.Fatalf("Delete: %v", err) } if err := db.Commit(); err != nil { t.Fatalf("提交: %v", err) } if _, err := tbl.Find(obj.ID); err == nil { t.Fatal("删除后行仍可读出") } freeAfter := db.CoreDB().DebugFreePageCount() if freeAfter <= freeBefore { t.Fatalf("删除带大 blob 的行后空闲页 %d 未回升 (删除前 %d): blob 链页泄漏", freeAfter, freeBefore) } t.Logf("空闲页: 删前=%d 删后=%d (归还 %d 页)", freeBefore, freeAfter, freeAfter-freeBefore) // 其余行不受牵连。 got, err := tbl.Find(nul.ID) if err != nil { t.Fatalf("删除牵连 Find(nil-body): %v", err) } if len(got.Body) != 0 || got.Name != "nil-body" { t.Fatalf("删除牵连邻行: %+v", got) } // 重开后内容依旧 (链上字节没被误归还污染)。 if err := db.Close(); err != nil { t.Fatalf("关库: %v", err) } db2, err := Open(dbPath) if err != nil { t.Fatalf("重开: %v", err) } defer func() { _ = db2.Close() }() db2.SetAutoCommitDelay(0) tbl2, err := UseTable[blobOrmRow](db2, "blobnd") if err != nil { t.Fatalf("重开建表: %v", err) } got2, err := tbl2.Find(empty.ID) if err != nil { t.Fatalf("重开 Find: %v", err) } if !bytes.Equal(got2.Body, []byte{}) || got2.Name != "empty-body" { t.Fatalf("重开后数据异常: %+v", got2) } }