1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
// SPDX-FileCopyrightText: 2026 Stefan Majewsky <majewsky@gmx.net>
// SPDX-License-Identifier: Apache-2.0
package pgruntime
import (
"fmt"
"maps"
"net"
"net/url"
"strings"
)
// ConnectionTarget describes the location of and credentials and other connection options for the database server that [Connector.Connect] connects to.
//
// - The HostName, UserName and DatabaseName fields are required. All other fields are optional.
// - The ConnectionOptions field is intended for options coming in via config.
// 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].
//
// This type is intended to be retrieved from environment variables, for example:
//
// ct := pgruntime.ConnectionTarget{
// HostName: cmp.Or(os.Getenv("FOOBAR_DB_HOSTNAME"), "localhost"),
// Port: cmp.Or(os.Getenv("FOOBAR_DB_PORT"), "5432"),
// UserName: cmp.Or(os.Getenv("FOOBAR_DB_USERNAME"), "postgres"),
// Password: os.Getenv("FOOBAR_DB_PASSWORD"),
// ConnectionOptions: os.Getenv("FOOBAR_DB_CONNECTION_OPTIONS"),
// DatabaseName: cmp.Or(os.Getenv("FOOBAR_DB_NAME"), "foobar"),
// }))
//
// [libpq-style connection URIs]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS
type ConnectionTarget struct {
HostName string
Port string
UserName string
Password string // default: <no password>
DatabaseName string
ConnectionOptions string
ExtraConnectionOptions url.Values
}
const (
connectionURISchemeShort = "postgres"
connectionURISchemeLong = "postgresql"
)
// 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.
//
// The special libpq syntax with multiple host:port pairs is not supported by this function and will result in an error.
//
// [libpq-style connection URI]: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS
func ParseConnectionTargetFromURL(u *url.URL) (ConnectionTarget, error) {
var (
result ConnectionTarget
none ConnectionTarget // shorthand for error return paths
)
if u.Scheme != connectionURISchemeShort && u.Scheme != connectionURISchemeLong {
return none, fmt.Errorf(`in ParseConnectionTargetFromURL: expected scheme %q or %q, but got %q`, connectionURISchemeShort, connectionURISchemeLong, u.Scheme)
}
if u.Opaque != "" {
return none, fmt.Errorf(`in ParseConnectionTargetFromURL: expected authority component, but got rootless path %q`, u.Opaque)
}
if u.Fragment != "" {
return none, fmt.Errorf(`in ParseConnectionTargetFromURL: unexpected fragment %q`, u.Fragment)
}
var err error
if strings.Contains(u.Host, ":") {
result.HostName, result.Port, err = net.SplitHostPort(u.Host)
if err != nil {
return none, fmt.Errorf(`in ParseConnectionTargetFromURL: malformed host %q`, u.Host)
}
} else {
result.HostName = u.Host
}
result.UserName = u.User.Username()
result.Password, _ = u.User.Password()
result.DatabaseName = strings.TrimPrefix(u.Path, "/")
if strings.Contains(result.DatabaseName, "/") {
return none, fmt.Errorf(`in ParseConnectionTargetFromURL: malformed database name %q`, result.DatabaseName)
}
if u.RawQuery != "" {
_, err := url.ParseQuery(u.RawQuery)
if err != nil {
return none, fmt.Errorf(`in ParseConnectionTargetFromURL: malformed connection options: %w`, err)
}
result.ConnectionOptions = u.RawQuery
}
return result, nil
}
// IntoURL formats the ConnectionTarget as a libpq-style connection URI.
func (t ConnectionTarget) IntoURL() (*url.URL, error) {
rawQuery, err := mergeConnectionOptions(t.ConnectionOptions, t.ExtraConnectionOptions)
if err != nil {
return nil, fmt.Errorf("in ConnectionTarget.IntoURL: malformed connection options: %w", err)
}
return &url.URL{
Scheme: connectionURISchemeShort, // could also use `connectionURISchemeLong`, but go-bits/easypg made this choice
User: t.buildUserInfo(),
Host: t.buildHostPort(),
Path: "/" + t.DatabaseName,
RawQuery: rawQuery,
}, nil
}
func (t ConnectionTarget) buildUserInfo() *url.Userinfo {
if t.Password == "" {
return url.User(t.UserName)
} else {
return url.UserPassword(t.UserName, t.Password)
}
}
func (t ConnectionTarget) buildHostPort() string {
if t.Port == "" {
return t.HostName
} else {
return net.JoinHostPort(t.HostName, t.Port)
}
}
func mergeConnectionOptions(opts string, extraOpts url.Values) (string, error) {
if len(extraOpts) == 0 {
return opts, nil
}
if opts == "" {
return extraOpts.Encode(), nil
}
values, err := url.ParseQuery(opts)
if err != nil {
return "", err
}
maps.Copy(values, extraOpts)
return values.Encode(), nil
}
|