@@ -21,9 +21,16 @@ type mysqlSyncConfig struct {
2121 TargetHost string
2222 TargetPort int
2323 TargetPasswordEnv string
24+ DatabaseSSH syncSSHTarget
2425 DumpFlags []string
2526}
2627
28+ type syncSSHTarget struct {
29+ Host string
30+ Username string
31+ Port int
32+ }
33+
2734func runDatabaseSync (cmd * cobra.Command , target ResolvedSyncTarget , _ * SyncSettings , dryRun bool , autoApprove bool ) error {
2835 if isNonInteractiveMode () {
2936 return fmt .Errorf ("database sync requires interactive prompts in current implementation; run with an interactive terminal" )
@@ -41,13 +48,13 @@ func runDatabaseSync(cmd *cobra.Command, target ResolvedSyncTarget, _ *SyncSetti
4148 return err
4249 }
4350
44- if isSameDatabaseTarget (target , databaseConfig ) {
51+ if isSameDatabaseTarget (databaseConfig ) {
4552 return fmt .Errorf ("source and target resolve to the same database destination" )
4653 }
4754
4855 fmt .Println ("Database sync plan:" )
4956 fmt .Printf (" Source: %s@%s:%d/%s\n " , databaseConfig .SourceUser , databaseConfig .SourceHost , databaseConfig .SourcePort , databaseConfig .SourceDatabase )
50- fmt .Printf (" Target: %s@%s:%d/%s (via %s@%s:%d)\n " , databaseConfig .TargetUser , databaseConfig .TargetHost , databaseConfig .TargetPort , databaseConfig .TargetDatabase , target . Username , target . Host , target .Port )
57+ fmt .Printf (" Target: %s@%s:%d/%s (via %s@%s:%d)\n " , databaseConfig .TargetUser , databaseConfig .TargetHost , databaseConfig .TargetPort , databaseConfig .TargetDatabase , databaseConfig . DatabaseSSH . Username , databaseConfig . DatabaseSSH . Host , databaseConfig . DatabaseSSH .Port )
5158
5259 if ! autoApprove {
5360 confirmed , confirmErr := promptYesNo ("Continue with MySQL import into target?" )
@@ -73,7 +80,7 @@ func runDatabaseSync(cmd *cobra.Command, target ResolvedSyncTarget, _ *SyncSetti
7380
7481 dumpArgs := buildMySQLDumpArgs (databaseConfig )
7582 remoteCommand := buildRemoteMySQLImportCommand (databaseConfig , targetPassword )
76- sshArgs := [] string { "-p" , strconv . Itoa ( target . Port ), fmt . Sprintf ( "%s@%s" , target . Username , target . Host ), remoteCommand }
83+ sshArgs := buildDatabaseSSHArgs ( databaseConfig . DatabaseSSH , remoteCommand )
7784
7885 fmt .Printf ("Running: MYSQL_PWD=<hidden> mysqldump %s | ssh %s\n " , strings .Join (dumpArgs , " " ), strings .Join (sshArgs , " " ))
7986
@@ -173,6 +180,11 @@ func promptMySQLSyncConfig(target ResolvedSyncTarget) (mysqlSyncConfig, error) {
173180 return mysqlSyncConfig {}, err
174181 }
175182
183+ databaseSSH , err := promptDatabaseSSHTarget (target )
184+ if err != nil {
185+ return mysqlSyncConfig {}, err
186+ }
187+
176188 flagsRaw , err := promptText ("Extra mysqldump flags (space-separated, optional)" , "--single-transaction --quick" , optionalValue , 0 )
177189 if err != nil {
178190 return mysqlSyncConfig {}, err
@@ -189,10 +201,47 @@ func promptMySQLSyncConfig(target ResolvedSyncTarget) (mysqlSyncConfig, error) {
189201 TargetHost : strings .TrimSpace (targetHost ),
190202 TargetPort : targetPort ,
191203 TargetPasswordEnv : strings .TrimSpace (targetPasswordEnv ),
204+ DatabaseSSH : databaseSSH ,
192205 DumpFlags : strings .Fields (strings .TrimSpace (flagsRaw )),
193206 }, nil
194207}
195208
209+ func promptDatabaseSSHTarget (target ResolvedSyncTarget ) (syncSSHTarget , error ) {
210+ defaultTarget := defaultDatabaseSSHTarget (target )
211+
212+ host , err := promptText ("Database SSH host override (optional)" , "" , optionalValue , 0 )
213+ if err != nil {
214+ return syncSSHTarget {}, err
215+ }
216+ if strings .TrimSpace (host ) == "" {
217+ return defaultTarget , nil
218+ }
219+
220+ username , err := promptText ("Database SSH user" , defaultTarget .Username , requiredValue , 0 )
221+ if err != nil {
222+ return syncSSHTarget {}, err
223+ }
224+
225+ port , err := promptPort ("Database SSH port" , strconv .Itoa (defaultTarget .Port ))
226+ if err != nil {
227+ return syncSSHTarget {}, err
228+ }
229+
230+ return syncSSHTarget {
231+ Host : strings .TrimSpace (host ),
232+ Username : strings .TrimSpace (username ),
233+ Port : port ,
234+ }, nil
235+ }
236+
237+ func defaultDatabaseSSHTarget (target ResolvedSyncTarget ) syncSSHTarget {
238+ return syncSSHTarget {
239+ Host : strings .TrimSpace (target .Host ),
240+ Username : strings .TrimSpace (target .Username ),
241+ Port : target .Port ,
242+ }
243+ }
244+
196245func promptPort (label , defaultValue string ) (int , error ) {
197246 value , err := promptText (label , defaultValue , requiredValue , 0 )
198247 if err != nil {
@@ -230,23 +279,42 @@ func buildRemoteMySQLImportCommand(config mysqlSyncConfig, targetPassword string
230279 )
231280}
232281
282+ func buildDatabaseSSHArgs (target syncSSHTarget , remoteCommand string ) []string {
283+ return []string {"-p" , strconv .Itoa (target .Port ), fmt .Sprintf ("%s@%s" , target .Username , target .Host ), remoteCommand }
284+ }
285+
233286func shellQuote (value string ) string {
234287 escaped := strings .ReplaceAll (value , "'" , "'\\ ''" )
235288 return "'" + escaped + "'"
236289}
237290
238- func isSameDatabaseTarget (target ResolvedSyncTarget , config mysqlSyncConfig ) bool {
239- if ! strings . EqualFold ( strings . TrimSpace ( target . Host ), strings . TrimSpace ( config .SourceHost ) ) {
291+ func isSameDatabaseTarget (config mysqlSyncConfig ) bool {
292+ if ! isSameDatabaseHost ( config . SourceHost , config . TargetHost , config .DatabaseSSH . Host ) {
240293 return false
241294 }
242295
243296 if config .TargetPort != config .SourcePort {
244297 return false
245298 }
246299
247- if ! strings .EqualFold (strings .TrimSpace (config .TargetHost ), strings .TrimSpace (config .SourceHost )) {
300+ return strings .EqualFold (strings .TrimSpace (config .SourceDatabase ), strings .TrimSpace (config .TargetDatabase ))
301+ }
302+
303+ func isSameDatabaseHost (sourceHost string , targetHost string , targetSSHHost string ) bool {
304+ source := strings .TrimSpace (sourceHost )
305+ target := strings .TrimSpace (targetHost )
306+ if source == "" || target == "" {
248307 return false
249308 }
250309
251- return strings .EqualFold (strings .TrimSpace (config .SourceDatabase ), strings .TrimSpace (config .TargetDatabase ))
310+ if ! isLoopbackHost (target ) {
311+ return strings .EqualFold (source , target )
312+ }
313+
314+ return strings .EqualFold (source , strings .TrimSpace (targetSSHHost ))
315+ }
316+
317+ func isLoopbackHost (host string ) bool {
318+ normalized := strings .ToLower (strings .TrimSpace (host ))
319+ return normalized == "localhost" || normalized == "127.0.0.1" || normalized == "::1"
252320}
0 commit comments