diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f9bc019..669acef55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ unreleased ---------- -This version of kq requires Go 1.18 or newer. +This version of pq requires Go 1.18 or newer. pq now supports only maintained PostgreSQL releases, which is PostgreSQL 14 and newer. Previously PostgreSQL 8.4 and newer were supported. @@ -33,6 +33,8 @@ newer. Previously PostgreSQL 8.4 and newer were supported. - Add `Config`, `NewConfig()`, and `NewConnectorConfig()` to supply connection details in a more structured way ([#1240]). +- Support `hostaddr` and `$PGHOSTADDR` ([#1243]). + - Add `PQGO_DEBUG=1` print the communication with PostgreSQL to stderr, to aid in debugging, testing, and bug reports ([#1223]). @@ -104,6 +106,7 @@ newer. Previously PostgreSQL 8.4 and newer were supported. [#1238]: https://github.com/lib/pq/pull/1238 [#1239]: https://github.com/lib/pq/pull/1239 [#1240]: https://github.com/lib/pq/pull/1240 +[#1243]: https://github.com/lib/pq/pull/1243 v1.10.9 (2023-04-26) diff --git a/conn.go b/conn.go index bb406ea15..ea04ab980 100644 --- a/conn.go +++ b/conn.go @@ -1077,7 +1077,7 @@ func (cn *conn) ssl(o values) error { // startup packet. func isDriverSetting(key string) bool { switch key { - case "host", "port", "password", "fallback_application_name", + case "host", "hostaddr", "port", "password", "fallback_application_name", "sslmode", "sslcert", "sslkey", "sslrootcert", "sslinline", "sslsni", "connect_timeout", "binary_parameters", "disable_prepared_binary_result", "krbsrvname", "krbspn": diff --git a/conn_test.go b/conn_test.go index 19194f699..c7137503f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -69,20 +69,34 @@ func TestCommitInFailedTransaction(t *testing.T) { } } -func TestOpenURL(t *testing.T) { - t.Parallel() - testURL := func(url string) { - db := pqtest.MustDB(t, url) - // database/sql might not call our Open at all unless we do something with - // the connection - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - txn.Rollback() +func TestOpen(t *testing.T) { + tests := []struct { + dsn, wantErr string + }{ + {"postgres://", ""}, + {"postgresql://", ""}, + {"host=doesnotexist hostaddr=127.0.0.1", ""}, // Should ignore the host + + {"hostaddr=255.255.255.255", "dial tcp 255.255.255.255"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.dsn, func(t *testing.T) { + t.Parallel() + + db, err := pqtest.DB(tt.dsn) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + err = db.Ping() + if !pqtest.ErrorContains(err, tt.wantErr) { + t.Errorf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr) + } + }) } - testURL("postgres://") - testURL("postgresql://") } func TestPgpass(t *testing.T) { diff --git a/connector.go b/connector.go index 78229ed7c..3d9283fa0 100644 --- a/connector.go +++ b/connector.go @@ -5,6 +5,7 @@ import ( "database/sql/driver" "fmt" "net" + "net/netip" neturl "net/url" "os" "path/filepath" @@ -104,6 +105,25 @@ type Config struct { // for unix domain sockets. Defaults to localhost. Host string `postgres:"host" env:"PGHOST"` + // IPv4 or IPv6 address to connect to. Using hostaddr allows the application + // to avoid a host name lookup, which might be important in applications + // with time constraints. A hostname is required for sslmode=verify-full and + // the GSSAPI or SSPI authentication methods. + // + // The following rules are used: + // + // - If host is given without hostaddr, a host name lookup occurs. + // + // - If hostaddr is given without host, the value for hostaddr gives the + // server network address. The connection attempt will fail if the + // authentication method requires a host name. + // + // - If both host and hostaddr are given, the value for hostaddr gives the + // server network address. The value for host is ignored unless the + // authentication method requires it, in which case it will be used as the + // host name. + Hostaddr netip.Addr `postgres:"hostaddr" env:"PGHOSTADDR"` + // The port to connect to. Defaults to 5432. Port uint16 `postgres:"port" env:"PGPORT"` @@ -302,6 +322,9 @@ func newConfig(dsn string, env []string) (Config, error) { } func (cfg Config) network() (string, string) { + if cfg.Hostaddr != (netip.Addr{}) { + return "tcp", net.JoinHostPort(cfg.Hostaddr.String(), strconv.Itoa(int(cfg.Port))) + } // UNIX domain sockets are either represented by an (absolute) file system // path or they live in the abstract name space (starting with an @). if filepath.IsAbs(cfg.Host) || strings.HasPrefix(cfg.Host, "@") { @@ -319,7 +342,7 @@ func (cfg *Config) fromEnv(env []string) error { continue } switch k { - case "PGHOSTADDR", "PGREQUIREAUTH", "PGCHANNELBINDING", "PGSERVICE", "PGSERVICEFILE", "PGREALM", + case "PGREQUIREAUTH", "PGCHANNELBINDING", "PGSERVICE", "PGSERVICEFILE", "PGREALM", "PGSSLCERTMODE", "PGSSLCOMPRESSION", "PGREQUIRESSL", "PGSSLCRL", "PGREQUIREPEER", "PGSYSCONFDIR", "PGLOCALEDIR", "PGSSLCRLDIR", "PGSSLMINPROTOCOLVERSION", "PGSSLMAXPROTOCOLVERSION", "PGGSSENCMODE", "PGGSSDELEGATION", "PGTARGETSESSIONATTRS", "PGLOADBALANCEHOSTS", "PGMINPROTOCOLVERSION", @@ -468,7 +491,17 @@ func (cfg *Config) setFromTag(o map[string]string, tag string) error { } switch rt.Type.Kind() { default: - return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type) + return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type.Kind()) + case reflect.Struct: + if rt.Type == reflect.TypeOf(netip.Addr{}) { + ip, err := netip.ParseAddr(v) + if err != nil { + return fmt.Errorf(f+"%w", k, err) + } + rv.Set(reflect.ValueOf(ip)) + } else { + return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type) + } case reflect.String: if ((tag == "postgres" && k == "sslmode") || (tag == "env" && k == "PGSSLMODE")) && !pqutil.Contains(sslModes, SSLMode(v)) && @@ -545,7 +578,11 @@ func (cfg Config) tomap() values { if !rv.IsZero() || pqutil.Contains(cfg.set, k) { switch rt.Type.Kind() { default: - o[k] = rv.String() + if s, ok := rv.Interface().(fmt.Stringer); ok { + o[k] = s.String() + } else { + o[k] = rv.String() + } case reflect.Uint16: n := rv.Uint() o[k] = strconv.FormatUint(n, 10) diff --git a/connector_test.go b/connector_test.go index da9a3b5ea..5e6ba482e 100644 --- a/connector_test.go +++ b/connector_test.go @@ -357,6 +357,11 @@ func TestNewConfig(t *testing.T) { {"port=5s", nil, "", `pq: wrong value for "port": strconv.ParseUint: parsing "5s": invalid syntax`}, {"", []string{"PGPORT=5s"}, "", `pq: wrong value for $PGPORT: strconv.ParseUint: parsing "5s": invalid syntax`}, + // hostaddr + {"hostaddr=127.1.2.3", nil, "hostaddr=127.1.2.3", ""}, + {"hostaddr=::1", nil, "hostaddr=::1", ""}, + {"", []string{"PGHOSTADDR=2a01:4f9:3081:5413::2"}, "hostaddr=2a01:4f9:3081:5413::2", ""}, + // Runtime {"user=u search_path=abc", nil, "search_path=abc user=u", ""}, {"database=db", nil, "dbname=db", ``}, diff --git a/deprecated.go b/deprecated.go index 7ea816426..e0b4cab74 100644 --- a/deprecated.go +++ b/deprecated.go @@ -75,6 +75,10 @@ func ParseURL(url string) (string, error) { return convertURL(url) } type values map[string]string func (o values) network() (string, string) { + if ho := o["hostaddr"]; ho != "" { + return "tcp", net.JoinHostPort(ho, o["port"]) + } + host := o["host"] // UNIX domain sockets are either represented by an (absolute) file system // path or they live in the abstract name space (starting with an @).