| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- // 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:<user>:<pw>@tcp(<host>:<port>)/<db>?<参数>
- 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 <DSN> -to <DSN> [选项]\n\n选项:\n")
- fs.PrintDefaults()
- fmt.Fprint(os.Stderr, "\nDSN: vrtx:<路径> | sqlite:<路径> | mysql:<user>:<pw>@tcp(<host>:<port>)/<db>\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)
- }
- }
|