Skip to content

Commit 35ce81b

Browse files
committed
Add support for hostaddr
Fixes #860
1 parent f1b4c41 commit 35ce81b

File tree

6 files changed

+81
-18
lines changed

6 files changed

+81
-18
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
unreleased
22
----------
3-
This version of kq requires Go 1.18 or newer.
3+
This version of pq requires Go 1.18 or newer.
44

55
pq now supports only maintained PostgreSQL releases, which is PostgreSQL 14 and
66
newer. Previously PostgreSQL 8.4 and newer were supported.
@@ -33,6 +33,8 @@ newer. Previously PostgreSQL 8.4 and newer were supported.
3333
- Add `Config`, `NewConfig()`, and `NewConnectorConfig()` to supply connection
3434
details in a more structured way ([#1240]).
3535

36+
- Support `hostaddr` and `$PGHOSTADDR` ([#1243]).
37+
3638
- Add `PQGO_DEBUG=1` print the communication with PostgreSQL to stderr, to aid
3739
in debugging, testing, and bug reports ([#1223]).
3840

@@ -104,6 +106,7 @@ newer. Previously PostgreSQL 8.4 and newer were supported.
104106
[#1238]: https://github.com/lib/pq/pull/1238
105107
[#1239]: https://github.com/lib/pq/pull/1239
106108
[#1240]: https://github.com/lib/pq/pull/1240
109+
[#1243]: https://github.com/lib/pq/pull/1243
107110

108111

109112
v1.10.9 (2023-04-26)

conn.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1077,7 +1077,7 @@ func (cn *conn) ssl(o values) error {
10771077
// startup packet.
10781078
func isDriverSetting(key string) bool {
10791079
switch key {
1080-
case "host", "port", "password", "fallback_application_name",
1080+
case "host", "hostaddr", "port", "password", "fallback_application_name",
10811081
"sslmode", "sslcert", "sslkey", "sslrootcert", "sslinline", "sslsni",
10821082
"connect_timeout", "binary_parameters", "disable_prepared_binary_result",
10831083
"krbsrvname", "krbspn":

conn_test.go

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,34 @@ func TestCommitInFailedTransaction(t *testing.T) {
6969
}
7070
}
7171

72-
func TestOpenURL(t *testing.T) {
73-
t.Parallel()
74-
testURL := func(url string) {
75-
db := pqtest.MustDB(t, url)
76-
// database/sql might not call our Open at all unless we do something with
77-
// the connection
78-
txn, err := db.Begin()
79-
if err != nil {
80-
t.Fatal(err)
81-
}
82-
txn.Rollback()
72+
func TestOpen(t *testing.T) {
73+
tests := []struct {
74+
dsn, wantErr string
75+
}{
76+
{"postgres://", ""},
77+
{"postgresql://", ""},
78+
{"host=doesnotexist hostaddr=127.0.0.1", ""}, // Should ignore the host
79+
80+
{"hostaddr=255.255.255.255", "dial tcp 255.255.255.255"},
81+
}
82+
83+
for _, tt := range tests {
84+
tt := tt
85+
t.Run(tt.dsn, func(t *testing.T) {
86+
t.Parallel()
87+
88+
db, err := pqtest.DB(tt.dsn)
89+
if err != nil {
90+
t.Fatal(err)
91+
}
92+
defer db.Close()
93+
94+
err = db.Ping()
95+
if !pqtest.ErrorContains(err, tt.wantErr) {
96+
t.Errorf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr)
97+
}
98+
})
8399
}
84-
testURL("postgres://")
85-
testURL("postgresql://")
86100
}
87101

88102
func TestPgpass(t *testing.T) {

connector.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"database/sql/driver"
66
"fmt"
77
"net"
8+
"net/netip"
89
neturl "net/url"
910
"os"
1011
"path/filepath"
@@ -104,6 +105,25 @@ type Config struct {
104105
// for unix domain sockets. Defaults to localhost.
105106
Host string `postgres:"host" env:"PGHOST"`
106107

108+
// IPv4 or IPv6 address to connect to. Using hostaddr allows the application
109+
// to avoid a host name lookup, which might be important in applications
110+
// with time constraints. A hostname is required for sslmode=verify-full and
111+
// the GSSAPI or SSPI authentication methods.
112+
//
113+
// The following rules are used:
114+
//
115+
// - If host is given without hostaddr, a host name lookup occurs.
116+
//
117+
// - If hostaddr is given without host, the value for hostaddr gives the
118+
// server network address. The connection attempt will fail if the
119+
// authentication method requires a host name.
120+
//
121+
// - If both host and hostaddr are given, the value for hostaddr gives the
122+
// server network address. The value for host is ignored unless the
123+
// authentication method requires it, in which case it will be used as the
124+
// host name.
125+
Hostaddr netip.Addr `postgres:"hostaddr" env:"PGHOSTADDR"`
126+
107127
// The port to connect to. Defaults to 5432.
108128
Port uint16 `postgres:"port" env:"PGPORT"`
109129

@@ -302,6 +322,9 @@ func newConfig(dsn string, env []string) (Config, error) {
302322
}
303323

304324
func (cfg Config) network() (string, string) {
325+
if cfg.Hostaddr != (netip.Addr{}) {
326+
return "tcp", net.JoinHostPort(cfg.Hostaddr.String(), strconv.Itoa(int(cfg.Port)))
327+
}
305328
// UNIX domain sockets are either represented by an (absolute) file system
306329
// path or they live in the abstract name space (starting with an @).
307330
if filepath.IsAbs(cfg.Host) || strings.HasPrefix(cfg.Host, "@") {
@@ -319,7 +342,7 @@ func (cfg *Config) fromEnv(env []string) error {
319342
continue
320343
}
321344
switch k {
322-
case "PGHOSTADDR", "PGREQUIREAUTH", "PGCHANNELBINDING", "PGSERVICE", "PGSERVICEFILE", "PGREALM",
345+
case "PGREQUIREAUTH", "PGCHANNELBINDING", "PGSERVICE", "PGSERVICEFILE", "PGREALM",
323346
"PGSSLCERTMODE", "PGSSLCOMPRESSION", "PGREQUIRESSL", "PGSSLCRL", "PGREQUIREPEER",
324347
"PGSYSCONFDIR", "PGLOCALEDIR", "PGSSLCRLDIR", "PGSSLMINPROTOCOLVERSION", "PGSSLMAXPROTOCOLVERSION",
325348
"PGGSSENCMODE", "PGGSSDELEGATION", "PGTARGETSESSIONATTRS", "PGLOADBALANCEHOSTS", "PGMINPROTOCOLVERSION",
@@ -468,7 +491,17 @@ func (cfg *Config) setFromTag(o map[string]string, tag string) error {
468491
}
469492
switch rt.Type.Kind() {
470493
default:
471-
return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type)
494+
return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type.Kind())
495+
case reflect.Struct:
496+
if rt.Type == reflect.TypeOf(netip.Addr{}) {
497+
ip, err := netip.ParseAddr(v)
498+
if err != nil {
499+
return fmt.Errorf(f+"%w", k, err)
500+
}
501+
rv.Set(reflect.ValueOf(ip))
502+
} else {
503+
return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type)
504+
}
472505
case reflect.String:
473506
if ((tag == "postgres" && k == "sslmode") || (tag == "env" && k == "PGSSLMODE")) &&
474507
!pqutil.Contains(sslModes, SSLMode(v)) &&
@@ -545,7 +578,11 @@ func (cfg Config) tomap() values {
545578
if !rv.IsZero() || pqutil.Contains(cfg.set, k) {
546579
switch rt.Type.Kind() {
547580
default:
548-
o[k] = rv.String()
581+
if s, ok := rv.Interface().(fmt.Stringer); ok {
582+
o[k] = s.String()
583+
} else {
584+
o[k] = rv.String()
585+
}
549586
case reflect.Uint16:
550587
n := rv.Uint()
551588
o[k] = strconv.FormatUint(n, 10)

connector_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,11 @@ func TestNewConfig(t *testing.T) {
357357
{"port=5s", nil, "", `pq: wrong value for "port": strconv.ParseUint: parsing "5s": invalid syntax`},
358358
{"", []string{"PGPORT=5s"}, "", `pq: wrong value for $PGPORT: strconv.ParseUint: parsing "5s": invalid syntax`},
359359

360+
// hostaddr
361+
{"hostaddr=127.1.2.3", nil, "hostaddr=127.1.2.3", ""},
362+
{"hostaddr=::1", nil, "hostaddr=::1", ""},
363+
{"", []string{"PGHOSTADDR=2a01:4f9:3081:5413::2"}, "hostaddr=2a01:4f9:3081:5413::2", ""},
364+
360365
// Runtime
361366
{"user=u search_path=abc", nil, "search_path=abc user=u", ""},
362367
{"database=db", nil, "dbname=db", ``},

deprecated.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ func ParseURL(url string) (string, error) { return convertURL(url) }
7575
type values map[string]string
7676

7777
func (o values) network() (string, string) {
78+
if ho := o["hostaddr"]; ho != "" {
79+
return "tcp", net.JoinHostPort(ho, o["port"])
80+
}
81+
7882
host := o["host"]
7983
// UNIX domain sockets are either represented by an (absolute) file system
8084
// path or they live in the abstract name space (starting with an @).

0 commit comments

Comments
 (0)