diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2026-08-05 21:47:03 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2026-08-05 21:47:03 +0200 |
| commit | 41d7a246f83d4ea44afb77a817e00a97a8ff5e40 (patch) | |
| tree | f425ebba832cf633459f207c145fe958ebb2d069 | |
| parent | 20f683238e98529f952f7bb8a5cf9455b72fd4b9 (diff) | |
| download | go-gg-41d7a246f83d4ea44afb77a817e00a97a8ff5e40.tar.gz | |
pgruntime: add ConnectionTarget.ApplicationName attribute
| -rw-r--r-- | CHANGELOG.md | 6 | ||||
| -rw-r--r-- | pgruntime/target.go | 38 | ||||
| -rw-r--r-- | pgruntime/target_test.go | 50 |
3 files changed, 82 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0872656..e50d463 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> SPDX-License-Identifier: Apache-2.0 --> +# v1.13.3 (TBD) + +Changes: + +- pgruntime: Add ApplicationName to ConnectionTarget for easy upgrade from go-bits/easypg. + # v1.13.2 (2026-08-05) Changes: diff --git a/pgruntime/target.go b/pgruntime/target.go index a03c8c5..f1f4729 100644 --- a/pgruntime/target.go +++ b/pgruntime/target.go @@ -8,6 +8,7 @@ import ( "maps" "net" "net/url" + "os" "strings" ) @@ -18,6 +19,9 @@ import ( // Its value must be formatted in the syntax accepted by [url.ParseQuery]. // - The ExtraConnectionOptions field is intended for options coming in via code. // - Both ConnectionOptions fields accept all query parameters that are allowed in [libpq-style connection URIs]. +// - ApplicationName is an alternative way of setting ExtraConnectionOptions["fallback_application_name"]. +// If filled, pgruntime will also try to automatically append the hostname if available. +// An explicit "application_name" setting in either ConnectionOptions or ExtraConnectionOptions takes precedence over this setting. // // This type is intended to be retrieved from environment variables, for example: // @@ -39,6 +43,7 @@ type ConnectionTarget struct { DatabaseName string ConnectionOptions string ExtraConnectionOptions url.Values + ApplicationName string } const ( @@ -47,7 +52,7 @@ const ( ) // ParseConnectionTargetFromURL parses a [libpq-style connection URI] into a [ConnectionTarget] instance. -// If there are connection options, they will be placed in the ConnectionOptions field, not in ExtraConnectionOptions. +// If there are connection options, they will be placed in the ConnectionOptions field, not in ExtraConnectionOptions or ApplicationName. // // The special libpq syntax with multiple host:port pairs is not supported by this function and will result in an error. // @@ -96,9 +101,20 @@ func ParseConnectionTargetFromURL(u *url.URL) (ConnectionTarget, error) { return result, nil } +// dependency injection slot to insert a double during tests +var osHostname = os.Hostname + // IntoURL formats the ConnectionTarget as a libpq-style connection URI. func (t ConnectionTarget) IntoURL() (*url.URL, error) { - rawQuery, err := mergeConnectionOptions(t.ConnectionOptions, t.ExtraConnectionOptions) + appName := t.ApplicationName + if appName != "" { + hostname, err := osHostname() + if err == nil { + appName += "@" + hostname + } + } + + rawQuery, err := mergeConnectionOptions(t.ConnectionOptions, t.ExtraConnectionOptions, appName) if err != nil { return nil, fmt.Errorf("in ConnectionTarget.IntoURL: malformed connection options: %w", err) } @@ -127,12 +143,21 @@ func (t ConnectionTarget) buildHostPort() string { } } -func mergeConnectionOptions(opts string, extraOpts url.Values) (string, error) { - if len(extraOpts) == 0 { +func mergeConnectionOptions(opts string, extraOpts url.Values, appName string) (string, error) { + if len(extraOpts) == 0 && appName == "" { return opts, nil } if opts == "" { - return extraOpts.Encode(), nil + if appName == "" { + return extraOpts.Encode(), nil + } else { + cloned := maps.Clone(extraOpts) + if cloned == nil { + cloned = make(url.Values) + } + cloned.Set("fallback_application_name", appName) + return cloned.Encode(), nil + } } values, err := url.ParseQuery(opts) @@ -140,5 +165,8 @@ func mergeConnectionOptions(opts string, extraOpts url.Values) (string, error) { return "", err } maps.Copy(values, extraOpts) + if appName != "" { + values.Set("fallback_application_name", appName) + } return values.Encode(), nil } diff --git a/pgruntime/target_test.go b/pgruntime/target_test.go index d62721c..e6e508b 100644 --- a/pgruntime/target_test.go +++ b/pgruntime/target_test.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net> // SPDX-License-Identifier: Apache-2.0 -package pgruntime_test +package pgruntime import ( "net/url" @@ -9,11 +9,10 @@ import ( "testing" "go.xyrillian.de/gg/assert" - "go.xyrillian.de/gg/pgruntime" ) func TestParseConnectionTargetSuccess(t *testing.T) { - testCases := map[string]pgruntime.ConnectionTarget{ + testCases := map[string]ConnectionTarget{ // minimal case: just the required fields are set `postgresql://alice@localhost/bookstore`: { HostName: "localhost", @@ -37,7 +36,7 @@ func TestParseConnectionTargetSuccess(t *testing.T) { if !assert.ErrEqual(t, err, nil) { t.FailNow() } - parsed, err := pgruntime.ParseConnectionTargetFromURL(u) + parsed, err := ParseConnectionTargetFromURL(u) if !assert.ErrEqual(t, err, nil) { t.FailNow() } @@ -67,14 +66,18 @@ func TestParseConnectionTargetFailure(t *testing.T) { if !assert.ErrEqual(t, err, nil) { t.FailNow() } - _, err = pgruntime.ParseConnectionTargetFromURL(u) + _, err = ParseConnectionTargetFromURL(u) assert.ErrEqual(t, err, "in ParseConnectionTargetFromURL: "+expected) }) } } func TestSerializeConnectionOptions(t *testing.T) { - ct := pgruntime.ConnectionTarget{ + osHostname = func() (string, error) { + return "bar", nil + } + + ct := ConnectionTarget{ HostName: "localhost", UserName: "alice", DatabaseName: "bookstore", @@ -83,29 +86,62 @@ func TestSerializeConnectionOptions(t *testing.T) { // cannot merge ConnectionOptions if the string part is malformed ct.ConnectionOptions = `sslmode=prefer;foo=bar` ct.ExtraConnectionOptions = url.Values{"application_name": {"frontdesk"}} + ct.ApplicationName = "" _, err := ct.IntoURL() assert.ErrEqual(t, err, `in ConnectionTarget.IntoURL: malformed connection options: invalid semicolon separator in query`) + // not really a merge at all + ct.ConnectionOptions = "" + ct.ExtraConnectionOptions = nil + ct.ApplicationName = "" + u, err := ct.IntoURL() + if assert.ErrEqual(t, err, nil) { + assert.Equal(t, u.RawQuery, "") + } + ct.ApplicationName = "foo" + u, err = ct.IntoURL() + if assert.ErrEqual(t, err, nil) { + assert.Equal(t, u.RawQuery, `fallback_application_name=foo%40bar`) + } + // successful trivial merges ct.ConnectionOptions = `sslmode=prefer` ct.ExtraConnectionOptions = nil - u, err := ct.IntoURL() + ct.ApplicationName = "" + u, err = ct.IntoURL() if assert.ErrEqual(t, err, nil) { assert.Equal(t, u.RawQuery, `sslmode=prefer`) } + ct.ApplicationName = "foo" + u, err = ct.IntoURL() + if assert.ErrEqual(t, err, nil) { + assert.Equal(t, u.RawQuery, `fallback_application_name=foo%40bar&sslmode=prefer`) + } ct.ConnectionOptions = "" ct.ExtraConnectionOptions = url.Values{"application_name": {"frontdesk"}} + ct.ApplicationName = "" u, err = ct.IntoURL() if assert.ErrEqual(t, err, nil) { assert.Equal(t, u.RawQuery, `application_name=frontdesk`) } + ct.ApplicationName = "foo" + u, err = ct.IntoURL() + if assert.ErrEqual(t, err, nil) { + assert.Equal(t, u.RawQuery, `application_name=frontdesk&fallback_application_name=foo%40bar`) + } // successful complex merge ct.ConnectionOptions = `sslmode=prefer` ct.ExtraConnectionOptions = url.Values{"application_name": {"frontdesk"}} + ct.ApplicationName = "" u, err = ct.IntoURL() if assert.ErrEqual(t, err, nil) { assert.Equal(t, u.RawQuery, `application_name=frontdesk&sslmode=prefer`) } + ct.ApplicationName = "foo" + u, err = ct.IntoURL() + if assert.ErrEqual(t, err, nil) { + assert.Equal(t, u.RawQuery, `application_name=frontdesk&fallback_application_name=foo%40bar&sslmode=prefer`) + } } |
