Skip to content

Commit 7bbd5aa

Browse files
ondrejsikaclaude
andcommitted
feat(cmd/migrate_mysql_database): Add slu migrate-mysql-database command
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gj7u6kyvegVdGpXfCfFvBZ
1 parent 878e808 commit 7bbd5aa

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
_ "github.com/sikalabs/slr/cmd/list_mimio_s3_bucket"
4343
_ "github.com/sikalabs/slr/cmd/list_mimio_s3_buckets"
4444
_ "github.com/sikalabs/slr/cmd/memory_usage"
45+
_ "github.com/sikalabs/slr/cmd/migrate_mysql_database"
4546
_ "github.com/sikalabs/slr/cmd/next_dev_docker_tag"
4647
_ "github.com/sikalabs/slr/cmd/nothing"
4748
_ "github.com/sikalabs/slr/cmd/ondrejsika"
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package migrate_mysql_database
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"os"
7+
"os/exec"
8+
"strings"
9+
10+
"github.com/sikalabs/slr/cmd/root"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var (
15+
FlagSource string
16+
FlagTarget string
17+
)
18+
19+
func init() {
20+
root.Cmd.AddCommand(Cmd)
21+
Cmd.Flags().StringVar(&FlagSource, "source", "", "Source MySQL URL, e.g. mysql://user:pass@host:port/database (required)")
22+
Cmd.Flags().StringVar(&FlagTarget, "target", "", "Target MySQL URL, e.g. mysql://user:pass@host:port/database (required)")
23+
Cmd.MarkFlagRequired("source")
24+
Cmd.MarkFlagRequired("target")
25+
}
26+
27+
var Cmd = &cobra.Command{
28+
Use: "migrate-mysql-database",
29+
Short: "Migrate a MySQL database from a source to a target server",
30+
Long: `Migrate a MySQL database from a source server/database to a target server/database.
31+
32+
It streams the output of "mysqldump" for the source database directly into
33+
"mysql" for the target database, so both the "mysqldump" and "mysql" binaries
34+
must be available on PATH.`,
35+
Args: cobra.NoArgs,
36+
Run: func(c *cobra.Command, args []string) {
37+
if err := migrate(FlagSource, FlagTarget); err != nil {
38+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
39+
os.Exit(1)
40+
}
41+
},
42+
}
43+
44+
type mysqlConn struct {
45+
Host string
46+
Port string
47+
User string
48+
Password string
49+
Database string
50+
}
51+
52+
func parseMySQLURL(raw string) (*mysqlConn, error) {
53+
u, err := url.Parse(raw)
54+
if err != nil {
55+
return nil, fmt.Errorf("invalid MySQL URL: %w", err)
56+
}
57+
if u.Scheme != "mysql" {
58+
return nil, fmt.Errorf("invalid MySQL URL scheme %q, expected \"mysql\"", u.Scheme)
59+
}
60+
database := strings.TrimPrefix(u.Path, "/")
61+
if database == "" {
62+
return nil, fmt.Errorf("MySQL URL must include a database name, e.g. mysql://user:pass@host:port/database")
63+
}
64+
host := u.Hostname()
65+
if host == "" {
66+
return nil, fmt.Errorf("MySQL URL must include a host")
67+
}
68+
port := u.Port()
69+
if port == "" {
70+
port = "3306"
71+
}
72+
password, _ := u.User.Password()
73+
return &mysqlConn{
74+
Host: host,
75+
Port: port,
76+
User: u.User.Username(),
77+
Password: password,
78+
Database: database,
79+
}, nil
80+
}
81+
82+
func migrate(sourceURL, targetURL string) error {
83+
source, err := parseMySQLURL(sourceURL)
84+
if err != nil {
85+
return fmt.Errorf("source: %w", err)
86+
}
87+
target, err := parseMySQLURL(targetURL)
88+
if err != nil {
89+
return fmt.Errorf("target: %w", err)
90+
}
91+
92+
if _, err := exec.LookPath("mysqldump"); err != nil {
93+
return fmt.Errorf("mysqldump binary not found in PATH: %w", err)
94+
}
95+
if _, err := exec.LookPath("mysql"); err != nil {
96+
return fmt.Errorf("mysql binary not found in PATH: %w", err)
97+
}
98+
99+
fmt.Printf("Migrating database %q from %s:%s to %q on %s:%s...\n",
100+
source.Database, source.Host, source.Port, target.Database, target.Host, target.Port)
101+
102+
dumpCmd := exec.Command("mysqldump",
103+
"-h", source.Host,
104+
"-P", source.Port,
105+
"-u", source.User,
106+
"--single-transaction",
107+
"--routines",
108+
"--triggers",
109+
source.Database,
110+
)
111+
dumpCmd.Env = append(os.Environ(), "MYSQL_PWD="+source.Password)
112+
dumpCmd.Stderr = os.Stderr
113+
114+
restoreCmd := exec.Command("mysql",
115+
"-h", target.Host,
116+
"-P", target.Port,
117+
"-u", target.User,
118+
target.Database,
119+
)
120+
restoreCmd.Env = append(os.Environ(), "MYSQL_PWD="+target.Password)
121+
restoreCmd.Stderr = os.Stderr
122+
restoreCmd.Stdout = os.Stdout
123+
124+
pipe, err := dumpCmd.StdoutPipe()
125+
if err != nil {
126+
return fmt.Errorf("failed to create pipe between mysqldump and mysql: %w", err)
127+
}
128+
restoreCmd.Stdin = pipe
129+
130+
if err := restoreCmd.Start(); err != nil {
131+
return fmt.Errorf("failed to start mysql: %w", err)
132+
}
133+
if err := dumpCmd.Run(); err != nil {
134+
return fmt.Errorf("mysqldump failed: %w", err)
135+
}
136+
if err := restoreCmd.Wait(); err != nil {
137+
return fmt.Errorf("mysql restore failed: %w", err)
138+
}
139+
140+
fmt.Println("Migration completed successfully!")
141+
return nil
142+
}

0 commit comments

Comments
 (0)