| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149 |
- package migrate
- // 回归: sqlite → VRTX 迁移 (含 VACUUM) 之后, blob 感知口径的孤儿页必须为 0。
- //
- // 样本刻意覆盖 DockerManager 案的要素:
- // - payload TEXT 列里有一行 >16360 字节 (动态列单值上限) —— 按三态分流落成
- // Blob 链式列, 迁移产生 Type=9 blob 链页;
- // - 一个带 UNIQUE 索引的表 + 普通表。
- //
- // 判据: orphancheck 的 analyzer 口径 (不走 blob 链) 会报出这些活链页 —— 这正是
- // analyzer 用户看到的"孤儿页"; 而 blob 感知口径 (走行内 BlobRef → 链) 必须为 0,
- // 非 0 即迁移/引擎在成功路径上泄漏了页。
- import (
- "bytes"
- "database/sql"
- "fmt"
- "path/filepath"
- "testing"
- "git.ycl.pub/yl32940/TsDB2/DB/core"
- "git.ycl.pub/yl32940/TsDB2/internal/orphancheck"
- )
- // buildSQLiteSource 造一个 sqlite 源库, 返回路径。
- func buildSQLiteSource(t *testing.T) string {
- t.Helper()
- path := filepath.Join(t.TempDir(), "src.db")
- db, err := sql.Open("sqlite", path)
- if err != nil {
- t.Fatal(err)
- }
- defer db.Close()
- stmts := []string{
- `CREATE TABLE configs (id INTEGER PRIMARY KEY, name VARCHAR(64), payload TEXT)`,
- `CREATE TABLE svc (
- id INTEGER PRIMARY KEY,
- host VARCHAR(128),
- port INTEGER,
- UNIQUE(host)
- )`,
- }
- for _, s := range stmts {
- if _, err := db.Exec(s); err != nil {
- t.Fatalf("建源表: %v", err)
- }
- }
- // 大 payload: 40KB > 16360 单值上限 → 迁移时落 Blob 链式列。
- big := bytes.Repeat([]byte("0123456789abcdef"), 2560) // 40960 字节
- for i := 1; i <= 20; i++ {
- var payload any
- if i == 1 {
- payload = big
- } else {
- payload = fmt.Sprintf(`{"i":%d,"s":"small"}`, i)
- }
- if _, err := db.Exec(`INSERT INTO configs (id, name, payload) VALUES (?, ?, ?)`,
- i, fmt.Sprintf("cfg-%d", i), payload); err != nil {
- t.Fatal(err)
- }
- }
- for i := 1; i <= 50; i++ {
- if _, err := db.Exec(`INSERT INTO svc (id, host, port) VALUES (?, ?, ?)`,
- i, fmt.Sprintf("host-%d.example", i), 10000+i); err != nil {
- t.Fatal(err)
- }
- }
- return path
- }
- // migrateToVRTX 跑一次官方迁移, 返回目标 VRTX 路径。
- func migrateToVRTX(t *testing.T, srcPath string) string {
- t.Helper()
- dstPath := filepath.Join(t.TempDir(), "out.db")
- src, err := OpenSQLite(srcPath, nil)
- if err != nil {
- t.Fatalf("打开 sqlite 源: %v", err)
- }
- dst, err := OpenVRTX(dstPath, nil)
- if err != nil {
- t.Fatalf("打开 VRTX 目标: %v", err)
- }
- if _, err := Run(src, dst, quiet()); err != nil {
- t.Fatalf("迁移: %v", err)
- }
- if err := dst.Close(); err != nil {
- t.Fatalf("关闭目标: %v", err)
- }
- if err := src.Close(); err != nil {
- t.Fatalf("关闭源: %v", err)
- }
- return dstPath
- }
- func checkOrphans(t *testing.T, dbPath string) *orphancheck.Result {
- t.Helper()
- R, err := orphancheck.Run(dbPath, orphancheck.Options{EngineBlobScan: true, SamplesPerType: 3})
- if err != nil {
- t.Fatalf("取证 %s: %v", dbPath, err)
- }
- return R
- }
- func TestMigrateLeavesNoOrphanPages(t *testing.T) {
- src := buildSQLiteSource(t)
- dst := migrateToVRTX(t, src)
- R := checkOrphans(t, dst)
- t.Logf("analyzer 口径孤儿=%d %v; blob 感知真实孤儿=%d %v",
- len(R.AnalyzerOrphans), R.AnalyzerByType, len(R.Orphans), R.ByType)
- if len(R.AnalyzerOrphans) == 0 {
- t.Log("注意: 样本未产生 analyzer 口径孤儿 (blob 链页), 误报对照组失效 —— 加大样本")
- }
- if len(R.Orphans) > 0 {
- var ids []uint32
- for _, p := range R.Orphans {
- ids = append(ids, p.ID)
- }
- t.Fatalf("迁移后存在 %d 页真实孤儿 %v (按类型 %v) —— 成功路径泄漏页",
- len(R.Orphans), ids, R.ByType)
- }
- }
- func TestMigrateVacuumLeavesNoOrphanPages(t *testing.T) {
- src := buildSQLiteSource(t)
- dst := migrateToVRTX(t, src)
- db, err := core.Open(dst)
- if err != nil {
- t.Fatalf("打开迁移产物: %v", err)
- }
- if _, err := db.VACUUM(); err != nil {
- t.Fatalf("VACUUM: %v", err)
- }
- if err := db.Close(); err != nil {
- t.Fatalf("关闭: %v", err)
- }
- R := checkOrphans(t, dst)
- if len(R.Orphans) > 0 {
- var ids []uint32
- for _, p := range R.Orphans {
- ids = append(ids, p.ID)
- }
- t.Fatalf("迁移+VACUUM 后存在 %d 页真实孤儿 %v (按类型 %v)", len(R.Orphans), ids, R.ByType)
- }
- }
|