main.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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. "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. keepGoing bool
  41. list bool
  42. verbose bool
  43. }
  44. func run(args []string) error {
  45. var c cliOptions
  46. fs := flag.NewFlagSet("vrtxsql", flag.ContinueOnError)
  47. fs.StringVar(&c.from, "from", "", "源库 DSN (必填), 如 vrtx:/data/app.db")
  48. fs.StringVar(&c.to, "to", "", "目标库 DSN (必填), 如 sqlite:/data/app.sqlite")
  49. fs.StringVar(&c.tables, "tables", "", "只迁这些表 (逗号分隔); 空 = 全部")
  50. fs.BoolVar(&c.recreate, "recreate", false, "目标表已存在时先 DROP 再重建 (默认复用并追加)")
  51. fs.IntVar(&c.batch, "batch", 500, "每批写入的行数")
  52. fs.Uint64Var(&c.limit, "limit", 0, "每张表最多迁多少行 (0 = 不限, 便于试跑)")
  53. fs.BoolVar(&c.dryRun, "dry-run", false, "只读结构、不写数据")
  54. fs.BoolVar(&c.noMeta, "no-meta", false, "不向 SQL 侧写 __vrtx_meta 保真表 (放弃定长档位/索引类型的还原)")
  55. fs.BoolVar(&c.autoPK, "auto-pk", false, "源表没有整数主键时自动补 _id 列 (否则报错)")
  56. fs.BoolVar(&c.loose, "loose", false, "值装不进目标列时按引擎语义截断 (默认报错, 免得静默丢数据)")
  57. fs.BoolVar(&c.keepGoing, "continue-on-error", false, "某张表失败时跳过它、继续迁其余表 (默认第一个错误就中止)")
  58. fs.BoolVar(&c.list, "list", false, "只打印源库的表结构, 不搬数据")
  59. fs.BoolVar(&c.verbose, "v", false, "打印每张表的进度")
  60. fs.Usage = func() {
  61. fmt.Fprint(os.Stderr, "用法: vrtxsql -from <DSN> -to <DSN> [选项]\n\n选项:\n")
  62. fs.PrintDefaults()
  63. fmt.Fprint(os.Stderr, "\nDSN: vrtx:<路径> | sqlite:<路径> | mysql:<user>:<pw>@tcp(<host>:<port>)/<db>\n")
  64. }
  65. if err := fs.Parse(args); err != nil {
  66. return err
  67. }
  68. if c.from == "" {
  69. fs.Usage()
  70. return fmt.Errorf("缺少 -from")
  71. }
  72. if c.list {
  73. return listSchemas(c)
  74. }
  75. if c.to == "" {
  76. fs.Usage()
  77. return fmt.Errorf("缺少 -to")
  78. }
  79. if c.from == c.to {
  80. return fmt.Errorf("-from 与 -to 是同一个库")
  81. }
  82. opts := buildOptions(c)
  83. src, err := migrate.ParseDSN(c.from, opts)
  84. if err != nil {
  85. return err
  86. }
  87. defer src.Close()
  88. dst, err := migrate.ParseDSN(c.to, opts)
  89. if err != nil {
  90. return err
  91. }
  92. defer dst.Close()
  93. if opts.Recreate {
  94. fmt.Fprintf(os.Stderr, "提示: -recreate 会先删掉目标库里同名的表\n")
  95. }
  96. rep, runErr := migrate.Run(src.Src, dst.Dst, opts)
  97. printReport(rep)
  98. if runErr != nil {
  99. return runErr
  100. }
  101. fmt.Printf("完成: %s → %s, 共 %d 张表 / %d 行, 用时 %s\n",
  102. rep.From, rep.To, len(rep.Tables), rep.Rows, rep.Elapsed.Round(1e6))
  103. return nil
  104. }
  105. func buildOptions(c cliOptions) *migrate.Options {
  106. o := migrate.DefaultOptions()
  107. o.Create = true
  108. o.EmbedMeta = !c.noMeta
  109. o.StrictValue = !c.loose
  110. o.Recreate = c.recreate
  111. o.Batch = c.batch
  112. o.Limit = c.limit
  113. o.DryRun = c.dryRun
  114. o.AutoPK = c.autoPK
  115. o.ContinueOnError = c.keepGoing
  116. o.Verbose = c.verbose
  117. if c.tables != "" {
  118. for _, t := range strings.Split(c.tables, ",") {
  119. if t = strings.TrimSpace(t); t != "" {
  120. o.Tables = append(o.Tables, t)
  121. }
  122. }
  123. }
  124. return o
  125. }
  126. // listSchemas 打印源库每张表的结构 (列 / 类型 / 主键 / 索引), 不碰目标库。
  127. func listSchemas(c cliOptions) error {
  128. opts := buildOptions(c)
  129. src, err := migrate.ParseDSN(c.from, opts)
  130. if err != nil {
  131. return err
  132. }
  133. defer src.Close()
  134. tables, err := src.Src.Tables()
  135. if err != nil {
  136. return err
  137. }
  138. if len(tables) == 0 {
  139. fmt.Printf("%s 里没有表\n", src.Identity())
  140. return nil
  141. }
  142. fmt.Printf("源: %s (%s), 共 %d 张表\n", src.Identity(), src.Kind, len(tables))
  143. for _, name := range tables {
  144. if !opts.Wants(name) {
  145. continue
  146. }
  147. s, err := src.Src.Schema(name)
  148. if err != nil {
  149. fmt.Printf("\n表 %s: 读结构失败: %v\n", name, err)
  150. continue
  151. }
  152. fmt.Printf("\n%s", s.Describe())
  153. }
  154. return nil
  155. }
  156. func printReport(rep *migrate.Report) {
  157. if rep == nil || len(rep.Tables) == 0 {
  158. return
  159. }
  160. var ok, failed int
  161. for _, tr := range rep.Tables {
  162. fmt.Println(rep.Line(tr))
  163. if tr.Error != "" {
  164. failed++
  165. } else if !tr.Skipped {
  166. ok++
  167. }
  168. }
  169. if failed > 0 {
  170. fmt.Printf("—— %d 张成功, %d 张失败 (上面带 [失败] 的行就是没迁完的表)\n", ok, failed)
  171. }
  172. }