| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145 |
- // mrepro — 迁移孤儿页复现工具。
- //
- // mrepro -src <sqlite 源库> [-workdir 目录] [-vacuum] [-keep]
- //
- // 流程: 把源库 cp 进工作目录 (绝不碰原件) → 官方 migrate.Run(sqlite→VRTX) →
- // (可选) VACUUM → orphancheck 取证 → 打印两套口径孤儿数。
- // 退出码: 真实孤儿页 > 0 时 exit 1 (供 CI/脚本判定复现)。
- package main
- import (
- "flag"
- "fmt"
- "io"
- "os"
- "path/filepath"
- "git.ycl.pub/yl32940/TsDB2/DB/core"
- "git.ycl.pub/yl32940/TsDB2/internal/orphancheck"
- "git.ycl.pub/yl32940/TsDB2/migrate"
- )
- func main() {
- src := flag.String("src", "", "sqlite 源库路径 (会先复制再迁, 不直接打开原件)")
- workdir := flag.String("workdir", "", "工作目录 (默认自动创建临时目录)")
- vac := flag.Bool("vacuum", false, "迁移后跑一次 VACUUM")
- keep := flag.Bool("keep", false, "保留工作目录 (默认结束后删除)")
- flag.Parse()
- if *src == "" {
- fmt.Fprintln(os.Stderr, "用法: mrepro -src <sqlite 源库> [-workdir 目录] [-vacuum] [-keep]")
- os.Exit(2)
- }
- wd := *workdir
- var err error
- if wd == "" {
- wd, err = os.MkdirTemp("", "mrepro-")
- if err != nil {
- fatal(err)
- }
- } else if err := os.MkdirAll(wd, 0o755); err != nil {
- fatal(err)
- }
- if !*keep {
- defer os.RemoveAll(wd)
- }
- fmt.Println("工作目录:", wd)
- // 1. 复制源库 —— 只迁副本
- srcCopy := filepath.Join(wd, "src.db")
- if err := copyFile(srcCopy, *src); err != nil {
- fatal(err)
- }
- // 2. 官方 migrate: sqlite → VRTX
- sqliteSrc, err := migrate.OpenSQLite(srcCopy, nil)
- if err != nil {
- fatal(fmt.Errorf("打开 sqlite 副本: %w", err))
- }
- outDB := filepath.Join(wd, "out.db")
- vrtxDst, err := migrate.OpenVRTX(outDB, nil)
- if err != nil {
- fatal(fmt.Errorf("打开 VRTX 目标: %w", err))
- }
- rep, runErr := migrate.Run(sqliteSrc, vrtxDst, migrate.DefaultOptions())
- _ = vrtxDst.Close()
- _ = sqliteSrc.Close()
- if runErr != nil {
- fmt.Fprintln(os.Stderr, "迁移报错 (继续取证):", runErr)
- }
- for _, tr := range rep.Tables {
- note := ""
- if tr.Error != "" {
- note = " ERROR: " + tr.Error
- }
- fmt.Printf(" 表 %-32s %8d 行 %s\n", tr.Table, tr.Rows, note)
- }
- // 3. (可选) VACUUM
- if *vac {
- db, err := core.Open(outDB)
- if err != nil {
- fatal(fmt.Errorf("打开迁移产物: %w", err))
- }
- vres, err := db.VACUUM()
- _ = db.Close()
- if err != nil {
- fatal(fmt.Errorf("VACUUM: %w", err))
- }
- fmt.Printf("VACUUM: %d → %d 字节 (省 %d), 复制 %d 行\n",
- vres.OldFileSize, vres.NewFileSize, vres.SavedBytes, vres.RowsCopied)
- }
- // 4. 取证
- R, err := orphancheck.Run(outDB, orphancheck.Options{EngineBlobScan: true, SamplesPerType: 3})
- if err != nil {
- fatal(err)
- }
- fmt.Printf("\n== 取证: %s\n", outDB)
- fmt.Printf("总页数=%d 空闲链=%d 结构可达=%d blob 链页=%d\n",
- R.TotalPages, R.FreeCount, R.ReachableNoBlob, R.BlobPages)
- fmt.Printf("analyzer 口径孤儿 = %d (按类型 %v)\n", len(R.AnalyzerOrphans), R.AnalyzerByType)
- fmt.Printf("blob 感知真实孤儿 = %d (按类型 %v)\n", len(R.Orphans), R.ByType)
- for _, n := range R.Notes {
- fmt.Println("备注:", n)
- }
- for _, tn := range orphancheck.SortedTypes(R.ByType) {
- for _, p := range R.Samples[tn] {
- fmt.Printf("-- 样例 页 %d 类型 %s UseSize=%d NextPage=%d\n", p.ID, tn, p.UseSize, p.Next)
- fmt.Print(orphancheck.SampleHex(p))
- }
- }
- if len(R.Orphans) > 0 {
- fmt.Printf("\n复现成功: %d 页真实孤儿。\n", len(R.Orphans))
- os.Exit(1)
- }
- extra := ""
- if d := len(R.AnalyzerOrphans) - len(R.Orphans); d > 0 {
- extra = fmt.Sprintf(" (analyzer 口径的 %d 页全部是活 blob 链页 — analyzer 误报)", d)
- }
- fmt.Printf("\n纯净: 真实孤儿页 = 0%s\n", extra)
- }
- func copyFile(dst, src string) error { //nolint:revive // 参数序与 io.Copy 对齐习惯
- in, err := os.Open(src)
- if err != nil {
- return err
- }
- defer in.Close()
- out, err := os.Create(dst)
- if err != nil {
- return err
- }
- if _, err := io.Copy(out, in); err != nil {
- _ = out.Close()
- return err
- }
- return out.Close()
- }
- func fatal(err error) {
- fmt.Fprintln(os.Stderr, "错误:", err)
- os.Exit(2)
- }
|