66 "encoding/base64"
77 "errors"
88 "fmt"
9- "net/http"
109 "os"
1110 "os/exec"
1211 "strings"
@@ -56,6 +55,8 @@ The service ID can be provided as an argument or will use the default service
5655from your configuration. The connection string includes all necessary parameters
5756for establishing a database connection to the TimescaleDB/PostgreSQL service.
5857
58+ You can also pass a read replica set ID to get a connection string for that replica.
59+
5960By default, passwords are excluded from the connection string for security.
6061Use --with-password to include the password directly in the connection string.
6162
@@ -92,29 +93,25 @@ Examples:
9293 return err
9394 }
9495
95- service , err := getServiceDetailsFunc (cmd , cfg , args )
96+ target , err := lookupConnectionTarget (cmd , cfg , args )
9697 if err != nil {
9798 return err
9899 }
99100
100- details , err := common . GetConnectionDetails ( service , common.ConnectionDetailsOptions {
101+ details , err := buildConnectionDetailsForTarget ( cmd , target , common.ConnectionDetailsOptions {
101102 Pooled : dbConnectionStringPooled ,
102103 Role : dbConnectionStringRole ,
103104 WithPassword : dbConnectionStringWithPassword ,
104105 ReadOnly : dbConnectionStringReadOnly || cfg .ReadOnly ,
105106 })
106107 if err != nil {
107- return fmt . Errorf ( "failed to build connection string: %w" , err )
108+ return err
108109 }
109110
110111 if dbConnectionStringWithPassword && details .Password == "" {
111112 return fmt .Errorf ("password not available to include in connection string" )
112113 }
113114
114- if err := details .RequirePooler (dbConnectionStringPooled ); err != nil {
115- return err
116- }
117-
118115 fmt .Fprintln (cmd .OutOrStdout (), details .String ())
119116 return nil
120117 },
@@ -163,11 +160,17 @@ primary. Use --no-replica-prompt to skip this prompt and always connect to the
163160requested service. The prompt is automatically skipped when stdin is not a
164161terminal (e.g. in scripts) or when the service has no read replicas.
165162
163+ You can also pass a read replica set ID to connect straight to that replica,
164+ skipping the prompt. Read replicas share the primary's credentials.
165+
166166Examples:
167167 # Connect to default service
168168 tiger db connect
169169 tiger db psql
170170
171+ # Connect directly to a read replica by its ID
172+ tiger db connect rep1234567
173+
171174 # Connect without the read replica prompt
172175 tiger db connect svc-12345 --no-replica-prompt
173176
@@ -202,7 +205,7 @@ Examples:
202205 // Separate service ID from additional psql flags
203206 serviceArgs , psqlFlags := separateServiceAndPsqlArgs (cmd , args )
204207
205- service , err := getServiceDetailsFunc (cmd , cfg , serviceArgs )
208+ target , err := lookupConnectionTarget (cmd , cfg , serviceArgs )
206209 if err != nil {
207210 return err
208211 }
@@ -219,23 +222,19 @@ Examples:
219222 ReadOnly : dbConnectReadOnly || cfg .ReadOnly ,
220223 }
221224
222- // Optionally offer to connect to an existing read replica instead of
223- // the primary service. In non-interactive contexts, or when the
224- // service has no read replicas, this returns the primary's details
225- // without prompting. Pooler availability is validated here: a hard
226- // error for the primary, warn-and-fall-back for replicas.
227- details , err := resolveConnectTarget (cmd .Context (), cmd , cfg .Client , cfg .ProjectID , service , opts , dbConnectNoReplicaPrompt )
225+ // Connects straight to a replica named by ID, or offers the interactive
226+ // replica menu for a primary. Returns nil details if the user cancels.
227+ details , err := selectConnection (cmd .Context (), cmd , cfg .Client , cfg .ProjectID , target , opts , dbConnectNoReplicaPrompt )
228228 if err != nil {
229229 return err
230230 }
231231 if details == nil {
232- // User cancelled the connection.
233232 return nil
234233 }
235234
236- // Replicas share the primary's credentials, so password storage and
237- // recovery always operate on the primary service.
238- return connectWithPasswordMenu (cmd .Context (), cmd , cfg .Client , service , details , psqlPath , psqlFlags )
235+ // Read replicas share the primary's credentials, so password storage
236+ // and recovery always operate on the credential service.
237+ return connectWithPasswordMenu (cmd .Context (), cmd , cfg .Client , target . CredentialService , details , psqlPath , psqlFlags )
239238 },
240239 }
241240
@@ -262,6 +261,8 @@ The service ID can be provided as an argument or will use the default service
262261from your configuration. This command tests if the database is accepting
263262connections and returns appropriate exit codes following pg_isready conventions.
264263
264+ You can also pass a read replica set ID to test connectivity to that replica.
265+
265266Return Codes:
266267 0: Server is accepting connections normally
267268 1: Server is rejecting connections (e.g., during startup)
@@ -292,22 +293,18 @@ Examples:
292293 return common .ExitWithCode (common .ExitInvalidParameters , err )
293294 }
294295
295- service , err := getServiceDetailsFunc (cmd , cfg , args )
296+ target , err := lookupConnectionTarget (cmd , cfg , args )
296297 if err != nil {
297298 return common .ExitWithCode (common .ExitInvalidParameters , err )
298299 }
299300
300301 // Build connection string for testing with password (if available)
301- details , err := common . GetConnectionDetails ( service , common.ConnectionDetailsOptions {
302+ details , err := buildConnectionDetailsForTarget ( cmd , target , common.ConnectionDetailsOptions {
302303 Pooled : dbTestConnectionPooled ,
303304 Role : dbTestConnectionRole ,
304305 WithPassword : true ,
305306 })
306307 if err != nil {
307- return common .ExitWithCode (common .ExitInvalidParameters , fmt .Errorf ("failed to build connection string: %w" , err ))
308- }
309-
310- if err := details .RequirePooler (dbTestConnectionPooled ); err != nil {
311308 return common .ExitWithCode (common .ExitInvalidParameters , err )
312309 }
313310
@@ -372,10 +369,14 @@ Examples:
372369 return err
373370 }
374371
375- service , err := getServiceDetailsFunc (cmd , cfg , args )
372+ // Resolve the target so a read replica id stores the password against
373+ // its parent primary: replicas share the primary's credentials, and
374+ // connect/test-connection look the password up against the primary.
375+ target , err := lookupConnectionTarget (cmd , cfg , args )
376376 if err != nil {
377377 return err
378378 }
379+ service := target .CredentialService
379380
380381 // Determine password based on precedence:
381382 // 1. --password flag with value
@@ -415,6 +416,10 @@ Examples:
415416 return fmt .Errorf ("failed to save password: %w" , err )
416417 }
417418
419+ if target .IsReplica {
420+ fmt .Fprintf (cmd .ErrOrStderr (), "Read replicas share the primary's credentials; saving against primary %s.\n " ,
421+ * service .ServiceId )
422+ }
418423 fmt .Fprintf (cmd .ErrOrStderr (), "Password saved successfully for service %s (role: %s)\n " ,
419424 * service .ServiceId , dbSavePasswordRole )
420425 return nil
@@ -653,7 +658,8 @@ func buildDbCreateRoleCmd() *cobra.Command {
653658 Long : `Create a new database role with optional read-only enforcement.
654659
655660The service ID can be provided as an argument or will use the default service
656- from your configuration.
661+ from your configuration. A read replica ID is rejected, since replicas are
662+ read-only; create the role on the primary instead.
657663
658664By default, a secure random password is auto-generated for the new role. You can:
659665- Provide an explicit password with --password=<value>
@@ -727,6 +733,12 @@ PostgreSQL Configuration Parameters That May Be Set:
727733 return err
728734 }
729735
736+ // A read replica is read-only, so a role can't be created there.
737+ if common .IsReadReplica (service ) {
738+ return fmt .Errorf ("%q is a read replica; create the role on its primary service %q instead" ,
739+ util .Deref (service .ServiceId ), util .DerefStr (service .ForkedFrom .ServiceId ))
740+ }
741+
730742 // Get password
731743 rolePassword , err := getPasswordForRole (passwordFlag )
732744 if err != nil {
@@ -813,8 +825,9 @@ indexes, triggers, and TimescaleDB hypertable and continuous aggregate
813825metadata.
814826
815827The service ID can be provided as an argument or will use the default service
816- from your configuration. Only objects the connecting role can access are
817- returned. The connection is opened in Tiger Cloud's immutable read-only mode.
828+ from your configuration. You can also pass a read replica set ID to introspect
829+ that replica. Only objects the connecting role can access are returned. The
830+ connection is opened in Tiger Cloud's immutable read-only mode.
818831
819832By default only user-facing schemas and objects are shown. View and routine
820833definitions and object comments are omitted unless requested, since they can be
@@ -844,12 +857,14 @@ Examples:
844857 return err
845858 }
846859
847- service , err := getServiceDetailsFunc (cmd , cfg , args )
860+ target , err := lookupConnectionTarget (cmd , cfg , args )
848861 if err != nil {
849862 return err
850863 }
851864
852- schema , err := common .FetchServiceSchema (cmd .Context (), service , dbSchemaRole , dbSchemaPooled , common.SchemaOptions {
865+ warnReplicaPooler (cmd , target , dbSchemaPooled )
866+
867+ schema , err := common .FetchServiceSchema (cmd .Context (), target , dbSchemaRole , dbSchemaPooled , common.SchemaOptions {
853868 Schema : dbSchemaSchema ,
854869 IncludeInternal : dbSchemaInternal ,
855870 IncludeDefinitions : dbSchemaDefinitions ,
@@ -891,6 +906,37 @@ func buildDbCmd() *cobra.Command {
891906 return cmd
892907}
893908
909+ // lookupConnectionTarget looks up the target named by args, which may be a
910+ // primary service ID or a read replica set ID. This lets a replica ID work
911+ // anywhere a service ID does across the db connection commands.
912+ func lookupConnectionTarget (cmd * cobra.Command , cfg * common.Config , args []string ) (* common.ConnectionTarget , error ) {
913+ service , err := getServiceDetailsFunc (cmd , cfg , args )
914+ if err != nil {
915+ return nil , err
916+ }
917+
918+ // The API resolves both primary and read replica IDs via GetService; a read
919+ // replica comes back linked to its parent, whose credentials it shares.
920+ ctx , cancel := context .WithTimeout (cmd .Context (), 30 * time .Second )
921+ defer cancel ()
922+ return common .ResolveConnectionTarget (ctx , cfg .Client , cfg .ProjectID , service )
923+ }
924+
925+ // warnReplicaPooler prints the replica pooler-fallback warning to stderr, if
926+ // any. It is a no-op for a primary target or when there's nothing to warn.
927+ func warnReplicaPooler (cmd * cobra.Command , target * common.ConnectionTarget , pooled bool ) {
928+ if warning := common .ReplicaPoolerWarning (target , pooled ); warning != "" {
929+ fmt .Fprintf (cmd .ErrOrStderr (), "⚠️ Warning: %s\n " , warning )
930+ }
931+ }
932+
933+ // buildConnectionDetailsForTarget builds connection details for a target,
934+ // warning first when a replica falls back from a requested pooler.
935+ func buildConnectionDetailsForTarget (cmd * cobra.Command , target * common.ConnectionTarget , opts common.ConnectionDetailsOptions ) (* common.ConnectionDetails , error ) {
936+ warnReplicaPooler (cmd , target , opts .Pooled )
937+ return target .Details (opts )
938+ }
939+
894940// getServiceDetails is a helper that handles common service lookup logic and returns the service details
895941func getServiceDetails (cmd * cobra.Command , cfg * common.Config , args []string ) (api.Service , error ) {
896942 // Determine service ID
@@ -901,25 +947,14 @@ func getServiceDetails(cmd *cobra.Command, cfg *common.Config, args []string) (a
901947
902948 cmd .SilenceUsage = true
903949
904- // Fetch service details
905950 ctx , cancel := context .WithTimeout (cmd .Context (), 30 * time .Second )
906951 defer cancel ()
907952
908- resp , err := cfg . Client . GetServiceWithResponse (ctx , cfg .ProjectID , serviceID )
953+ service , err := common . GetService (ctx , cfg . Client , cfg .ProjectID , serviceID )
909954 if err != nil {
910- return api.Service {}, fmt .Errorf ("failed to fetch service details: %w" , err )
911- }
912-
913- // Handle API response
914- if resp .StatusCode () != http .StatusOK {
915- return api.Service {}, common .ExitWithErrorFromStatusCode (resp .StatusCode (), resp .JSON4XX )
916- }
917-
918- if resp .JSON200 == nil {
919- return api.Service {}, fmt .Errorf ("empty response from API" )
955+ return api.Service {}, err
920956 }
921-
922- return * resp .JSON200 , nil
957+ return * service , nil
923958}
924959
925960// ArgsLenAtDashProvider defines the interface for getting ArgsLenAtDash
0 commit comments