Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,14 +479,18 @@ External create required flags: `--host`, `--source-database`, `--username`, `--

## Vitess MoveTables

Copy tables between keyspaces with `pscale branch vtctld move-tables`. `pscale workflow` will be deprecated soon; prefer `move-tables` for new work. JSON output includes `next_steps` — follow those commands. Typical order: create the target keyspace (`keyspace create` or `keyspace create-external`), create the workflow, poll `status`, switch replica traffic, then primary traffic (ask the user first), then `complete --dry-run` and `complete` after approval.
Copy tables between keyspaces with `pscale branch vtctld move-tables`. `pscale workflow` will be deprecated soon; prefer `move-tables` for new work. JSON output includes `next_steps` — follow those commands. Typical order: create the target keyspace (`keyspace create` or `keyspace create-external`), create the workflow, `start` if you used `--auto-start=false` or after `stop`, poll `status`, switch replica traffic, then primary traffic (ask the user first), then `complete --dry-run` and `complete` after approval.

`--workflow` is the workflow name you choose. `--source-keyspace` and `--target-keyspace` are required on create. Pass `--tables t1,t2` or `--all-tables` (mutually exclusive).
`--workflow` is the workflow name you choose. `--source-keyspace` and `--target-keyspace` are required on create. Pass `--tables t1,t2` or `--all-tables` (mutually exclusive). `start` and `stop` take `--workflow` and `--target-keyspace`.

```bash
pscale branch vtctld move-tables list <database> <branch> --org <org> --format json
pscale branch vtctld move-tables create <database> <branch> --org <org> --format json \
--workflow <workflow> --source-keyspace <source> --target-keyspace <target> --tables <table>
pscale branch vtctld move-tables start <database> <branch> --org <org> --format json \
--workflow <workflow> --target-keyspace <target>
pscale branch vtctld move-tables stop <database> <branch> --org <org> --format json \
--workflow <workflow> --target-keyspace <target>
pscale branch vtctld move-tables status <database> <branch> --org <org> --format json \
--workflow <workflow> --target-keyspace <target>
pscale branch vtctld move-tables switch-traffic <database> <branch> --org <org> --format json \
Expand Down
112 changes: 110 additions & 2 deletions internal/cmd/branch/vtctld/move_tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func MoveTablesCmd(ch *cmdutil.Helper) *cobra.Command {
cmd.AddCommand(MoveTablesCreateCmd(ch))
cmd.AddCommand(MoveTablesShowCmd(ch))
cmd.AddCommand(MoveTablesStatusCmd(ch))
cmd.AddCommand(MoveTablesStartCmd(ch))
cmd.AddCommand(MoveTablesStopCmd(ch))
cmd.AddCommand(MoveTablesSwitchTrafficCmd(ch))
cmd.AddCommand(MoveTablesReverseTrafficCmd(ch))
cmd.AddCommand(MoveTablesCancelCmd(ch))
Expand Down Expand Up @@ -121,9 +123,15 @@ func MoveTablesCreateCmd(ch *cmdutil.Helper) *cobra.Command {
}

end()
return printWorkflowJSON(ch.Printer, data, []workflowNextStep{
nextSteps := []workflowNextStep{
moveTablesStatusStep(ch.Config.Organization, database, branch, flags.workflow, flags.targetKeyspace, "Monitor copy and replication progress"),
})
}
if cmd.Flags().Changed("auto-start") && !flags.autoStart {
nextSteps = []workflowNextStep{
moveTablesStartStep(ch.Config.Organization, database, branch, flags.workflow, flags.targetKeyspace, "Start the workflow after creating it with --auto-start=false"),
}
}
return printWorkflowJSON(ch.Printer, data, nextSteps)
},
}

Expand Down Expand Up @@ -300,6 +308,106 @@ func MoveTablesStatusCmd(ch *cmdutil.Helper) *cobra.Command {
return cmd
}

func MoveTablesStartCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
workflow string
targetKeyspace string
}

cmd := &cobra.Command{
Use: "start <database> <branch>",
Short: "Start a MoveTables workflow",
Args: cmdutil.RequiredArgs("database", "branch"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
database, branch := args[0], args[1]

client, err := ch.Client()
if err != nil {
return err
}

end := ch.Printer.PrintProgress(
fmt.Sprintf("Starting MoveTables workflow %s on %s\u2026",
printer.BoldBlue(flags.workflow), progressTarget(ch.Config.Organization, database, branch)))
defer end()

data, err := client.Vtctld.StartWorkflow(ctx, &ps.VtctldStartWorkflowRequest{
Organization: ch.Config.Organization,
Database: database,
Branch: branch,
Workflow: flags.workflow,
Keyspace: flags.targetKeyspace,
})
if err != nil {
return cmdutil.HandleError(err)
}

end()
return printWorkflowJSON(ch.Printer, data, []workflowNextStep{
moveTablesStatusStep(ch.Config.Organization, database, branch, flags.workflow, flags.targetKeyspace, "Monitor copy and replication progress"),
})
},
}

cmd.Flags().StringVar(&flags.workflow, "workflow", "", "Name of the workflow")
cmd.Flags().StringVar(&flags.targetKeyspace, "target-keyspace", "", "Target keyspace")
cmd.MarkFlagRequired("workflow") // nolint:errcheck
cmd.MarkFlagRequired("target-keyspace") // nolint:errcheck

return cmd
}

func MoveTablesStopCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
workflow string
targetKeyspace string
}

cmd := &cobra.Command{
Use: "stop <database> <branch>",
Short: "Stop a MoveTables workflow",
Args: cmdutil.RequiredArgs("database", "branch"),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
database, branch := args[0], args[1]

client, err := ch.Client()
if err != nil {
return err
}

end := ch.Printer.PrintProgress(
fmt.Sprintf("Stopping MoveTables workflow %s on %s\u2026",
printer.BoldBlue(flags.workflow), progressTarget(ch.Config.Organization, database, branch)))
defer end()

data, err := client.Vtctld.StopWorkflow(ctx, &ps.VtctldStopWorkflowRequest{
Organization: ch.Config.Organization,
Database: database,
Branch: branch,
Workflow: flags.workflow,
Keyspace: flags.targetKeyspace,
})
if err != nil {
return cmdutil.HandleError(err)
}

end()
return printWorkflowJSON(ch.Printer, data, []workflowNextStep{
moveTablesStartStep(ch.Config.Organization, database, branch, flags.workflow, flags.targetKeyspace, "Resume the workflow when you are ready to continue"),
})
},
}

cmd.Flags().StringVar(&flags.workflow, "workflow", "", "Name of the workflow")
cmd.Flags().StringVar(&flags.targetKeyspace, "target-keyspace", "", "Target keyspace")
cmd.MarkFlagRequired("workflow") // nolint:errcheck
cmd.MarkFlagRequired("target-keyspace") // nolint:errcheck

return cmd
}

func MoveTablesSwitchTrafficCmd(ch *cmdutil.Helper) *cobra.Command {
var flags struct {
workflow string
Expand Down
167 changes: 167 additions & 0 deletions internal/cmd/branch/vtctld/move_tables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,55 @@ func TestMoveTablesCreateWithAllFlags(t *testing.T) {
})
}

func TestMoveTablesCreateWithAutoStartFalse(t *testing.T) {
c := qt.New(t)
setMoveTablesPollInterval(t, 0)

org := "my-org"
db := "my-db"
branch := "my-branch"

svc := &mock.MoveTablesService{
CreateFn: func(ctx context.Context, req *ps.MoveTablesCreateRequest) (*ps.VtctldOperationReference, error) {
c.Assert(req.AutoStart, qt.IsNotNil)
c.Assert(*req.AutoStart, qt.IsFalse)
return &ps.VtctldOperationReference{ID: "create-op"}, nil
},
}

vtctldSvc := &mock.VtctldService{
GetOperationFn: func(ctx context.Context, req *ps.GetVtctldOperationRequest) (*ps.VtctldOperation, error) {
return &ps.VtctldOperation{
ID: "create-op",
State: "completed",
Completed: true,
Result: json.RawMessage(`{"summary":"created"}`),
}, nil
},
}

var buf bytes.Buffer
ch := moveTablesTestHelper(org, svc, vtctldSvc, &buf)

cmd := MoveTablesCmd(ch)
cmd.SetArgs([]string{"create", db, branch,
"--workflow", "my-workflow",
"--target-keyspace", "target-ks",
"--source-keyspace", "source-ks",
"--auto-start=false",
})
err := cmd.Execute()
c.Assert(err, qt.IsNil)
c.Assert(svc.CreateFnInvoked, qt.IsTrue)
c.Assert(buf.String(), qt.JSONEquals, map[string]any{
"summary": "created",
"next_steps": []any{map[string]any{
"command": "pscale branch vtctld move-tables start my-db my-branch --org my-org --workflow my-workflow --target-keyspace target-ks --format json",
"reason": "Start the workflow after creating it with --auto-start=false",
}},
})
}

func TestMoveTablesSwitchTrafficWithMaxLag(t *testing.T) {
c := qt.New(t)
setMoveTablesPollInterval(t, 0)
Expand Down Expand Up @@ -764,3 +813,121 @@ func TestMoveTablesStatusAddsNextSteps(t *testing.T) {
}},
})
}

func TestMoveTablesStart(t *testing.T) {
c := qt.New(t)

org := "my-org"
db := "my-db"
branch := "my-branch"

vtctldSvc := &mock.VtctldService{
StartWorkflowFn: func(ctx context.Context, req *ps.VtctldStartWorkflowRequest) (json.RawMessage, error) {
c.Assert(req.Organization, qt.Equals, org)
c.Assert(req.Database, qt.Equals, db)
c.Assert(req.Branch, qt.Equals, branch)
c.Assert(req.Workflow, qt.Equals, "my-workflow")
c.Assert(req.Keyspace, qt.Equals, "target-ks")
return json.RawMessage(`{"summary":"started"}`), nil
},
}

var buf bytes.Buffer
ch := moveTablesTestHelper(org, nil, vtctldSvc, &buf)

cmd := MoveTablesCmd(ch)
cmd.SetArgs([]string{"start", db, branch,
"--workflow", "my-workflow",
"--target-keyspace", "target-ks",
})
err := cmd.Execute()
c.Assert(err, qt.IsNil)
c.Assert(vtctldSvc.StartWorkflowFnInvoked, qt.IsTrue)
c.Assert(buf.String(), qt.JSONEquals, map[string]any{
"summary": "started",
"next_steps": []any{map[string]any{
"command": "pscale branch vtctld move-tables status my-db my-branch --org my-org --workflow my-workflow --target-keyspace target-ks --format json",
"reason": "Monitor copy and replication progress",
}},
})
}

func TestMoveTablesStartRequiresFlags(t *testing.T) {
c := qt.New(t)

org := "my-org"
db := "my-db"
branch := "my-branch"

vtctldSvc := &mock.VtctldService{}
var buf bytes.Buffer
ch := moveTablesTestHelper(org, nil, vtctldSvc, &buf)

cmd := MoveTablesCmd(ch)
cmd.SetArgs([]string{"start", db, branch})
err := cmd.Execute()

c.Assert(err, qt.IsNotNil)
c.Assert(err.Error(), qt.Contains, "required flag")
c.Assert(vtctldSvc.StartWorkflowFnInvoked, qt.IsFalse)
c.Assert(buf.String(), qt.Equals, "")
}

func TestMoveTablesStop(t *testing.T) {
c := qt.New(t)

org := "my-org"
db := "my-db"
branch := "my-branch"

vtctldSvc := &mock.VtctldService{
StopWorkflowFn: func(ctx context.Context, req *ps.VtctldStopWorkflowRequest) (json.RawMessage, error) {
c.Assert(req.Organization, qt.Equals, org)
c.Assert(req.Database, qt.Equals, db)
c.Assert(req.Branch, qt.Equals, branch)
c.Assert(req.Workflow, qt.Equals, "my-workflow")
c.Assert(req.Keyspace, qt.Equals, "target-ks")
return json.RawMessage(`{"summary":"stopped"}`), nil
},
}

var buf bytes.Buffer
ch := moveTablesTestHelper(org, nil, vtctldSvc, &buf)

cmd := MoveTablesCmd(ch)
cmd.SetArgs([]string{"stop", db, branch,
"--workflow", "my-workflow",
"--target-keyspace", "target-ks",
})
err := cmd.Execute()
c.Assert(err, qt.IsNil)
c.Assert(vtctldSvc.StopWorkflowFnInvoked, qt.IsTrue)
c.Assert(buf.String(), qt.JSONEquals, map[string]any{
"summary": "stopped",
"next_steps": []any{map[string]any{
"command": "pscale branch vtctld move-tables start my-db my-branch --org my-org --workflow my-workflow --target-keyspace target-ks --format json",
"reason": "Resume the workflow when you are ready to continue",
}},
})
}

func TestMoveTablesStopRequiresFlags(t *testing.T) {
c := qt.New(t)

org := "my-org"
db := "my-db"
branch := "my-branch"

vtctldSvc := &mock.VtctldService{}
var buf bytes.Buffer
ch := moveTablesTestHelper(org, nil, vtctldSvc, &buf)

cmd := MoveTablesCmd(ch)
cmd.SetArgs([]string{"stop", db, branch, "--workflow", "my-workflow"})
err := cmd.Execute()

c.Assert(err, qt.IsNotNil)
c.Assert(err.Error(), qt.Contains, "required flag")
c.Assert(vtctldSvc.StopWorkflowFnInvoked, qt.IsFalse)
c.Assert(buf.String(), qt.Equals, "")
}
50 changes: 50 additions & 0 deletions internal/cmd/branch/vtctld/progress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,56 @@ func TestMoveTablesShowProgressIncludesOrganization(t *testing.T) {
c.Assert(progress.String(), qt.Contains, "Fetching MoveTables workflow my-workflow on my-org/my-db/my-branch…")
}

func TestMoveTablesStartProgressIncludesOrganization(t *testing.T) {
c := qt.New(t)
setNonTTYProgress(t)

org := "my-org"
db := "my-db"
branch := "my-branch"

svc := &mock.VtctldService{
StartWorkflowFn: func(ctx context.Context, req *ps.VtctldStartWorkflowRequest) (json.RawMessage, error) {
return json.RawMessage(`{"summary":"started"}`), nil
},
}

var progress bytes.Buffer
ch := newHumanProgressHelper(org, &progress, &ps.Client{Vtctld: svc})

cmd := MoveTablesCmd(ch)
cmd.SetArgs([]string{"start", db, branch, "--workflow", "my-workflow", "--target-keyspace", "target-ks"})

err := cmd.Execute()
c.Assert(err, qt.IsNil)
c.Assert(progress.String(), qt.Contains, "Starting MoveTables workflow my-workflow on my-org/my-db/my-branch…")
}

func TestMoveTablesStopProgressIncludesOrganization(t *testing.T) {
c := qt.New(t)
setNonTTYProgress(t)

org := "my-org"
db := "my-db"
branch := "my-branch"

svc := &mock.VtctldService{
StopWorkflowFn: func(ctx context.Context, req *ps.VtctldStopWorkflowRequest) (json.RawMessage, error) {
return json.RawMessage(`{"summary":"stopped"}`), nil
},
}

var progress bytes.Buffer
ch := newHumanProgressHelper(org, &progress, &ps.Client{Vtctld: svc})

cmd := MoveTablesCmd(ch)
cmd.SetArgs([]string{"stop", db, branch, "--workflow", "my-workflow", "--target-keyspace", "target-ks"})

err := cmd.Execute()
c.Assert(err, qt.IsNil)
c.Assert(progress.String(), qt.Contains, "Stopping MoveTables workflow my-workflow on my-org/my-db/my-branch…")
}

func TestVDiffListProgressIncludesOrganization(t *testing.T) {
c := qt.New(t)
setNonTTYProgress(t)
Expand Down
Loading
Loading