Skip to content

Commit a90b587

Browse files
Add pgroll latest url command (#927)
Add a `pgroll latest url` command to the CLI. The command returns a Postgres connection string with the `search_path` option set to the latest version schema name. This can be used to set the connection string for an application to work with the latest schema version. By default, with no arguments, `pgroll latest url` will use the connection string set via the global `--postgres-url` (or `PGROLL_PG_URL` env var). If an argument is given to `pgroll latest url`, the command will use that as the base connection string instead. The resulting connection string sets the `search_path` by setting the `options` parameter in the URL query string: ``` options=-c search_path='public_01_some_migration' ``` As described in the Postgres [libpq documentation](https://www.postgresql.org/docs/current/libpq-connect.html). ## Examples With no connection string argument, use the connection string used by `pgroll` itself: ```bash $ export PGROLL_PG_URL=postgres://postgres:postgres@localhost:5432?sslmode=disable $ pgroll latest url postgres://postgres:postgres@localhost:5432?options=-c%20search_path%3Dpublic_05_some_migration&sslmode=disable ``` Take the connection string as a command line argument: ```bash $ pgroll latest url postgres://me:[email protected]:5432 postgres://me:[email protected]:5432?options=-c%20search_path%3Dpublic_05_some_migration ``` Like `pgroll latest migration` and `pgroll latest schema`, `pgroll latest url` takes a `--local/-l` flag to take the latest version schema name from a directory of migration files rather than the target database. This allows for a connection string to be constructed offline, without having to lookup the latest version schema name in the target database. ```bash $ pgroll latest url --local migrations/ postgres://me:mypass@localhost:5432 postgres://me:mypass@localhost:5432?options=-c%20search_path%3Dpublic_05_some_migration ``` The resulting connection string can be used directly as an argument to tools like `psql` that accept a connection string on the command line: ```bash psql $(pgroll . latest url) ``` Documentation for the new command is added in #928 Closes #900
1 parent c3c73e4 commit a90b587

File tree

5 files changed

+154
-0
lines changed

5 files changed

+154
-0
lines changed

cli-definition.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,24 @@
138138
],
139139
"subcommands": [],
140140
"args": []
141+
},
142+
{
143+
"name": "url",
144+
"short": "Print a database connection URL for the latest schema version",
145+
"use": "url",
146+
"example": "pgroll latest url <connection-string> --local ./migrations",
147+
"flags": [
148+
{
149+
"name": "local",
150+
"shorthand": "l",
151+
"description": "retrieve the latest schema version from a local migration directory",
152+
"default": ""
153+
}
154+
],
155+
"subcommands": [],
156+
"args": [
157+
"connection-string"
158+
]
141159
}
142160
],
143161
"args": []

cmd/latest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func latestCmd() *cobra.Command {
1515

1616
latestCmd.AddCommand(latestSchemaCmd())
1717
latestCmd.AddCommand(latestMigrationCmd())
18+
latestCmd.AddCommand(latestURLCmd())
1819

1920
return latestCmd
2021
}

cmd/latest_url.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package cmd
4+
5+
import (
6+
"fmt"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/xataio/pgroll/cmd/flags"
10+
"github.com/xataio/pgroll/internal/connstr"
11+
)
12+
13+
func latestURLCmd() *cobra.Command {
14+
var migrationsDir string
15+
16+
urlCmd := &cobra.Command{
17+
Use: "url",
18+
Short: "Print a database connection URL for the latest schema version",
19+
Long: "Print a database connection URL for the latest schema version, either from the target database or a local directory",
20+
Example: "pgroll latest url <connection-string> --local ./migrations",
21+
Args: cobra.MaximumNArgs(1),
22+
ValidArgs: []string{"connection-string"},
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
ctx := cmd.Context()
25+
26+
// Default to the Postgres URL from flags, or use the first argument if provided
27+
pgURL := flags.PostgresURL()
28+
if len(args) > 0 {
29+
pgURL = args[0]
30+
}
31+
32+
// Get the latest version schema name, either from the remote database or a local directory
33+
latestVersion, err := latestVersion(ctx, migrationsDir)
34+
if err != nil {
35+
return fmt.Errorf("failed to get latest version: %w", err)
36+
}
37+
38+
// Append the search_path option to the connection string
39+
str, err := connstr.AppendSearchPathOption(pgURL, latestVersion)
40+
if err != nil {
41+
return fmt.Errorf("failed to add search_path option: %w", err)
42+
}
43+
44+
fmt.Println(str)
45+
46+
return nil
47+
},
48+
}
49+
50+
urlCmd.Flags().StringVarP(&migrationsDir, "local", "l", "", "retrieve the latest schema version from a local migration directory")
51+
52+
return urlCmd
53+
}

internal/connstr/connstr.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package connstr
4+
5+
import (
6+
"fmt"
7+
"net/url"
8+
"strings"
9+
)
10+
11+
// AppendSearchPathOption take a Postgres connection string in URL format and
12+
// produces the same connection string with the search_path option set to the
13+
// provided schema.
14+
func AppendSearchPathOption(connStr, schema string) (string, error) {
15+
u, err := url.Parse(connStr)
16+
if err != nil {
17+
return "", fmt.Errorf("failed to parse connection string: %w", err)
18+
}
19+
20+
if schema == "" {
21+
return connStr, nil
22+
}
23+
24+
q := u.Query()
25+
q.Set("options", fmt.Sprintf("-c search_path=%s", schema))
26+
encodedQuery := q.Encode()
27+
28+
// Replace '+' with '%20' to ensure proper encoding of spaces within the
29+
// `options` query parameter.
30+
encodedQuery = strings.ReplaceAll(encodedQuery, "+", "%20")
31+
32+
u.RawQuery = encodedQuery
33+
34+
return u.String(), nil
35+
}

internal/connstr/connstr_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package connstr_test
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/xataio/pgroll/internal/connstr"
10+
)
11+
12+
func TestAppendSearchPathOption(t *testing.T) {
13+
tests := []struct {
14+
Name string
15+
ConnStr string
16+
Schema string
17+
Expected string
18+
}{
19+
{
20+
Name: "empty schema doesn't change connection string",
21+
ConnStr: "postgres://postgres:postgres@localhost:5432?sslmode=disable",
22+
Schema: "",
23+
Expected: "postgres://postgres:postgres@localhost:5432?sslmode=disable",
24+
},
25+
{
26+
Name: "can set options as the only query parameter",
27+
ConnStr: "postgres://postgres:postgres@localhost:5432",
28+
Schema: "apples",
29+
Expected: "postgres://postgres:postgres@localhost:5432?options=-c%20search_path%3Dapples",
30+
},
31+
{
32+
Name: "can set options as an additional query parameter",
33+
ConnStr: "postgres://postgres:postgres@localhost:5432?sslmode=disable",
34+
Schema: "bananas",
35+
Expected: "postgres://postgres:postgres@localhost:5432?options=-c%20search_path%3Dbananas&sslmode=disable",
36+
},
37+
}
38+
39+
for _, tt := range tests {
40+
t.Run(tt.Name, func(t *testing.T) {
41+
result, err := connstr.AppendSearchPathOption(tt.ConnStr, tt.Schema)
42+
assert.NoError(t, err)
43+
44+
assert.Equal(t, tt.Expected, result)
45+
})
46+
}
47+
}

0 commit comments

Comments
 (0)