Skip to content

Add pgroll latest url command #927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 30, 2025
Merged
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
18 changes: 18 additions & 0 deletions cli-definition.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@
],
"subcommands": [],
"args": []
},
{
"name": "url",
"short": "Print a database connection URL for the latest schema version",
"use": "url",
"example": "pgroll latest url <connection-string> --local ./migrations",
"flags": [
{
"name": "local",
"shorthand": "l",
"description": "retrieve the latest schema version from a local migration directory",
"default": ""
}
],
"subcommands": [],
"args": [
"connection-string"
]
}
],
"args": []
Expand Down
1 change: 1 addition & 0 deletions cmd/latest.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func latestCmd() *cobra.Command {

latestCmd.AddCommand(latestSchemaCmd())
latestCmd.AddCommand(latestMigrationCmd())
latestCmd.AddCommand(latestURLCmd())

return latestCmd
}
53 changes: 53 additions & 0 deletions cmd/latest_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"

"github.com/spf13/cobra"
"github.com/xataio/pgroll/cmd/flags"
"github.com/xataio/pgroll/internal/connstr"
)

func latestURLCmd() *cobra.Command {
var migrationsDir string

urlCmd := &cobra.Command{
Use: "url",
Short: "Print a database connection URL for the latest schema version",
Long: "Print a database connection URL for the latest schema version, either from the target database or a local directory",
Example: "pgroll latest url <connection-string> --local ./migrations",
Args: cobra.MaximumNArgs(1),
ValidArgs: []string{"connection-string"},
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

// Default to the Postgres URL from flags, or use the first argument if provided
pgURL := flags.PostgresURL()
if len(args) > 0 {
pgURL = args[0]
}

// Get the latest version schema name, either from the remote database or a local directory
latestVersion, err := latestVersion(ctx, migrationsDir)
if err != nil {
return fmt.Errorf("failed to get latest version: %w", err)
}

// Append the search_path option to the connection string
str, err := connstr.AppendSearchPathOption(pgURL, latestVersion)
if err != nil {
return fmt.Errorf("failed to add search_path option: %w", err)
}

fmt.Println(str)

return nil
},
}

urlCmd.Flags().StringVarP(&migrationsDir, "local", "l", "", "retrieve the latest schema version from a local migration directory")

return urlCmd
}
35 changes: 35 additions & 0 deletions internal/connstr/connstr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0

package connstr

import (
"fmt"
"net/url"
"strings"
)

// AppendSearchPathOption take a Postgres connection string in URL format and
// produces the same connection string with the search_path option set to the
// provided schema.
func AppendSearchPathOption(connStr, schema string) (string, error) {
u, err := url.Parse(connStr)
if err != nil {
return "", fmt.Errorf("failed to parse connection string: %w", err)
}

if schema == "" {
return connStr, nil
}

q := u.Query()
q.Set("options", fmt.Sprintf("-c search_path=%s", schema))
encodedQuery := q.Encode()

// Replace '+' with '%20' to ensure proper encoding of spaces within the
// `options` query parameter.
encodedQuery = strings.ReplaceAll(encodedQuery, "+", "%20")

u.RawQuery = encodedQuery

return u.String(), nil
}
47 changes: 47 additions & 0 deletions internal/connstr/connstr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0

package connstr_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/xataio/pgroll/internal/connstr"
)

func TestAppendSearchPathOption(t *testing.T) {
tests := []struct {
Name string
ConnStr string
Schema string
Expected string
}{
{
Name: "empty schema doesn't change connection string",
ConnStr: "postgres://postgres:postgres@localhost:5432?sslmode=disable",
Schema: "",
Expected: "postgres://postgres:postgres@localhost:5432?sslmode=disable",
},
{
Name: "can set options as the only query parameter",
ConnStr: "postgres://postgres:postgres@localhost:5432",
Schema: "apples",
Expected: "postgres://postgres:postgres@localhost:5432?options=-c%20search_path%3Dapples",
},
{
Name: "can set options as an additional query parameter",
ConnStr: "postgres://postgres:postgres@localhost:5432?sslmode=disable",
Schema: "bananas",
Expected: "postgres://postgres:postgres@localhost:5432?options=-c%20search_path%3Dbananas&sslmode=disable",
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
result, err := connstr.AppendSearchPathOption(tt.ConnStr, tt.Schema)
assert.NoError(t, err)

assert.Equal(t, tt.Expected, result)
})
}
}
Loading