main.go 5.4 KB

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