main.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. // Command vrtxsql 在 VRTX / MySQL / SQLite 三个库之间搬表:
  2. // 结构、数据、索引、主键一起走, 任意方向任意两个库都能互转。
  3. //
  4. // vrtxsql -from vrtx:/data/app.db -to sqlite:/data/app.sqlite
  5. // vrtxsql -from sqlite:/data/app.db -to vrtx:/data/app.db
  6. // vrtxsql -from mysql:root:pw@tcp(127.0.0.1:3306)/shop -to vrtx:/data/shop.db
  7. // vrtxsql -from mysql:root:pw@tcp(127.0.0.1:3306)/shop -to sqlite:/data/shop.sqlite
  8. // vrtxsql -from vrtx:/data/app.db -list // 只看结构, 不搬
  9. //
  10. // DSN 的前缀决定库的种类, 冒号后面整段就是它的标识 (路径 / 连接串):
  11. //
  12. // vrtx:<文件路径>
  13. // sqlite:<文件路径>
  14. // mysql:<user>:<pw>@tcp(<host>:<port>)/<db>?<参数>
  15. package main
  16. import (
  17. "flag"
  18. "fmt"
  19. "os"
  20. "strings"
  21. "git.ycl.pub/yl32940/TsDB2/migrate"
  22. )
  23. func main() {
  24. if err := run(os.Args[1:]); err != nil {
  25. fmt.Fprintln(os.Stderr, "vrtxsql:", err)
  26. os.Exit(1)
  27. }
  28. }
  29. type cliOptions struct {
  30. from string
  31. to string
  32. tables string
  33. recreate bool
  34. batch int
  35. limit uint64
  36. dryRun bool
  37. noMeta bool
  38. autoPK bool
  39. loose bool
  40. truncOversize bool
  41. keepGoing bool
  42. list bool
  43. verbose bool
  44. forceInspect bool
  45. }
  46. func run(args []string) error {
  47. var c cliOptions
  48. fs := flag.NewFlagSet("vrtxsql", flag.ContinueOnError)
  49. fs.StringVar(&c.from, "from", "", "源库 DSN (必填), 如 vrtx:/data/app.db")
  50. fs.StringVar(&c.to, "to", "", "目标库 DSN (必填), 如 sqlite:/data/app.sqlite")
  51. fs.StringVar(&c.tables, "tables", "", "只迁这些表 (逗号分隔); 空 = 全部")
  52. fs.BoolVar(&c.recreate, "recreate", false, "目标表已存在时先 DROP 再重建 (默认复用并追加)")
  53. fs.IntVar(&c.batch, "batch", 500, "每批写入的行数")
  54. fs.Uint64Var(&c.limit, "limit", 0, "每张表最多迁多少行 (0 = 不限, 便于试跑)")
  55. fs.BoolVar(&c.dryRun, "dry-run", false, "只读结构、不写数据")
  56. fs.BoolVar(&c.noMeta, "no-meta", false, "不向 SQL 侧写 __vrtx_meta 保真表 (放弃定长档位/索引类型的还原)")
  57. fs.BoolVar(&c.autoPK, "auto-pk", false, "源表没有整数主键时自动补 _id 列 (否则报错)")
  58. fs.BoolVar(&c.loose, "loose", false, "值装不进目标列时按引擎语义截断 (默认报错, 免得静默丢数据)")
  59. fs.BoolVar(&c.truncOversize, "truncate-oversize", false,
  60. "值超过单值上限 (16360 字节) 时截断并逐行报出 (默认报错; 明确的数据丢失)")
  61. fs.BoolVar(&c.keepGoing, "continue-on-error", false, "某张表失败时跳过它、继续迁其余表 (默认第一个错误就中止)")
  62. fs.BoolVar(&c.list, "list", false, "只打印源库的表结构, 不搬数据")
  63. fs.BoolVar(&c.forceInspect, "force-inspect", false, "抢救模式: VRTX 端头页校验失败仍只读打开 (写禁用), 供损坏库抢救数据")
  64. fs.BoolVar(&c.verbose, "v", false, "打印每张表的进度")
  65. fs.Usage = func() {
  66. fmt.Fprint(os.Stderr, "用法: vrtxsql -from <DSN> -to <DSN> [选项]\n\n选项:\n")
  67. fs.PrintDefaults()
  68. fmt.Fprint(os.Stderr, "\nDSN: vrtx:<路径> | sqlite:<路径> | mysql:<user>:<pw>@tcp(<host>:<port>)/<db>\n")
  69. }
  70. if err := fs.Parse(args); err != nil {
  71. return err
  72. }
  73. if c.from == "" {
  74. fs.Usage()
  75. return fmt.Errorf("缺少 -from")
  76. }
  77. if c.list {
  78. return listSchemas(c)
  79. }
  80. if c.to == "" {
  81. fs.Usage()
  82. return fmt.Errorf("缺少 -to")
  83. }
  84. if c.from == c.to {
  85. return fmt.Errorf("-from 与 -to 是同一个库")
  86. }
  87. opts := buildOptions(c)
  88. src, err := migrate.ParseDSN(c.from, opts)
  89. if err != nil {
  90. return err
  91. }
  92. defer src.Close()
  93. dst, err := migrate.ParseDSN(c.to, opts)
  94. if err != nil {
  95. return err
  96. }
  97. defer dst.Close()
  98. if opts.Recreate {
  99. fmt.Fprintf(os.Stderr, "提示: -recreate 会先删掉目标库里同名的表\n")
  100. }
  101. rep, runErr := migrate.Run(src.Src, dst.Dst, opts)
  102. printReport(rep)
  103. if runErr != nil {
  104. return runErr
  105. }
  106. fmt.Printf("完成: %s → %s, 共 %d 张表 / %d 行, 用时 %s\n",
  107. rep.From, rep.To, len(rep.Tables), rep.Rows, rep.Elapsed.Round(1e6))
  108. return nil
  109. }
  110. func buildOptions(c cliOptions) *migrate.Options {
  111. o := migrate.DefaultOptions()
  112. o.Create = true
  113. o.EmbedMeta = !c.noMeta
  114. o.StrictValue = !c.loose
  115. o.TruncateOversize = c.truncOversize
  116. o.Recreate = c.recreate
  117. o.Batch = c.batch
  118. o.Limit = c.limit
  119. o.DryRun = c.dryRun
  120. o.AutoPK = c.autoPK
  121. o.ContinueOnError = c.keepGoing
  122. o.Verbose = c.verbose
  123. o.ForceInspect = c.forceInspect
  124. if c.tables != "" {
  125. for _, t := range strings.Split(c.tables, ",") {
  126. if t = strings.TrimSpace(t); t != "" {
  127. o.Tables = append(o.Tables, t)
  128. }
  129. }
  130. }
  131. return o
  132. }
  133. // listSchemas 打印源库每张表的结构 (列 / 类型 / 主键 / 索引), 不碰目标库。
  134. func listSchemas(c cliOptions) error {
  135. opts := buildOptions(c)
  136. src, err := migrate.ParseDSN(c.from, opts)
  137. if err != nil {
  138. return err
  139. }
  140. defer src.Close()
  141. tables, err := src.Src.Tables()
  142. if err != nil {
  143. return err
  144. }
  145. if len(tables) == 0 {
  146. fmt.Printf("%s 里没有表\n", src.Identity())
  147. return nil
  148. }
  149. fmt.Printf("源: %s (%s), 共 %d 张表\n", src.Identity(), src.Kind, len(tables))
  150. for _, name := range tables {
  151. if !opts.Wants(name) {
  152. continue
  153. }
  154. s, err := src.Src.Schema(name)
  155. if err != nil {
  156. fmt.Printf("\n表 %s: 读结构失败: %v\n", name, err)
  157. continue
  158. }
  159. fmt.Printf("\n%s", s.Describe())
  160. }
  161. return nil
  162. }
  163. func printReport(rep *migrate.Report) {
  164. if rep == nil || len(rep.Tables) == 0 {
  165. return
  166. }
  167. var ok, failed int
  168. for _, tr := range rep.Tables {
  169. fmt.Println(rep.Line(tr))
  170. if tr.Error != "" {
  171. failed++
  172. } else if !tr.Skipped {
  173. ok++
  174. }
  175. }
  176. if failed > 0 {
  177. fmt.Printf("—— %d 张成功, %d 张失败 (上面带 [失败] 的行就是没迁完的表)\n", ok, failed)
  178. }
  179. }