main.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. const maxBatchRows = 50000 // 与 vrtxsqlweb 一致: 防 make([]Row,0,Batch) 预分配 OOM
  88. if c.batch > maxBatchRows {
  89. return fmt.Errorf("-batch 超过上限 %d", maxBatchRows)
  90. }
  91. opts := buildOptions(c)
  92. src, err := migrate.ParseDSN(c.from, opts)
  93. if err != nil {
  94. return err
  95. }
  96. defer src.Close()
  97. dst, err := migrate.ParseDSN(c.to, opts)
  98. if err != nil {
  99. return err
  100. }
  101. defer dst.Close()
  102. if opts.Recreate {
  103. fmt.Fprintf(os.Stderr, "提示: -recreate 会先删掉目标库里同名的表\n")
  104. }
  105. rep, runErr := migrate.Run(src.Src, dst.Dst, opts)
  106. printReport(rep)
  107. if runErr != nil {
  108. return runErr
  109. }
  110. fmt.Printf("完成: %s → %s, 共 %d 张表 / %d 行, 用时 %s\n",
  111. rep.From, rep.To, len(rep.Tables), rep.Rows, rep.Elapsed.Round(1e6))
  112. return nil
  113. }
  114. func buildOptions(c cliOptions) *migrate.Options {
  115. o := migrate.DefaultOptions()
  116. o.Create = true
  117. o.EmbedMeta = !c.noMeta
  118. o.StrictValue = !c.loose
  119. o.TruncateOversize = c.truncOversize
  120. o.Recreate = c.recreate
  121. o.Batch = c.batch
  122. o.Limit = c.limit
  123. o.DryRun = c.dryRun
  124. o.AutoPK = c.autoPK
  125. o.ContinueOnError = c.keepGoing
  126. o.Verbose = c.verbose
  127. o.ForceInspect = c.forceInspect
  128. if c.tables != "" {
  129. for _, t := range strings.Split(c.tables, ",") {
  130. if t = strings.TrimSpace(t); t != "" {
  131. o.Tables = append(o.Tables, t)
  132. }
  133. }
  134. }
  135. return o
  136. }
  137. // listSchemas 打印源库每张表的结构 (列 / 类型 / 主键 / 索引), 不碰目标库。
  138. func listSchemas(c cliOptions) error {
  139. opts := buildOptions(c)
  140. src, err := migrate.ParseDSN(c.from, opts)
  141. if err != nil {
  142. return err
  143. }
  144. defer src.Close()
  145. tables, err := src.Src.Tables()
  146. if err != nil {
  147. return err
  148. }
  149. if len(tables) == 0 {
  150. fmt.Printf("%s 里没有表\n", src.Identity())
  151. return nil
  152. }
  153. fmt.Printf("源: %s (%s), 共 %d 张表\n", src.Identity(), src.Kind, len(tables))
  154. for _, name := range tables {
  155. if !opts.Wants(name) {
  156. continue
  157. }
  158. s, err := src.Src.Schema(name)
  159. if err != nil {
  160. fmt.Printf("\n表 %s: 读结构失败: %v\n", name, err)
  161. continue
  162. }
  163. fmt.Printf("\n%s", s.Describe())
  164. }
  165. return nil
  166. }
  167. func printReport(rep *migrate.Report) {
  168. if rep == nil || len(rep.Tables) == 0 {
  169. return
  170. }
  171. var ok, failed int
  172. for _, tr := range rep.Tables {
  173. fmt.Println(rep.Line(tr))
  174. if tr.Error != "" {
  175. failed++
  176. } else if !tr.Skipped {
  177. ok++
  178. }
  179. }
  180. if failed > 0 {
  181. fmt.Printf("—— %d 张成功, %d 张失败 (上面带 [失败] 的行就是没迁完的表)\n", ok, failed)
  182. }
  183. }