// Command vrtxsql 在 VRTX / MySQL / SQLite 三个库之间搬表: // 结构、数据、索引、主键一起走, 任意方向任意两个库都能互转。 // // vrtxsql -from vrtx:/data/app.db -to sqlite:/data/app.sqlite // vrtxsql -from sqlite:/data/app.db -to vrtx:/data/app.db // vrtxsql -from mysql:root:pw@tcp(127.0.0.1:3306)/shop -to vrtx:/data/shop.db // vrtxsql -from mysql:root:pw@tcp(127.0.0.1:3306)/shop -to sqlite:/data/shop.sqlite // vrtxsql -from vrtx:/data/app.db -list // 只看结构, 不搬 // // DSN 的前缀决定库的种类, 冒号后面整段就是它的标识 (路径 / 连接串): // // vrtx:<文件路径> // sqlite:<文件路径> // mysql::@tcp(:)/?<参数> package main import ( "flag" "fmt" "os" "strings" "git.ycl.pub/yl32940/TsDB2/migrate" ) func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "vrtxsql:", err) os.Exit(1) } } type cliOptions struct { from string to string tables string recreate bool batch int limit uint64 dryRun bool noMeta bool autoPK bool loose bool truncOversize bool keepGoing bool list bool verbose bool forceInspect bool } func run(args []string) error { var c cliOptions fs := flag.NewFlagSet("vrtxsql", flag.ContinueOnError) fs.StringVar(&c.from, "from", "", "源库 DSN (必填), 如 vrtx:/data/app.db") fs.StringVar(&c.to, "to", "", "目标库 DSN (必填), 如 sqlite:/data/app.sqlite") fs.StringVar(&c.tables, "tables", "", "只迁这些表 (逗号分隔); 空 = 全部") fs.BoolVar(&c.recreate, "recreate", false, "目标表已存在时先 DROP 再重建 (默认复用并追加)") fs.IntVar(&c.batch, "batch", 500, "每批写入的行数") fs.Uint64Var(&c.limit, "limit", 0, "每张表最多迁多少行 (0 = 不限, 便于试跑)") fs.BoolVar(&c.dryRun, "dry-run", false, "只读结构、不写数据") fs.BoolVar(&c.noMeta, "no-meta", false, "不向 SQL 侧写 __vrtx_meta 保真表 (放弃定长档位/索引类型的还原)") fs.BoolVar(&c.autoPK, "auto-pk", false, "源表没有整数主键时自动补 _id 列 (否则报错)") fs.BoolVar(&c.loose, "loose", false, "值装不进目标列时按引擎语义截断 (默认报错, 免得静默丢数据)") fs.BoolVar(&c.truncOversize, "truncate-oversize", false, "值超过单值上限 (16360 字节) 时截断并逐行报出 (默认报错; 明确的数据丢失)") fs.BoolVar(&c.keepGoing, "continue-on-error", false, "某张表失败时跳过它、继续迁其余表 (默认第一个错误就中止)") fs.BoolVar(&c.list, "list", false, "只打印源库的表结构, 不搬数据") fs.BoolVar(&c.forceInspect, "force-inspect", false, "抢救模式: VRTX 端头页校验失败仍只读打开 (写禁用), 供损坏库抢救数据") fs.BoolVar(&c.verbose, "v", false, "打印每张表的进度") fs.Usage = func() { fmt.Fprint(os.Stderr, "用法: vrtxsql -from -to [选项]\n\n选项:\n") fs.PrintDefaults() fmt.Fprint(os.Stderr, "\nDSN: vrtx:<路径> | sqlite:<路径> | mysql::@tcp(:)/\n") } if err := fs.Parse(args); err != nil { return err } if c.from == "" { fs.Usage() return fmt.Errorf("缺少 -from") } if c.list { return listSchemas(c) } if c.to == "" { fs.Usage() return fmt.Errorf("缺少 -to") } if c.from == c.to { return fmt.Errorf("-from 与 -to 是同一个库") } const maxBatchRows = 50000 // 与 vrtxsqlweb 一致: 防 make([]Row,0,Batch) 预分配 OOM if c.batch > maxBatchRows { return fmt.Errorf("-batch 超过上限 %d", maxBatchRows) } opts := buildOptions(c) src, err := migrate.ParseDSN(c.from, opts) if err != nil { return err } defer src.Close() dst, err := migrate.ParseDSN(c.to, opts) if err != nil { return err } defer dst.Close() if opts.Recreate { fmt.Fprintf(os.Stderr, "提示: -recreate 会先删掉目标库里同名的表\n") } rep, runErr := migrate.Run(src.Src, dst.Dst, opts) printReport(rep) if runErr != nil { return runErr } fmt.Printf("完成: %s → %s, 共 %d 张表 / %d 行, 用时 %s\n", rep.From, rep.To, len(rep.Tables), rep.Rows, rep.Elapsed.Round(1e6)) return nil } func buildOptions(c cliOptions) *migrate.Options { o := migrate.DefaultOptions() o.Create = true o.EmbedMeta = !c.noMeta o.StrictValue = !c.loose o.TruncateOversize = c.truncOversize o.Recreate = c.recreate o.Batch = c.batch o.Limit = c.limit o.DryRun = c.dryRun o.AutoPK = c.autoPK o.ContinueOnError = c.keepGoing o.Verbose = c.verbose o.ForceInspect = c.forceInspect if c.tables != "" { for _, t := range strings.Split(c.tables, ",") { if t = strings.TrimSpace(t); t != "" { o.Tables = append(o.Tables, t) } } } return o } // listSchemas 打印源库每张表的结构 (列 / 类型 / 主键 / 索引), 不碰目标库。 func listSchemas(c cliOptions) error { opts := buildOptions(c) src, err := migrate.ParseDSN(c.from, opts) if err != nil { return err } defer src.Close() tables, err := src.Src.Tables() if err != nil { return err } if len(tables) == 0 { fmt.Printf("%s 里没有表\n", src.Identity()) return nil } fmt.Printf("源: %s (%s), 共 %d 张表\n", src.Identity(), src.Kind, len(tables)) for _, name := range tables { if !opts.Wants(name) { continue } s, err := src.Src.Schema(name) if err != nil { fmt.Printf("\n表 %s: 读结构失败: %v\n", name, err) continue } fmt.Printf("\n%s", s.Describe()) } return nil } func printReport(rep *migrate.Report) { if rep == nil || len(rep.Tables) == 0 { return } var ok, failed int for _, tr := range rep.Tables { fmt.Println(rep.Line(tr)) if tr.Error != "" { failed++ } else if !tr.Skipped { ok++ } } if failed > 0 { fmt.Printf("—— %d 张成功, %d 张失败 (上面带 [失败] 的行就是没迁完的表)\n", ok, failed) } }