aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2026-08-05 21:47:03 +0200
committerStefan Majewsky <majewsky@gmx.net>2026-08-05 21:47:03 +0200
commit41d7a246f83d4ea44afb77a817e00a97a8ff5e40 (patch)
treef425ebba832cf633459f207c145fe958ebb2d069
parent20f683238e98529f952f7bb8a5cf9455b72fd4b9 (diff)
downloadgo-gg-41d7a246f83d4ea44afb77a817e00a97a8ff5e40.tar.gz
pgruntime: add ConnectionTarget.ApplicationName attribute
-rw-r--r--CHANGELOG.md6
-rw-r--r--pgruntime/target.go38
-rw-r--r--pgruntime/target_test.go50
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`)
+ }
}