From 3f4b24bbc3974d826a8ad4877a759f92f396d1b2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 2 Oct 2026 18:32:28 +0000 Subject: [PATCH 1/5] Rename read-only replica CLI commands Co-authored-by: Nick Holden --- AGENTS.md | 33 ++++++------ internal/cmd/readonlyreplica/create.go | 14 +++--- internal/cmd/readonlyreplica/delete.go | 18 +++---- internal/cmd/readonlyreplica/list.go | 10 ++-- .../cmd/readonlyreplica/readonlyreplica.go | 15 +++--- .../readonlyreplica/readonlyreplica_test.go | 11 +++- internal/cmd/readonlyreplica/show.go | 10 ++-- internal/cmd/readonlyreplica/update.go | 20 ++++---- internal/cmd/role/get.go | 28 ++++++----- internal/cmd/role/get_test.go | 50 ++++++++++++++----- internal/cmd/root.go | 6 +-- .../postgres_read_only_replicas.go | 26 +++++----- internal/planetscale/postgres_roles_test.go | 2 +- 13 files changed, 142 insertions(+), 101 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index aeb6b307..3305d005 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -545,9 +545,9 @@ pscale role create --org --format json --inheri pscale role create --org --format json --inherited-roles neki_viewer,pg_read_all_data pscale role create --org --format json --inherited-roles neki_operator,postgres -# Role connection details for a branch replica, read-only replica, or PgBouncer +# Role connection details for a branch replica, dedicated read replica, or PgBouncer pscale role get --org --format json --replica -pscale role get --org --format json --read-only-replica +pscale role get --org --format json --dedicated-read-replica pscale role get --org --format json --bouncer # Neki: router is a username suffix; replica and shard are libpq options on the URL @@ -573,34 +573,35 @@ pscale branch resize cancel --org --format json - At least one of `--cluster-size`, `--replicas`, or `--parameters` is required. - Neki `--inherited-roles` may include `neki_viewer` (must also include `pg_read_all_data`) and `neki_operator` (must also include `postgres`). The API rejects those pairings if the required role is missing. -- `--replica`, `--read-only-replica`, and `--bouncer` are mutually exclusive. `--read-only-replica` and `--bouncer` are Postgres-only and cannot combine with `--router` or `--shard`. On Neki, `--replica`, `--shard`, and `--router` can be combined. `--router` rewrites `username` to `user|`. `--replica` and `--shard` set `options` (`-c __neki.target=REPLICA`, `-c __neki.shard=…`) and add them to `database_url`. List names with `pscale branch router list` and `pscale branch shard list` (use the shard name, not the API id). +- `--replica`, `--dedicated-read-replica`, and `--bouncer` are mutually exclusive. `--dedicated-read-replica` and `--bouncer` are Postgres-only and cannot combine with `--router` or `--shard`. The deprecated `--read-only-replica` flag is a compatibility alias for `--dedicated-read-replica`. On Neki, `--replica`, `--shard`, and `--router` can be combined. `--router` rewrites `username` to `user|`. `--replica` and `--shard` set `options` (`-c __neki.target=REPLICA`, `-c __neki.shard=…`) and add them to `database_url`. List names with `pscale branch router list` and `pscale branch shard list` (use the shard name, not the API id). - `--parameters` values are validated against the catalog before submission; unknown or immutable parameters fail fast. Parameters with `"restart": true` in the catalog restart the database when applied — surface this to the user before changing them. - Change request `state` is one of `queued`, `pending`, `resizing`, `completed`, `canceled`. Only `completed` and `canceled` are terminal. Without `--wait`, poll `resize status` instead of assuming completion. - A no-op (branch already matches the requested configuration) prints `{"result": "no_change", "branch": ""}` in JSON mode instead of a change request. - `resize cancel` prints `{"result": "canceled", "branch": ""}` in JSON mode. - MySQL databases are rejected: use `pscale keyspace resize` for Vitess keyspaces. -## Postgres read-only replicas +## Postgres dedicated read replicas -Read-only replicas provide dedicated regional capacity for Postgres queries -that can tolerate replication lag. They are separate from the replicas in the -primary branch cluster and from Vitess read-only regions. +Dedicated read replicas provide dedicated regional capacity for Postgres +queries that can tolerate replication lag. They are separate from the replicas +in the primary branch cluster and from Vitess read-only regions. ```bash -pscale read-only-replica list --org --format json -pscale read-only-replica show --org --format json -pscale read-only-replica create --region --org --format json -pscale read-only-replica create --region --replicas 2 --cluster-size PS_10_GCP_X86 --org --format json -pscale read-only-replica update --replicas 3 --org --format json -pscale read-only-replica update --cluster-size PS_20_GCP_X86 --parameters pgconf.max_connections=300 --org --format json -pscale read-only-replica delete --org --format json --force +pscale dedicated-read-replica list --org --format json +pscale dedicated-read-replica show --org --format json +pscale dedicated-read-replica create --region --org --format json +pscale dedicated-read-replica create --region --replicas 2 --cluster-size PS_10_GCP_X86 --org --format json +pscale dedicated-read-replica update --replicas 3 --org --format json +pscale dedicated-read-replica update --cluster-size PS_20_GCP_X86 --parameters pgconf.max_connections=300 --org --format json +pscale dedicated-read-replica delete --org --format json --force ``` - `create` requires a name and `--region`. The API defaults to one instance and the primary cluster size when `--replicas` and `--cluster-size` are omitted. -- `show`, `update`, and `delete` identify the read-only replica by name. +- `show`, `update`, and `delete` identify the dedicated read replica by name. - `update` requires at least one of `--replicas`, `--cluster-size`, or repeatable `--parameters namespace.name=value`. Parameter values must be greater than or equal to the primary's corresponding values. -- Creating and updating replicas is asynchronous; inspect `state` and `ready` in the returned object or with `list`. +- Creating and updating dedicated read replicas is asynchronous; inspect `state` and `ready` in the returned object or with `list`. - `delete` requires explicit approval before using `--force`. +- `read-only-replica` remains available as a backwards-compatible command alias. - PostgreSQL only. For Vitess/MySQL, use `pscale keyspace read-only-regions`. ## Postgres switchovers diff --git a/internal/cmd/readonlyreplica/create.go b/internal/cmd/readonlyreplica/create.go index 945d01fa..7c883d88 100644 --- a/internal/cmd/readonlyreplica/create.go +++ b/internal/cmd/readonlyreplica/create.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -// CreateCmd creates a read-only replica. +// CreateCmd creates a dedicated read replica. func CreateCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { region string @@ -19,8 +19,8 @@ func CreateCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "create ", - Short: "Create a read-only replica", - Long: `Create a read-only replica for a PostgreSQL database branch. + Short: "Create a dedicated read replica", + Long: `Create a dedicated read replica for a PostgreSQL database branch. Region is required. The replica count defaults to 1 and the cluster size defaults to the primary cluster size when those flags are omitted.`, @@ -33,7 +33,7 @@ defaults to the primary cluster size when those flags are omitted.`, if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } @@ -49,7 +49,7 @@ defaults to the primary cluster size when those flags are omitted.`, req.Replicas = &flags.replicas } - end := ch.Printer.PrintProgress(fmt.Sprintf("Creating read-only replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Creating dedicated read replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replica, err := client.PostgresReadOnlyReplicas.Create(ctx, req) @@ -65,7 +65,7 @@ defaults to the primary cluster size when those flags are omitted.`, end() if ch.Printer.Format() == printer.Human { - ch.Printer.Printf("Read-only replica %s is being created for %s/%s (state: %s).\n", + ch.Printer.Printf("Dedicated read replica %s is being created for %s/%s (state: %s).\n", printer.BoldBlue(replica.Name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(replica.State)) return nil } @@ -73,7 +73,7 @@ defaults to the primary cluster size when those flags are omitted.`, }, } - cmd.Flags().StringVar(&flags.region, "region", "", "Region slug for the read-only replica") + cmd.Flags().StringVar(&flags.region, "region", "", "Region slug for the dedicated read replica") cmd.Flags().IntVar(&flags.replicas, "replicas", 1, "Number of instances serving reads") cmd.Flags().StringVar(&flags.clusterSize, "cluster-size", "", "Cluster size SKU; defaults to the primary cluster size") cmd.MarkFlagRequired("region") // nolint:errcheck diff --git a/internal/cmd/readonlyreplica/delete.go b/internal/cmd/readonlyreplica/delete.go index bd5a06f7..c5156102 100644 --- a/internal/cmd/readonlyreplica/delete.go +++ b/internal/cmd/readonlyreplica/delete.go @@ -9,13 +9,13 @@ import ( "github.com/spf13/cobra" ) -// DeleteCmd deletes a read-only replica by name. +// DeleteCmd deletes a dedicated read replica by name. func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { var force bool cmd := &cobra.Command{ Use: "delete ", - Short: "Delete a read-only replica", + Short: "Delete a dedicated read replica", Args: cmdutil.RequiredArgs("database", "branch", "name"), Aliases: []string{"rm"}, RunE: func(cmd *cobra.Command, args []string) error { @@ -26,18 +26,18 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } if !force { confirmationName := fmt.Sprintf("%s/%s/%s", database, branch, name) - if err := ch.Printer.ConfirmCommand(confirmationName, "delete read-only replica", "deletion of read-only replica"); err != nil { + if err := ch.Printer.ConfirmCommand(confirmationName, "delete dedicated read replica", "deletion of dedicated read replica"); err != nil { return err } } - end := ch.Printer.PrintProgress(fmt.Sprintf("Deleting read-only replica %s from %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Deleting dedicated read replica %s from %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() err = client.PostgresReadOnlyReplicas.Delete(ctx, &ps.DeletePostgresReadOnlyReplicaRequest{ @@ -49,7 +49,7 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - return fmt.Errorf("read-only replica %s does not exist on %s/%s (organization: %s)", + return fmt.Errorf("dedicated read replica %s does not exist on %s/%s (organization: %s)", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization)) default: return cmdutil.HandleError(err) @@ -58,13 +58,13 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { end() if ch.Printer.Format() == printer.Human { - ch.Printer.Printf("Read-only replica %s was successfully deleted from %s/%s.\n", + ch.Printer.Printf("Dedicated read replica %s was successfully deleted from %s/%s.\n", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch)) return nil } return ch.Printer.PrintResource(map[string]string{ - "result": "read-only replica deleted", + "result": "dedicated read replica deleted", "name": name, "database": database, "branch": branch, @@ -72,6 +72,6 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { }, } - cmd.Flags().BoolVar(&force, "force", false, "Delete a read-only replica without confirmation") + cmd.Flags().BoolVar(&force, "force", false, "Delete a dedicated read replica without confirmation") return cmd } diff --git a/internal/cmd/readonlyreplica/list.go b/internal/cmd/readonlyreplica/list.go index 0aa62ea5..3928cd15 100644 --- a/internal/cmd/readonlyreplica/list.go +++ b/internal/cmd/readonlyreplica/list.go @@ -9,11 +9,11 @@ import ( "github.com/spf13/cobra" ) -// ListCmd lists read-only replicas for a Postgres branch. +// ListCmd lists dedicated read replicas for a Postgres branch. func ListCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "list ", - Short: "List read-only replicas for a Postgres branch", + Short: "List dedicated read replicas for a Postgres branch", Args: cmdutil.RequiredArgs("database", "branch"), Aliases: []string{"ls"}, RunE: func(cmd *cobra.Command, args []string) error { @@ -24,11 +24,11 @@ func ListCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } - end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching read-only replicas for %s/%s", printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching dedicated read replicas for %s/%s", printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replicas, err := client.PostgresReadOnlyReplicas.List(ctx, &ps.ListPostgresReadOnlyReplicasRequest{ @@ -48,7 +48,7 @@ func ListCmd(ch *cmdutil.Helper) *cobra.Command { end() if len(replicas) == 0 && ch.Printer.Format() == printer.Human { - ch.Printer.Printf("No read-only replicas exist for %s/%s.\n", printer.BoldBlue(database), printer.BoldBlue(branch)) + ch.Printer.Printf("No dedicated read replicas exist for %s/%s.\n", printer.BoldBlue(database), printer.BoldBlue(branch)) return nil } diff --git a/internal/cmd/readonlyreplica/readonlyreplica.go b/internal/cmd/readonlyreplica/readonlyreplica.go index 9bd290b0..698f5c54 100644 --- a/internal/cmd/readonlyreplica/readonlyreplica.go +++ b/internal/cmd/readonlyreplica/readonlyreplica.go @@ -9,15 +9,16 @@ import ( "github.com/spf13/cobra" ) -// Cmd manages read-only replicas for Postgres branches. +// Cmd manages dedicated read replicas for Postgres branches. func Cmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ - Use: "read-only-replica ", - Short: "Manage read-only replicas for a Postgres branch", - Long: `Manage read-only replicas for a PostgreSQL database branch. + Use: "dedicated-read-replica ", + Aliases: []string{"read-only-replica"}, + Short: "Manage dedicated read replicas for a Postgres branch", + Long: `Manage dedicated read replicas for a PostgreSQL database branch. -Read-only replicas provide dedicated capacity for queries that can tolerate -replication lag. They accept read traffic only. +Dedicated read replicas provide dedicated capacity for queries that can +tolerate replication lag. They accept read traffic only. This command is only available for PostgreSQL databases.`, PersistentPreRunE: cmdutil.CheckAuthentication(ch.Config), @@ -35,7 +36,7 @@ This command is only available for PostgreSQL databases.`, return cmd } -// ReadOnlyReplica is the human/JSON/CSV view of a Postgres read-only replica. +// ReadOnlyReplica is the human/JSON/CSV view of a Postgres dedicated read replica. type ReadOnlyReplica struct { ID string `header:"id" json:"id"` Name string `header:"name" json:"name"` diff --git a/internal/cmd/readonlyreplica/readonlyreplica_test.go b/internal/cmd/readonlyreplica/readonlyreplica_test.go index 636fbf1f..3ac329b9 100644 --- a/internal/cmd/readonlyreplica/readonlyreplica_test.go +++ b/internal/cmd/readonlyreplica/readonlyreplica_test.go @@ -62,6 +62,15 @@ func databaseService(c *qt.C, org, database string) *mock.DatabaseService { } } +func TestCmdUsesDedicatedReadReplicaName(t *testing.T) { + c := qt.New(t) + + cmd := Cmd(&cmdutil.Helper{Config: &config.Config{}}) + + c.Assert(cmd.Use, qt.Equals, "dedicated-read-replica ") + c.Assert(cmd.Aliases, qt.DeepEquals, []string{"read-only-replica"}) +} + func TestListCmd(t *testing.T) { c := qt.New(t) var buf bytes.Buffer @@ -192,7 +201,7 @@ func TestDeleteCmd(t *testing.T) { c.Assert(cmd.Execute(), qt.IsNil) c.Assert(svc.DeleteFnInvoked, qt.IsTrue) c.Assert(buf.String(), qt.JSONEquals, map[string]string{ - "result": "read-only replica deleted", + "result": "dedicated read replica deleted", "name": "analytics", "database": database, "branch": branch, diff --git a/internal/cmd/readonlyreplica/show.go b/internal/cmd/readonlyreplica/show.go index 72072334..84e65dfa 100644 --- a/internal/cmd/readonlyreplica/show.go +++ b/internal/cmd/readonlyreplica/show.go @@ -9,11 +9,11 @@ import ( "github.com/spf13/cobra" ) -// ShowCmd shows a read-only replica by name. +// ShowCmd shows a dedicated read replica by name. func ShowCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "show ", - Short: "Show a read-only replica", + Short: "Show a dedicated read replica", Args: cmdutil.RequiredArgs("database", "branch", "name"), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -23,11 +23,11 @@ func ShowCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } - end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching read-only replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Fetching dedicated read replica %s for %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replica, err := client.PostgresReadOnlyReplicas.Get(ctx, &ps.GetPostgresReadOnlyReplicaRequest{ @@ -39,7 +39,7 @@ func ShowCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - return fmt.Errorf("read-only replica %s does not exist on %s/%s (organization: %s)", + return fmt.Errorf("dedicated read replica %s does not exist on %s/%s (organization: %s)", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization)) default: return cmdutil.HandleError(err) diff --git a/internal/cmd/readonlyreplica/update.go b/internal/cmd/readonlyreplica/update.go index 77d88d92..a4c428bb 100644 --- a/internal/cmd/readonlyreplica/update.go +++ b/internal/cmd/readonlyreplica/update.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -// UpdateCmd updates a read-only replica. +// UpdateCmd updates a dedicated read replica. func UpdateCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { replicas int @@ -21,13 +21,13 @@ func UpdateCmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ Use: "update ", - Short: "Update a read-only replica", - Long: `Update a read-only replica's cluster size, instance count, and/or + Short: "Update a dedicated read replica", + Long: `Update a dedicated read replica's cluster size, instance count, and/or PostgreSQL configuration parameters. Parameter values must be greater than or equal to the primary branch's corresponding values.`, - Example: ` pscale read-only-replica update mydb main analytics --replicas 2 - pscale read-only-replica update mydb main analytics --cluster-size PS_20_GCP_X86 - pscale read-only-replica update mydb main analytics --parameters pgconf.max_connections=300`, + Example: ` pscale dedicated-read-replica update mydb main analytics --replicas 2 + pscale dedicated-read-replica update mydb main analytics --cluster-size PS_20_GCP_X86 + pscale dedicated-read-replica update mydb main analytics --parameters pgconf.max_connections=300`, Args: cmdutil.RequiredArgs("database", "branch", "name"), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -46,7 +46,7 @@ equal to the primary branch's corresponding values.`, if err != nil { return err } - if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "read-only replicas"); err != nil { + if err := cmdutil.RequirePostgresDatabase(ctx, client, ch.Config.Organization, database, "dedicated read replicas"); err != nil { return err } @@ -62,14 +62,14 @@ equal to the primary branch's corresponding values.`, req.Replicas = &flags.replicas } - end := ch.Printer.PrintProgress(fmt.Sprintf("Updating read-only replica %s on %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) + end := ch.Printer.PrintProgress(fmt.Sprintf("Updating dedicated read replica %s on %s/%s", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch))) defer end() replica, err := client.PostgresReadOnlyReplicas.Update(ctx, req) if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - return fmt.Errorf("read-only replica %s does not exist on %s/%s (organization: %s)", + return fmt.Errorf("dedicated read replica %s does not exist on %s/%s (organization: %s)", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(ch.Config.Organization)) default: return cmdutil.HandleError(err) @@ -78,7 +78,7 @@ equal to the primary branch's corresponding values.`, end() if ch.Printer.Format() == printer.Human { - ch.Printer.Printf("Update requested for read-only replica %s on %s/%s (state: %s).\n", + ch.Printer.Printf("Update requested for dedicated read replica %s on %s/%s (state: %s).\n", printer.BoldBlue(name), printer.BoldBlue(database), printer.BoldBlue(branch), printer.BoldBlue(replica.State)) return nil } diff --git a/internal/cmd/role/get.go b/internal/cmd/role/get.go index 763eaa1c..6b8ade36 100644 --- a/internal/cmd/role/get.go +++ b/internal/cmd/role/get.go @@ -13,11 +13,11 @@ import ( func GetCmd(ch *cmdutil.Helper) *cobra.Command { var flags struct { - replica bool - readOnlyReplica string - bouncer string - router string - shard string + replica bool + dedicatedReadReplica string + bouncer string + router string + shard string } cmd := &cobra.Command{ @@ -44,7 +44,7 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { Branch: branch, RoleId: roleID, Replica: flags.replica, - ReadOnlyReplica: flags.readOnlyReplica, + ReadOnlyReplica: flags.dedicatedReadReplica, Bouncer: flags.bouncer, Router: flags.router, Shard: flags.shard, @@ -52,7 +52,7 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { if err != nil { switch cmdutil.ErrCode(err) { case ps.ErrNotFound: - notFoundFormat, notFoundArgs := roleGetNotFound(roleID, branch, database, ch.Config.Organization, flags.readOnlyReplica, flags.bouncer, flags.router, flags.shard) + notFoundFormat, notFoundArgs := roleGetNotFound(roleID, branch, database, ch.Config.Organization, flags.dedicatedReadReplica, flags.bouncer, flags.router, flags.shard) return cmdutil.HandleNotFoundWithServiceTokenCheck( ctx, cmd, ch.Config, ch.Client, err, @@ -82,11 +82,15 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { } cmd.Flags().BoolVar(&flags.replica, "replica", false, "Return connection details for a branch replica. On Neki this sets libpq options, not a username suffix.") - cmd.Flags().StringVar(&flags.readOnlyReplica, "read-only-replica", "", "Return connection details for a read-only replica (name). Postgres only.") + cmd.Flags().StringVar(&flags.dedicatedReadReplica, "dedicated-read-replica", "", "Return connection details for a dedicated read replica (name). Postgres only.") + cmd.Flags().StringVar(&flags.dedicatedReadReplica, "read-only-replica", "", "Deprecated alias for --dedicated-read-replica.") + cmd.Flags().MarkDeprecated("read-only-replica", "use --dedicated-read-replica instead") // nolint:errcheck cmd.Flags().StringVar(&flags.bouncer, "bouncer", "", "Return connection details for a PgBouncer (name). Postgres only.") cmd.Flags().StringVar(&flags.router, "router", "", "Return connection details for a Neki router group (name). List routers with: pscale branch router list .") cmd.Flags().StringVar(&flags.shard, "shard", "", "Return connection details that pin a Neki shard (name from pscale branch shard list).") - cmd.MarkFlagsMutuallyExclusive("replica", "read-only-replica", "bouncer") + cmd.MarkFlagsMutuallyExclusive("replica", "dedicated-read-replica", "read-only-replica", "bouncer") + cmd.MarkFlagsMutuallyExclusive("dedicated-read-replica", "router") + cmd.MarkFlagsMutuallyExclusive("dedicated-read-replica", "shard") cmd.MarkFlagsMutuallyExclusive("read-only-replica", "router") cmd.MarkFlagsMutuallyExclusive("read-only-replica", "shard") cmd.MarkFlagsMutuallyExclusive("bouncer", "router") @@ -95,15 +99,15 @@ func GetCmd(ch *cmdutil.Helper) *cobra.Command { return cmd } -func roleGetNotFound(roleID, branch, database, org, readOnlyReplica, bouncer, router, shard string) (string, []any) { +func roleGetNotFound(roleID, branch, database, org, dedicatedReadReplica, bouncer, router, shard string) (string, []any) { type extra struct { label string value string } extras := make([]extra, 0, 4) - if readOnlyReplica != "" { - extras = append(extras, extra{"read-only replica", readOnlyReplica}) + if dedicatedReadReplica != "" { + extras = append(extras, extra{"dedicated read replica", dedicatedReadReplica}) } if bouncer != "" { extras = append(extras, extra{"PgBouncer", bouncer}) diff --git a/internal/cmd/role/get_test.go b/internal/cmd/role/get_test.go index 03a096f6..78166ba2 100644 --- a/internal/cmd/role/get_test.go +++ b/internal/cmd/role/get_test.go @@ -87,7 +87,17 @@ func TestRole_GetCmdConnectionTargets(t *testing.T) { databaseURL: "postgresql://app.branch%7Creplica:@primary.pg.psdb.cloud:5432/postgres?sslmode=verify-full", }, { - name: "read-only replica", + name: "dedicated read replica", + args: []string{"mydb", "main", "role-id", "--dedicated-read-replica", "analytics"}, + request: ps.GetPostgresRoleRequest{ + ReadOnlyReplica: "analytics", + }, + username: "app.read-only|replica", + accessHostURL: "analytics.pg.psdb.cloud", + databaseURL: "postgresql://app.read-only%7Creplica:@analytics.pg.psdb.cloud:5432/postgres?sslmode=verify-full", + }, + { + name: "deprecated read-only replica alias", args: []string{"mydb", "main", "role-id", "--read-only-replica", "analytics"}, request: ps.GetPostgresRoleRequest{ ReadOnlyReplica: "analytics", @@ -161,6 +171,15 @@ func TestRole_GetCmdConnectionTargets(t *testing.T) { } } +func TestRole_GetCmdDedicatedReadReplicaFlags(t *testing.T) { + c := qt.New(t) + + cmd := GetCmd(&cmdutil.Helper{}) + + c.Assert(cmd.Flags().Lookup("dedicated-read-replica").Deprecated, qt.Equals, "") + c.Assert(cmd.Flags().Lookup("read-only-replica").Deprecated, qt.Equals, "use --dedicated-read-replica instead") +} + func TestRole_GetCmdRejectsMultipleConnectionTargets(t *testing.T) { c := qt.New(t) @@ -172,16 +191,23 @@ func TestRole_GetCmdRejectsMultipleConnectionTargets(t *testing.T) { }, } - cmd := GetCmd(ch) - cmd.SetArgs([]string{"mydb", "main", "role-id", "--replica", "--bouncer", "pool"}) + tests := [][]string{ + {"mydb", "main", "role-id", "--replica", "--bouncer", "pool"}, + {"mydb", "main", "role-id", "--bouncer", "pool", "--router", "default"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--read-only-replica", "analytics"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--bouncer", "pool"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--router", "default"}, + {"mydb", "main", "role-id", "--dedicated-read-replica", "analytics", "--shard", "shzabc"}, + {"mydb", "main", "role-id", "--read-only-replica", "analytics", "--router", "default"}, + } - c.Assert(cmd.Execute(), qt.IsNotNil) - c.Assert(svc.GetFnInvoked, qt.IsFalse) + for _, args := range tests { + cmd := GetCmd(ch) + cmd.SetArgs(args) - cmd = GetCmd(ch) - cmd.SetArgs([]string{"mydb", "main", "role-id", "--bouncer", "pool", "--router", "default"}) - c.Assert(cmd.Execute(), qt.IsNotNil) - c.Assert(svc.GetFnInvoked, qt.IsFalse) + c.Assert(cmd.Execute(), qt.IsNotNil) + c.Assert(svc.GetFnInvoked, qt.IsFalse) + } } func TestRole_GetCmdNekiConnectionTargets(t *testing.T) { @@ -305,9 +331,9 @@ func TestRole_GetCmdConnectionTargetNotFound(t *testing.T) { target string }{ { - name: "read-only replica", - args: []string{"mydb", "main", "role-id", "--read-only-replica", "missing-replica"}, - targetType: "read-only replica", + name: "dedicated read replica", + args: []string{"mydb", "main", "role-id", "--dedicated-read-replica", "missing-replica"}, + targetType: "dedicated read replica", target: "missing-replica", }, { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index b230e1b3..8b92cc82 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -406,9 +406,9 @@ func runCmd(ctx context.Context, ver, commit, buildDate string, format *printer. pgbouncerCmd.GroupID = "postgres" rootCmd.AddCommand(pgbouncerCmd) - readOnlyReplicaCmd := readonlyreplica.Cmd(ch) - readOnlyReplicaCmd.GroupID = "postgres" - rootCmd.AddCommand(readOnlyReplicaCmd) + dedicatedReadReplicaCmd := readonlyreplica.Cmd(ch) + dedicatedReadReplicaCmd.GroupID = "postgres" + rootCmd.AddCommand(dedicatedReadReplicaCmd) trafficCmd := trafficcontrol.TrafficCmd(ch) trafficCmd.GroupID = "postgres" diff --git a/internal/planetscale/postgres_read_only_replicas.go b/internal/planetscale/postgres_read_only_replicas.go index c0714adc..24ee8726 100644 --- a/internal/planetscale/postgres_read_only_replicas.go +++ b/internal/planetscale/postgres_read_only_replicas.go @@ -8,7 +8,7 @@ import ( "time" ) -// PostgresReadOnlyReplica represents a read-only replica for a Postgres branch. +// PostgresReadOnlyReplica represents a dedicated read replica for a Postgres branch. type PostgresReadOnlyReplica struct { ID string `json:"id"` Name string `json:"name"` @@ -28,14 +28,14 @@ type PostgresReadOnlyReplica struct { Parameters []*PostgresParameter `json:"parameters"` } -// ListPostgresReadOnlyReplicasRequest encapsulates listing read-only replicas. +// ListPostgresReadOnlyReplicasRequest encapsulates listing dedicated read replicas. type ListPostgresReadOnlyReplicasRequest struct { Organization string Database string Branch string } -// GetPostgresReadOnlyReplicaRequest encapsulates getting a read-only replica by name. +// GetPostgresReadOnlyReplicaRequest encapsulates getting a dedicated read replica by name. type GetPostgresReadOnlyReplicaRequest struct { Organization string Database string @@ -43,7 +43,7 @@ type GetPostgresReadOnlyReplicaRequest struct { Replica string } -// CreatePostgresReadOnlyReplicaRequest encapsulates creating a read-only replica. +// CreatePostgresReadOnlyReplicaRequest encapsulates creating a dedicated read replica. type CreatePostgresReadOnlyReplicaRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -54,7 +54,7 @@ type CreatePostgresReadOnlyReplicaRequest struct { ClusterSize string `json:"cluster_size,omitempty"` } -// UpdatePostgresReadOnlyReplicaRequest encapsulates updating a read-only replica. +// UpdatePostgresReadOnlyReplicaRequest encapsulates updating a dedicated read replica. type UpdatePostgresReadOnlyReplicaRequest struct { Organization string `json:"-"` Database string `json:"-"` @@ -65,7 +65,7 @@ type UpdatePostgresReadOnlyReplicaRequest struct { Parameters map[string]map[string]string `json:"parameters,omitempty"` } -// DeletePostgresReadOnlyReplicaRequest encapsulates deleting a read-only replica. +// DeletePostgresReadOnlyReplicaRequest encapsulates deleting a dedicated read replica. type DeletePostgresReadOnlyReplicaRequest struct { Organization string Database string @@ -73,8 +73,8 @@ type DeletePostgresReadOnlyReplicaRequest struct { Replica string } -// PostgresReadOnlyReplicasService is an interface for the Postgres read-only -// replicas API. +// PostgresReadOnlyReplicasService is an interface for the Postgres dedicated +// read replicas API. type PostgresReadOnlyReplicasService interface { List(context.Context, *ListPostgresReadOnlyReplicasRequest) ([]*PostgresReadOnlyReplica, error) Get(context.Context, *GetPostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) @@ -92,7 +92,7 @@ var _ PostgresReadOnlyReplicasService = &postgresReadOnlyReplicasService{} func (s *postgresReadOnlyReplicasService) List(ctx context.Context, listReq *ListPostgresReadOnlyReplicasRequest) ([]*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodGet, postgresReadOnlyReplicasAPIPath(listReq.Organization, listReq.Database, listReq.Branch), nil) if err != nil { - return nil, fmt.Errorf("error creating request for list postgres read-only replicas: %w", err) + return nil, fmt.Errorf("error creating request for list postgres dedicated read replicas: %w", err) } replicas := []*PostgresReadOnlyReplica{} @@ -105,7 +105,7 @@ func (s *postgresReadOnlyReplicasService) List(ctx context.Context, listReq *Lis func (s *postgresReadOnlyReplicasService) Get(ctx context.Context, getReq *GetPostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodGet, postgresReadOnlyReplicaAPIPath(getReq.Organization, getReq.Database, getReq.Branch, getReq.Replica), nil) if err != nil { - return nil, fmt.Errorf("error creating request for get postgres read-only replica: %w", err) + return nil, fmt.Errorf("error creating request for get postgres dedicated read replica: %w", err) } replica := &PostgresReadOnlyReplica{} @@ -118,7 +118,7 @@ func (s *postgresReadOnlyReplicasService) Get(ctx context.Context, getReq *GetPo func (s *postgresReadOnlyReplicasService) Create(ctx context.Context, createReq *CreatePostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodPost, postgresReadOnlyReplicasAPIPath(createReq.Organization, createReq.Database, createReq.Branch), createReq) if err != nil { - return nil, fmt.Errorf("error creating request for create postgres read-only replica: %w", err) + return nil, fmt.Errorf("error creating request for create postgres dedicated read replica: %w", err) } replica := &PostgresReadOnlyReplica{} @@ -131,7 +131,7 @@ func (s *postgresReadOnlyReplicasService) Create(ctx context.Context, createReq func (s *postgresReadOnlyReplicasService) Update(ctx context.Context, updateReq *UpdatePostgresReadOnlyReplicaRequest) (*PostgresReadOnlyReplica, error) { req, err := s.client.newRequest(http.MethodPatch, postgresReadOnlyReplicaAPIPath(updateReq.Organization, updateReq.Database, updateReq.Branch, updateReq.Replica), updateReq) if err != nil { - return nil, fmt.Errorf("error creating request for update postgres read-only replica: %w", err) + return nil, fmt.Errorf("error creating request for update postgres dedicated read replica: %w", err) } replica := &PostgresReadOnlyReplica{} @@ -144,7 +144,7 @@ func (s *postgresReadOnlyReplicasService) Update(ctx context.Context, updateReq func (s *postgresReadOnlyReplicasService) Delete(ctx context.Context, deleteReq *DeletePostgresReadOnlyReplicaRequest) error { req, err := s.client.newRequest(http.MethodDelete, postgresReadOnlyReplicaAPIPath(deleteReq.Organization, deleteReq.Database, deleteReq.Branch, deleteReq.Replica), nil) if err != nil { - return fmt.Errorf("error creating request for delete postgres read-only replica: %w", err) + return fmt.Errorf("error creating request for delete postgres dedicated read replica: %w", err) } return s.client.do(ctx, req, nil) } diff --git a/internal/planetscale/postgres_roles_test.go b/internal/planetscale/postgres_roles_test.go index b246a226..c905fb67 100644 --- a/internal/planetscale/postgres_roles_test.go +++ b/internal/planetscale/postgres_roles_test.go @@ -398,7 +398,7 @@ func TestPostgresRoles_GetConnectionTargets(t *testing.T) { accessHost: "primary.planetscale.com", }, { - name: "read-only replica", + name: "dedicated read replica", request: GetPostgresRoleRequest{ReadOnlyReplica: "analytics"}, query: url.Values{"read_only_replica": []string{"analytics"}}, username: "test-user.replica-id|replica", From a7c8a25742307a3e50626a9913f98c532cb12efe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 2 Oct 2026 18:42:19 +0000 Subject: [PATCH 2/5] Preserve delete JSON compatibility Co-authored-by: Nick Holden --- internal/cmd/readonlyreplica/delete.go | 2 +- internal/cmd/readonlyreplica/readonlyreplica_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cmd/readonlyreplica/delete.go b/internal/cmd/readonlyreplica/delete.go index c5156102..f3a4132c 100644 --- a/internal/cmd/readonlyreplica/delete.go +++ b/internal/cmd/readonlyreplica/delete.go @@ -64,7 +64,7 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { } return ch.Printer.PrintResource(map[string]string{ - "result": "dedicated read replica deleted", + "result": "read-only replica deleted", "name": name, "database": database, "branch": branch, diff --git a/internal/cmd/readonlyreplica/readonlyreplica_test.go b/internal/cmd/readonlyreplica/readonlyreplica_test.go index 3ac329b9..104dbbce 100644 --- a/internal/cmd/readonlyreplica/readonlyreplica_test.go +++ b/internal/cmd/readonlyreplica/readonlyreplica_test.go @@ -201,7 +201,7 @@ func TestDeleteCmd(t *testing.T) { c.Assert(cmd.Execute(), qt.IsNil) c.Assert(svc.DeleteFnInvoked, qt.IsTrue) c.Assert(buf.String(), qt.JSONEquals, map[string]string{ - "result": "dedicated read replica deleted", + "result": "read-only replica deleted", "name": "analytics", "database": database, "branch": branch, From e8ccf7239d8f85e5a8907c58adbcf7e4e73c7bf9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 2 Oct 2026 20:18:00 +0000 Subject: [PATCH 3/5] Move dedicated replica command package Co-authored-by: Nick Holden --- .../cmd/{readonlyreplica => dedicatedreadreplica}/create.go | 2 +- .../dedicatedreadreplica.go} | 2 +- .../dedicatedreadreplica_test.go} | 2 +- .../cmd/{readonlyreplica => dedicatedreadreplica}/delete.go | 2 +- .../cmd/{readonlyreplica => dedicatedreadreplica}/list.go | 2 +- .../cmd/{readonlyreplica => dedicatedreadreplica}/show.go | 2 +- .../cmd/{readonlyreplica => dedicatedreadreplica}/update.go | 2 +- internal/cmd/root.go | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) rename internal/cmd/{readonlyreplica => dedicatedreadreplica}/create.go (99%) rename internal/cmd/{readonlyreplica/readonlyreplica.go => dedicatedreadreplica/dedicatedreadreplica.go} (98%) rename internal/cmd/{readonlyreplica/readonlyreplica_test.go => dedicatedreadreplica/dedicatedreadreplica_test.go} (99%) rename internal/cmd/{readonlyreplica => dedicatedreadreplica}/delete.go (98%) rename internal/cmd/{readonlyreplica => dedicatedreadreplica}/list.go (98%) rename internal/cmd/{readonlyreplica => dedicatedreadreplica}/show.go (98%) rename internal/cmd/{readonlyreplica => dedicatedreadreplica}/update.go (99%) diff --git a/internal/cmd/readonlyreplica/create.go b/internal/cmd/dedicatedreadreplica/create.go similarity index 99% rename from internal/cmd/readonlyreplica/create.go rename to internal/cmd/dedicatedreadreplica/create.go index 7c883d88..d27e9552 100644 --- a/internal/cmd/readonlyreplica/create.go +++ b/internal/cmd/dedicatedreadreplica/create.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" diff --git a/internal/cmd/readonlyreplica/readonlyreplica.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go similarity index 98% rename from internal/cmd/readonlyreplica/readonlyreplica.go rename to internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go index 698f5c54..59b435d7 100644 --- a/internal/cmd/readonlyreplica/readonlyreplica.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "encoding/json" diff --git a/internal/cmd/readonlyreplica/readonlyreplica_test.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go similarity index 99% rename from internal/cmd/readonlyreplica/readonlyreplica_test.go rename to internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go index 104dbbce..8c6f8359 100644 --- a/internal/cmd/readonlyreplica/readonlyreplica_test.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "bytes" diff --git a/internal/cmd/readonlyreplica/delete.go b/internal/cmd/dedicatedreadreplica/delete.go similarity index 98% rename from internal/cmd/readonlyreplica/delete.go rename to internal/cmd/dedicatedreadreplica/delete.go index f3a4132c..08e7f245 100644 --- a/internal/cmd/readonlyreplica/delete.go +++ b/internal/cmd/dedicatedreadreplica/delete.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" diff --git a/internal/cmd/readonlyreplica/list.go b/internal/cmd/dedicatedreadreplica/list.go similarity index 98% rename from internal/cmd/readonlyreplica/list.go rename to internal/cmd/dedicatedreadreplica/list.go index 3928cd15..7c431559 100644 --- a/internal/cmd/readonlyreplica/list.go +++ b/internal/cmd/dedicatedreadreplica/list.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" diff --git a/internal/cmd/readonlyreplica/show.go b/internal/cmd/dedicatedreadreplica/show.go similarity index 98% rename from internal/cmd/readonlyreplica/show.go rename to internal/cmd/dedicatedreadreplica/show.go index 84e65dfa..adf86073 100644 --- a/internal/cmd/readonlyreplica/show.go +++ b/internal/cmd/dedicatedreadreplica/show.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "fmt" diff --git a/internal/cmd/readonlyreplica/update.go b/internal/cmd/dedicatedreadreplica/update.go similarity index 99% rename from internal/cmd/readonlyreplica/update.go rename to internal/cmd/dedicatedreadreplica/update.go index a4c428bb..a5a85495 100644 --- a/internal/cmd/readonlyreplica/update.go +++ b/internal/cmd/dedicatedreadreplica/update.go @@ -1,4 +1,4 @@ -package readonlyreplica +package dedicatedreadreplica import ( "errors" diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 8b92cc82..eb26325c 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -43,6 +43,7 @@ import ( "github.com/planetscale/cli/internal/cmd/branch" "github.com/planetscale/cli/internal/cmd/connect" "github.com/planetscale/cli/internal/cmd/database" + "github.com/planetscale/cli/internal/cmd/dedicatedreadreplica" "github.com/planetscale/cli/internal/cmd/deployrequest" "github.com/planetscale/cli/internal/cmd/importcmd" "github.com/planetscale/cli/internal/cmd/insights" @@ -55,7 +56,6 @@ import ( "github.com/planetscale/cli/internal/cmd/password" "github.com/planetscale/cli/internal/cmd/pgbouncer" "github.com/planetscale/cli/internal/cmd/ping" - "github.com/planetscale/cli/internal/cmd/readonlyreplica" "github.com/planetscale/cli/internal/cmd/region" "github.com/planetscale/cli/internal/cmd/shell" "github.com/planetscale/cli/internal/cmd/signup" @@ -406,7 +406,7 @@ func runCmd(ctx context.Context, ver, commit, buildDate string, format *printer. pgbouncerCmd.GroupID = "postgres" rootCmd.AddCommand(pgbouncerCmd) - dedicatedReadReplicaCmd := readonlyreplica.Cmd(ch) + dedicatedReadReplicaCmd := dedicatedreadreplica.Cmd(ch) dedicatedReadReplicaCmd.GroupID = "postgres" rootCmd.AddCommand(dedicatedReadReplicaCmd) From 2ffedde9bf8dd0a6dc88968a420004d668dda12c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 2 Oct 2026 20:33:01 +0000 Subject: [PATCH 4/5] Hide deprecated replica command Co-authored-by: Nick Holden --- .../dedicatedreadreplica.go | 18 +++++++++++++++--- .../dedicatedreadreplica_test.go | 12 +++++++++++- internal/cmd/root.go | 1 + 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go index 59b435d7..1b7f1aca 100644 --- a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go @@ -11,10 +11,22 @@ import ( // Cmd manages dedicated read replicas for Postgres branches. func Cmd(ch *cmdutil.Helper) *cobra.Command { + return cmd(ch) +} + +// DeprecatedCmd preserves the previous command name without advertising it. +func DeprecatedCmd(ch *cmdutil.Helper) *cobra.Command { + cmd := cmd(ch) + cmd.Use = "read-only-replica " + cmd.Hidden = true + cmd.Deprecated = "use dedicated-read-replica instead" + return cmd +} + +func cmd(ch *cmdutil.Helper) *cobra.Command { cmd := &cobra.Command{ - Use: "dedicated-read-replica ", - Aliases: []string{"read-only-replica"}, - Short: "Manage dedicated read replicas for a Postgres branch", + Use: "dedicated-read-replica ", + Short: "Manage dedicated read replicas for a Postgres branch", Long: `Manage dedicated read replicas for a PostgreSQL database branch. Dedicated read replicas provide dedicated capacity for queries that can diff --git a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go index 8c6f8359..09bef736 100644 --- a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go @@ -68,7 +68,17 @@ func TestCmdUsesDedicatedReadReplicaName(t *testing.T) { cmd := Cmd(&cmdutil.Helper{Config: &config.Config{}}) c.Assert(cmd.Use, qt.Equals, "dedicated-read-replica ") - c.Assert(cmd.Aliases, qt.DeepEquals, []string{"read-only-replica"}) + c.Assert(cmd.Aliases, qt.HasLen, 0) +} + +func TestDeprecatedCmdIsHidden(t *testing.T) { + c := qt.New(t) + + cmd := DeprecatedCmd(&cmdutil.Helper{Config: &config.Config{}}) + + c.Assert(cmd.Use, qt.Equals, "read-only-replica ") + c.Assert(cmd.Hidden, qt.IsTrue) + c.Assert(cmd.Deprecated, qt.Equals, "use dedicated-read-replica instead") } func TestListCmd(t *testing.T) { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index eb26325c..bdac8678 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -409,6 +409,7 @@ func runCmd(ctx context.Context, ver, commit, buildDate string, format *printer. dedicatedReadReplicaCmd := dedicatedreadreplica.Cmd(ch) dedicatedReadReplicaCmd.GroupID = "postgres" rootCmd.AddCommand(dedicatedReadReplicaCmd) + rootCmd.AddCommand(dedicatedreadreplica.DeprecatedCmd(ch)) trafficCmd := trafficcontrol.TrafficCmd(ch) trafficCmd.GroupID = "postgres" From a3c5bae7238b95231a868413e81f93d35d52dbde Mon Sep 17 00:00:00 2001 From: Nick Holden Date: Fri, 2 Oct 2026 15:58:40 -0700 Subject: [PATCH 5/5] Warn on deprecated read-only-replica subcommands Cobra only prints a command's Deprecated message when that command itself runs, so `pscale read-only-replica list` and the other subcommands were silent. Wire cmdutil.WarnDeprecated into the alias's PersistentPreRunE, matching the deprecated workflow command, so every subcommand warns on stderr while stdout stays unchanged. Also use the new "dedicated read replica deleted" result string in the delete command's JSON output. Co-Authored-By: Claude Fable 5.1 --- AGENTS.md | 2 +- .../dedicatedreadreplica.go | 11 +++++-- .../dedicatedreadreplica_test.go | 29 ++++++++++++++++++- internal/cmd/dedicatedreadreplica/delete.go | 2 +- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3305d005..a544e4fa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -601,7 +601,7 @@ pscale dedicated-read-replica delete --org --fo - `update` requires at least one of `--replicas`, `--cluster-size`, or repeatable `--parameters namespace.name=value`. Parameter values must be greater than or equal to the primary's corresponding values. - Creating and updating dedicated read replicas is asynchronous; inspect `state` and `ready` in the returned object or with `list`. - `delete` requires explicit approval before using `--force`. -- `read-only-replica` remains available as a backwards-compatible command alias. +- `read-only-replica` remains available as a backwards-compatible command alias. It prints a deprecation warning on stderr; stdout is unchanged. - PostgreSQL only. For Vitess/MySQL, use `pscale keyspace read-only-regions`. ## Postgres switchovers diff --git a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go index 1b7f1aca..015316fe 100644 --- a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica.go @@ -14,12 +14,19 @@ func Cmd(ch *cmdutil.Helper) *cobra.Command { return cmd(ch) } +const deprecatedName = "read-only-replica" + +const deprecationMessage = "use dedicated-read-replica instead" + // DeprecatedCmd preserves the previous command name without advertising it. +// Cobra only prints Deprecated for the command that runs, so subcommands warn +// through PersistentPreRunE the same way the deprecated workflow command does. func DeprecatedCmd(ch *cmdutil.Helper) *cobra.Command { cmd := cmd(ch) - cmd.Use = "read-only-replica " + cmd.Use = deprecatedName + " " cmd.Hidden = true - cmd.Deprecated = "use dedicated-read-replica instead" + cmd.Deprecated = deprecationMessage + cmd.PersistentPreRunE = cmdutil.WarnDeprecated(deprecatedName, deprecationMessage, cmdutil.CheckAuthentication(ch.Config)) return cmd } diff --git a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go index 09bef736..08534f44 100644 --- a/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go +++ b/internal/cmd/dedicatedreadreplica/dedicatedreadreplica_test.go @@ -3,6 +3,7 @@ package dedicatedreadreplica import ( "bytes" "context" + "encoding/json" "testing" "time" @@ -81,6 +82,32 @@ func TestDeprecatedCmdIsHidden(t *testing.T) { c.Assert(cmd.Deprecated, qt.Equals, "use dedicated-read-replica instead") } +func TestDeprecatedCmdWarnsOnSubcommand(t *testing.T) { + c := qt.New(t) + var stdout, stderr bytes.Buffer + org, database, branch := "planetscale", "mydb", "main" + svc := &mock.PostgresReadOnlyReplicasService{ + ListFn: func(ctx context.Context, req *ps.ListPostgresReadOnlyReplicasRequest) ([]*ps.PostgresReadOnlyReplica, error) { + return []*ps.PostgresReadOnlyReplica{testReplica()}, nil + }, + } + + ch := testHelper(org, databaseService(c, org, database), svc, printer.JSON, &stdout) + ch.Config.AccessToken = "token" + + cmd := DeprecatedCmd(ch) + cmd.SetErr(&stderr) + cmd.SetArgs([]string{"list", database, branch, "--org", org}) + c.Assert(cmd.Execute(), qt.IsNil) + c.Assert(svc.ListFnInvoked, qt.IsTrue) + c.Assert(stderr.String(), qt.Equals, "Command \"read-only-replica\" is deprecated, use dedicated-read-replica instead\n") + + var got []map[string]any + c.Assert(json.Unmarshal(stdout.Bytes(), &got), qt.IsNil) + c.Assert(got, qt.HasLen, 1) + c.Assert(got[0]["name"], qt.Equals, "analytics") +} + func TestListCmd(t *testing.T) { c := qt.New(t) var buf bytes.Buffer @@ -211,7 +238,7 @@ func TestDeleteCmd(t *testing.T) { c.Assert(cmd.Execute(), qt.IsNil) c.Assert(svc.DeleteFnInvoked, qt.IsTrue) c.Assert(buf.String(), qt.JSONEquals, map[string]string{ - "result": "read-only replica deleted", + "result": "dedicated read replica deleted", "name": "analytics", "database": database, "branch": branch, diff --git a/internal/cmd/dedicatedreadreplica/delete.go b/internal/cmd/dedicatedreadreplica/delete.go index 08e7f245..6857bae2 100644 --- a/internal/cmd/dedicatedreadreplica/delete.go +++ b/internal/cmd/dedicatedreadreplica/delete.go @@ -64,7 +64,7 @@ func DeleteCmd(ch *cmdutil.Helper) *cobra.Command { } return ch.Printer.PrintResource(map[string]string{ - "result": "read-only replica deleted", + "result": "dedicated read replica deleted", "name": name, "database": database, "branch": branch,