Repository: jackc/pgx
Pull request #2008 (2024-05-10 06:03:10)

Add pgx pool acquire tracer
resolves https://github.com/jackc/pgx/issues/1747


Patch:
diff --git a/batch.go b/batch.go
index b9b46d1d..3540f57f 100644
--- a/batch.go
+++ b/batch.go
@@ -12,7 +12,7 @@ import (
 type QueuedQuery struct {
 	SQL       string
 	Arguments []any
-	fn        batchItemFunc
+	Fn        batchItemFunc
 	sd        *pgconn.StatementDescription
 }
 
@@ -20,7 +20,7 @@ type batchItemFunc func(br BatchResults) error
 
 // Query sets fn to be called when the response to qq is received.
 func (qq *QueuedQuery) Query(fn func(rows Rows) error) {
-	qq.fn = func(br BatchResults) error {
+	qq.Fn = func(br BatchResults) error {
 		rows, _ := br.Query()
 		defer rows.Close()
 
@@ -36,7 +36,7 @@ func (qq *QueuedQuery) Query(fn func(rows Rows) error) {
 
 // Query sets fn to be called when the response to qq is received.
 func (qq *QueuedQuery) QueryRow(fn func(row Row) error) {
-	qq.fn = func(br BatchResults) error {
+	qq.Fn = func(br BatchResults) error {
 		row := br.QueryRow()
 		return fn(row)
 	}
@@ -44,7 +44,7 @@ func (qq *QueuedQuery) QueryRow(fn func(row Row) error) {
 
 // Exec sets fn to be called when the response to qq is received.
 func (qq *QueuedQuery) Exec(fn func(ct pgconn.CommandTag) error) {
-	qq.fn = func(br BatchResults) error {
+	qq.Fn = func(br BatchResults) error {
 		ct, err := br.Exec()
 		if err != nil {
 			return err
@@ -228,8 +228,8 @@ func (br *batchResults) Close() error {
 
 	// Read and run fn for all remaining items
 	for br.err == nil && !br.closed && br.b != nil && br.qqIdx < len(br.b.QueuedQueries) {
-		if br.b.QueuedQueries[br.qqIdx].fn != nil {
-			err := br.b.QueuedQueries[br.qqIdx].fn(br)
+		if br.b.QueuedQueries[br.qqIdx].Fn != nil {
+			err := br.b.QueuedQueries[br.qqIdx].Fn(br)
 			if err != nil {
 				br.err = err
 			}
@@ -397,8 +397,8 @@ func (br *pipelineBatchResults) Close() error {
 
 	// Read and run fn for all remaining items
 	for br.err == nil && !br.closed && br.b != nil && br.qqIdx < len(br.b.QueuedQueries) {
-		if br.b.QueuedQueries[br.qqIdx].fn != nil {
-			err := br.b.QueuedQueries[br.qqIdx].fn(br)
+		if br.b.QueuedQueries[br.qqIdx].Fn != nil {
+			err := br.b.QueuedQueries[br.qqIdx].Fn(br)
 			if err != nil {
 				br.err = err
 			}
diff --git a/doc.go b/doc.go
index 911571b1..bc0391dd 100644
--- a/doc.go
+++ b/doc.go
@@ -11,9 +11,10 @@ The primary way of establishing a connection is with [pgx.Connect]:
 
     conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
 
-The database connection string can be in URL or DSN format. Both PostgreSQL settings and pgx settings can be specified
-here. In addition, a config struct can be created by [ParseConfig] and modified before establishing the connection with
-[ConnectConfig] to configure settings such as tracing that cannot be configured with a connection string.
+The database connection string can be in URL or key/value format. Both PostgreSQL settings and pgx settings can be
+specified here. In addition, a config struct can be created by [ParseConfig] and modified before establishing the
+connection with [ConnectConfig] to configure settings such as tracing that cannot be configured with a connection
+string.
 
 Connection Pool
 
@@ -23,8 +24,8 @@ github.com/jackc/pgx/v5/pgxpool for a concurrency safe connection pool.
 Query Interface
 
 pgx implements Query in the familiar database/sql style. However, pgx provides generic functions such as CollectRows and
-ForEachRow that are a simpler and safer way of processing rows than manually calling defer rows.Close(), rows.Next(), rows.Scan, and
-rows.Err().
+ForEachRow that are a simpler and safer way of processing rows than manually calling defer rows.Close(), rows.Next(),
+rows.Scan, and rows.Err().
 
 CollectRows can be used collect all returned rows into a slice.
 
diff --git a/large_objects.go b/large_objects.go
index a3028b63..9d21afdc 100644
--- a/large_objects.go
+++ b/large_objects.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"errors"
 	"io"
+
+	"github.com/jackc/pgx/v5/pgtype"
 )
 
 // The PostgreSQL wire protocol has a limit of 1 GB - 1 per message. See definition of
@@ -115,9 +117,10 @@ func (o *LargeObject) Read(p []byte) (int, error) {
 			expected = maxLargeObjectMessageLength
 		}
 
-		var res []byte
+		res := pgtype.PreallocBytes(p[nTotal:])
 		err := o.tx.QueryRow(o.ctx, "select loread($1, $2)", o.fd, expected).Scan(&res)
-		copy(p[nTotal:], res)
+		// We compute expected so that it always fits into p, so it should never happen
+		// that PreallocBytes's ScanBytes had to allocate a new slice.
 		nTotal += len(res)
 		if err != nil {
 			return nTotal, err
diff --git a/pgconn/config.go b/pgconn/config.go
index 95b7c912..7d1c6fba 100644
--- a/pgconn/config.go
+++ b/pgconn/config.go
@@ -118,6 +118,14 @@ type FallbackConfig struct {
 	TLSConfig *tls.Config // nil disables TLS
 }
 
+// connectOneConfig is the configuration for a single attempt to connect to a single host.
+type connectOneConfig struct {
+	network          string
+	address          string
+	originalHostname string      // original hostname before resolving
+	tlsConfig        *tls.Config // nil disables TLS
+}
+
 // isAbsolutePath checks if the provided value is an absolute path either
 // beginning with a forward slash (as on Linux-based systems) or with a capital
 // letter A-Z followed by a colon and a backslash, e.g., "C:\", (as on Windows).
@@ -152,11 +160,11 @@ func NetworkAddress(host string, port uint16) (network, address string) {
 
 // ParseConfig builds a *Config from connString with similar behavior to the PostgreSQL standard C library libpq. It
 // uses the same defaults as libpq (e.g. port=5432) and understands most PG* environment variables. ParseConfig closely
-// matches the parsing behavior of libpq. connString may either be in URL format or keyword = value format (DSN style).
-// See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING for details. connString also may be
-// empty to only read from the environment. If a password is not supplied it will attempt to read the .pgpass file.
+// matches the parsing behavior of libpq. connString may either be in URL format or keyword = value format. See
+// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING for details. connString also may be empty
+// to only read from the environment. If a password is not supplied it will attempt to read the .pgpass file.
 //
-//	# Example DSN
+//	# Example Keyword/Value
 //	user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca
 //
 //	# Example URL
@@ -175,7 +183,7 @@ func NetworkAddress(host string, port uint16) (network, address string) {
 //	postgres://jack:secret@foo.example.com:5432,bar.example.com:5432/mydb
 //
 // ParseConfig currently recognizes the following environment variable and their parameter key word equivalents passed
-// via database URL or DSN:
+// via database URL or keyword/value:
 //
 //	PGHOST
 //	PGPORT
@@ -239,16 +247,16 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con
 	connStringSettings := make(map[string]string)
 	if connString != "" {
 		var err error
-		// connString may be a database URL or a DSN
+		// connString may be a database URL or in PostgreSQL keyword/value format
 		if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") {
 			connStringSettings, err = parseURLSettings(connString)
 			if err != nil {
 				return nil, &ParseConfigError{ConnString: connString, msg: "failed to parse as URL", err: err}
 			}
 		} else {
-			connStringSettings, err = parseDSNSettings(connString)
+			connStringSettings, err = parseKeywordValueSettings(connString)
 			if err != nil {
-				return nil, &ParseConfigError{ConnString: connString, msg: "failed to parse as DSN", err: err}
+				return nil, &ParseConfigError{ConnString: connString, msg: "failed to parse as keyword/value", err: err}
 			}
 		}
 	}
@@ -526,7 +534,7 @@ func isIPOnly(host string) bool {
 
 var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
 
-func parseDSNSettings(s string) (map[string]string, error) {
+func parseKeywordValueSettings(s string) (map[string]string, error) {
 	settings := make(map[string]string)
 
 	nameMap := map[string]string{
@@ -537,7 +545,7 @@ func parseDSNSettings(s string) (map[string]string, error) {
 		var key, val string
 		eqIdx := strings.IndexRune(s, '=')
 		if eqIdx < 0 {
-			return nil, errors.New("invalid dsn")
+			return nil, errors.New("invalid keyword/value")
 		}
 
 		key = strings.Trim(s[:eqIdx], " \t\n\r\v\f")
@@ -589,7 +597,7 @@ func parseDSNSettings(s string) (map[string]string, error) {
 		}
 
 		if key == "" {
-			return nil, errors.New("invalid dsn")
+			return nil, errors.New("invalid keyword/value")
 		}
 
 		settings[key] = val
diff --git a/pgconn/doc.go b/pgconn/doc.go
index 8cf8688a..70137501 100644
--- a/pgconn/doc.go
+++ b/pgconn/doc.go
@@ -5,8 +5,8 @@ nearly the same level is the C library libpq.
 
 Establishing a Connection
 
-Use Connect to establish a connection. It accepts a connection string in URL or DSN and will read the environment for
-libpq style environment variables.
+Use Connect to establish a connection. It accepts a connection string in URL or keyword/value format and will read the
+environment for libpq style environment variables.
 
 Executing a Query
 
diff --git a/pgconn/errors.go b/pgconn/errors.go
index ecc9d332..ec4a6d47 100644
--- a/pgconn/errors.go
+++ b/pgconn/errors.go
@@ -62,23 +62,37 @@ func (pe *PgError) SQLState() string {
 // ConnectError is the error returned when a connection attempt fails.
 type ConnectError struct {
 	Config *Config // The configuration that was used in the connection attempt.
-	msg    string
 	err    error
 }
 
 func (e *ConnectError) Error() string {
-	sb := &strings.Builder{}
-	fmt.Fprintf(sb, "failed to connect to `host=%s user=%s database=%s`: %s", e.Config.Host, e.Config.User, e.Config.Database, e.msg)
-	if e.err != nil {
-		fmt.Fprintf(sb, " (%s)", e.err.Error())
+	prefix := fmt.Sprintf("failed to connect to `user=%s database=%s`:", e.Config.User, e.Config.Database)
+	details := e.err.Error()
+	if strings.Contains(details, "\n") {
+		return prefix + "\n\t" + strings.ReplaceAll(details, "\n", "\n\t")
+	} else {
+		return prefix + " " + details
 	}
-	return sb.String()
 }
 
 func (e *ConnectError) Unwrap() error {
 	return e.err
 }
 
+type perDialConnectError struct {
+	address          string
+	originalHostname string
+	err              error
+}
+
+func (e *perDialConnectError) Error() string {
+	return fmt.Sprintf("%s (%s): %s", e.address, e.originalHostname, e.err.Error())
+}
+
+func (e *perDialConnectError) Unwrap() error {
+	return e.err
+}
+
 type connLockError struct {
 	status string
 }
@@ -197,10 +211,10 @@ func redactPW(connString string) string {
 			return redactURL(u)
 		}
 	}
-	quotedDSN := regexp.MustCompile(`password='[^']*'`)
-	connString = quotedDSN.ReplaceAllLiteralString(connString, "password=xxxxx")
-	plainDSN := regexp.MustCompile(`password=[^ ]*`)
-	connString = plainDSN.ReplaceAllLiteralString(connString, "password=xxxxx")
+	quotedKV := regexp.MustCompile(`password='[^']*'`)
+	connString = quotedKV.ReplaceAllLiteralString(connString, "password=xxxxx")
+	plainKV := regexp.MustCompile(`password=[^ ]*`)
+	connString = plainKV.ReplaceAllLiteralString(connString, "password=xxxxx")
 	brokenURL := regexp.MustCompile(`:[^:@]+?@`)
 	connString = brokenURL.ReplaceAllLiteralString(connString, ":xxxxxx@")
 	return connString
diff --git a/pgconn/pgconn.go b/pgconn/pgconn.go
index 50587807..7efb522a 100644
--- a/pgconn/pgconn.go
+++ b/pgconn/pgconn.go
@@ -105,8 +105,9 @@ type PgConn struct {
 	cleanupDone chan struct{}
 }
 
-// Connect establishes a connection to a PostgreSQL server using the environment and connString (in URL or DSN format)
-// to provide configuration. See documentation for [ParseConfig] for details. ctx can be used to cancel a connect attempt.
+// Connect establishes a connection to a PostgreSQL server using the environment and connString (in URL or keyword/value
+// format) to provide configuration. See documentation for [ParseConfig] for details. ctx can be used to cancel a
+// connect attempt.
 func Connect(ctx context.Context, connString string) (*PgConn, error) {
 	config, err := ParseConfig(connString)
 	if err != nil {
@@ -116,9 +117,9 @@ func Connect(ctx context.Context, connString string) (*PgConn, error) {
 	return ConnectConfig(ctx, config)
 }
 
-// Connect establishes a connection to a PostgreSQL server using the environment and connString (in URL or DSN format)
-// and ParseConfigOptions to provide additional configuration. See documentation for [ParseConfig] for details. ctx can be
-// used to cancel a connect attempt.
+// Connect establishes a connection to a PostgreSQL server using the environment and connString (in URL or keyword/value
+// format) and ParseConfigOptions to provide additional configuration. See documentation for [ParseConfig] for details.
+// ctx can be used to cancel a connect attempt.
 func ConnectWithOptions(ctx context.Context, connString string, parseConfigOptions ParseConfigOptions) (*PgConn, error) {
 	config, err := ParseConfigWithOptions(connString, parseConfigOptions)
 	if err != nil {
@@ -133,113 +134,77 @@ func ConnectWithOptions(ctx context.Context, connString string, parseConfigOptio
 //
 // If config.Fallbacks are present they will sequentially be tried in case of error establishing network connection. An
 // authentication error will terminate the chain of attempts (like libpq:
-// https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS) and be returned as the error. Otherwise,
-// if all attempts fail the last error is returned.
-func ConnectConfig(octx context.Context, config *Config) (pgConn *PgConn, err error) {
+// https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS) and be returned as the error.
+func ConnectConfig(ctx context.Context, config *Config) (*PgConn, error) {
 	// Default values are set in ParseConfig. Enforce initial creation by ParseConfig rather than setting defaults from
 	// zero values.
 	if !config.createdByParseConfig {
 		panic("config must be created by ParseConfig")
 	}
 
-	// Simplify usage by treating primary config and fallbacks the same.
-	fallbackConfigs := []*FallbackConfig{
-		{
-			Host:      config.Host,
-			Port:      config.Port,
-			TLSConfig: config.TLSConfig,
-		},
-	}
-	fallbackConfigs = append(fallbackConfigs, config.Fallbacks...)
-	ctx := octx
-	fallbackConfigs, err = expandWithIPs(ctx, config.LookupFunc, fallbackConfigs)
-	if err != nil {
-		return nil, &ConnectError{Config: config, msg: "hostname resolving error", err: err}
-	}
-
-	if len(fallbackConfigs) == 0 {
-		return nil, &ConnectError{Config: config, msg: "hostname resolving error", err: errors.New("ip addr wasn't found")}
-	}
+	var allErrors []error
 
-	foundBestServer := false
-	var fallbackConfig *FallbackConfig
-	for i, fc := range fallbackConfigs {
-		// ConnectTimeout restricts the whole connection process.
-		if config.ConnectTimeout != 0 {
-			// create new context first time or when previous host was different
-			if i == 0 || (fallbackConfigs[i].Host != fallbackConfigs[i-1].Host) {
-				var cancel context.CancelFunc
-				ctx, cancel = context.WithTimeout(octx, config.ConnectTimeout)
-				defer cancel()
-			}
-		} else {
-			ctx = octx
-		}
-		pgConn, err = connect(ctx, config, fc, false)
-		if err == nil {
-			foundBestServer = true
-			break
-		} else if pgerr, ok := err.(*PgError); ok {
-			err = &ConnectError{Config: config, msg: "server error", err: pgerr}
-			const ERRCODE_INVALID_PASSWORD = "28P01"                    // wrong password
-			const ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION = "28000" // wrong password or bad pg_hba.conf settings
-			const ERRCODE_INVALID_CATALOG_NAME = "3D000"                // db does not exist
-			const ERRCODE_INSUFFICIENT_PRIVILEGE = "42501"              // missing connect privilege
-			if pgerr.Code == ERRCODE_INVALID_PASSWORD ||
-				pgerr.Code == ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION && fc.TLSConfig != nil ||
-				pgerr.Code == ERRCODE_INVALID_CATALOG_NAME ||
-				pgerr.Code == ERRCODE_INSUFFICIENT_PRIVILEGE {
-				break
-			}
-		} else if cerr, ok := err.(*ConnectError); ok {
-			if _, ok := cerr.err.(*NotPreferredError); ok {
-				fallbackConfig = fc
-			}
-		}
+	connectConfigs, errs := buildConnectOneConfigs(ctx, config)
+	if len(errs) > 0 {
+		allErrors = append(allErrors, errs...)
 	}
 
-	if !foundBestServer && fallbackConfig != nil {
-		pgConn, err = connect(ctx, config, fallbackConfig, true)
-		if pgerr, ok := err.(*PgError); ok {
-			err = &ConnectError{Config: config, msg: "server error", err: pgerr}
-		}
+	if len(connectConfigs) == 0 {
+		return nil, &ConnectError{Config: config, err: fmt.Errorf("hostname resolving error: %w", errors.Join(allErrors...))}
 	}
 
-	if err != nil {
-		return nil, err // no need to wrap in connectError because it will already be wrapped in all cases except PgError
+	pgConn, errs := connectPreferred(ctx, config, connectConfigs)
+	if len(errs) > 0 {
+		allErrors = append(allErrors, errs...)
+		return nil, &ConnectError{Config: config, err: errors.Join(allErrors...)}
 	}
 
 	if config.AfterConnect != nil {
 		err := config.AfterConnect(ctx, pgConn)
 		if err != nil {
 			pgConn.conn.Close()
-			return nil, &ConnectError{Config: config, msg: "AfterConnect error", err: err}
+			return nil, &ConnectError{Config: config, err: fmt.Errorf("AfterConnect error: %w", err)}
 		}
 	}
 
 	return pgConn, nil
 }
 
-func expandWithIPs(ctx context.Context, lookupFn LookupFunc, fallbacks []*FallbackConfig) ([]*FallbackConfig, error) {
-	var configs []*FallbackConfig
+// buildConnectOneConfigs resolves hostnames and builds a list of connectOneConfigs to try connecting to. It returns a
+// slice of successfully resolved connectOneConfigs and a slice of errors. It is possible for both slices to contain
+// values if some hosts were successfully resolved and others were not.
+func buildConnectOneConfigs(ctx context.Context, config *Config) ([]*connectOneConfig, []error) {
+	// Simplify usage by treating primary config and fallbacks the same.
+	fallbackConfigs := []*FallbackConfig{
+		{
+			Host:      config.Host,
+			Port:      config.Port,
+			TLSConfig: config.TLSConfig,
+		},
+	}
+	fallbackConfigs = append(fallbackConfigs, config.Fallbacks...)
+
+	var configs []*connectOneConfig
 
-	var lookupErrors []error
+	var allErrors []error
 
-	for _, fb := range fallbacks {
+	for _, fb := range fallbackConfigs {
 		// skip resolve for unix sockets
 		if isAbsolutePath(fb.Host) {
-			configs = append(configs, &FallbackConfig{
-				Host:      fb.Host,
-				Port:      fb.Port,
-				TLSConfig: fb.TLSConfig,
+			network, address := NetworkAddress(fb.Host, fb.Port)
+			configs = append(configs, &connectOneConfig{
+				network:          network,
+				address:          address,
+				originalHostname: fb.Host,
+				tlsConfig:        fb.TLSConfig,
 			})
 
 			continue
 		}
 
-		ips, err := lookupFn(ctx, fb.Host)
+		ips, err := config.LookupFunc(ctx, fb.Host)
 		if err != nil {
-			lookupErrors = append(lookupErrors, err)
+			allErrors = append(allErrors, err)
 			continue
 		}
 
@@ -248,33 +213,91 @@ func expandWithIPs(ctx context.Context, lookupFn LookupFunc, fallbacks []*Fallba
 			if err == nil {
 				port, err := strconv.ParseUint(splitPort, 10, 16)
 				if err != nil {
-					return nil, fmt.Errorf("error parsing port (%s) from lookup: %w", splitPort, err)
+					return nil, []error{fmt.Errorf("error parsing port (%s) from lookup: %w", splitPort, err)}
 				}
-				configs = append(configs, &FallbackConfig{
-					Host:      splitIP,
-					Port:      uint16(port),
-					TLSConfig: fb.TLSConfig,
+				network, address := NetworkAddress(splitIP, uint16(port))
+				configs = append(configs, &connectOneConfig{
+					network:          network,
+					address:          address,
+					originalHostname: fb.Host,
+					tlsConfig:        fb.TLSConfig,
 				})
 			} else {
-				configs = append(configs, &FallbackConfig{
-					Host:      ip,
-					Port:      fb.Port,
-					TLSConfig: fb.TLSConfig,
+				network, address := NetworkAddress(ip, fb.Port)
+				configs = append(configs, &connectOneConfig{
+					network:          network,
+					address:          address,
+					originalHostname: fb.Host,
+					tlsConfig:        fb.TLSConfig,
 				})
 			}
 		}
 	}
 
-	// See https://github.com/jackc/pgx/issues/1464. When Go 1.20 can be used in pgx consider using errors.Join so all
-	// errors are reported.
-	if len(configs) == 0 && len(lookupErrors) > 0 {
-		return nil, lookupErrors[0]
+	return configs, allErrors
+}
+
+// connectPreferred attempts to connect to the preferred host from connectOneConfigs. The connections are attempted in
+// order. If a connection is successful it is returned. If no connection is successful then all errors are returned. If
+// a connection attempt returns a [NotPreferredError], then that host will be used if no other hosts are successful.
+func connectPreferred(ctx context.Context, config *Config, connectOneConfigs []*connectOneConfig) (*PgConn, []error) {
+	octx := ctx
+	var allErrors []error
+
+	var fallbackConnectOneConfig *connectOneConfig
+	for i, c := range connectOneConfigs {
+		// ConnectTimeout restricts the whole connection process.
+		if config.ConnectTimeout != 0 {
+			// create new context first time or when previous host was different
+			if i == 0 || (connectOneConfigs[i].address != connectOneConfigs[i-1].address) {
+				var cancel context.CancelFunc
+				ctx, cancel = context.WithTimeout(octx, config.ConnectTimeout)
+				defer cancel()
+			}
+		} else {
+			ctx = octx
+		}
+
+		pgConn, err := connectOne(ctx, config, c, false)
+		if pgConn != nil {
+			return pgConn, nil
+		}
+
+		allErrors = append(allErrors, err)
+
+		var pgErr *PgError
+		if errors.As(err, &pgErr) {
+			const ERRCODE_INVALID_PASSWORD = "28P01"                    // wrong password
+			const ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION = "28000" // wrong password or bad pg_hba.conf settings
+			const ERRCODE_INVALID_CATALOG_NAME = "3D000"                // db does not exist
+			const ERRCODE_INSUFFICIENT_PRIVILEGE = "42501"              // missing connect privilege
+			if pgErr.Code == ERRCODE_INVALID_PASSWORD ||
+				pgErr.Code == ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION && c.tlsConfig != nil ||
+				pgErr.Code == ERRCODE_INVALID_CATALOG_NAME ||
+				pgErr.Code == ERRCODE_INSUFFICIENT_PRIVILEGE {
+				return nil, allErrors
+			}
+		}
+
+		var npErr *NotPreferredError
+		if errors.As(err, &npErr) {
+			fallbackConnectOneConfig = c
+		}
 	}
 
-	return configs, nil
+	if fallbackConnectOneConfig != nil {
+		pgConn, err := connectOne(ctx, config, fallbackConnectOneConfig, true)
+		if err == nil {
+			return pgConn, nil
+		}
+		allErrors = append(allErrors, err)
+	}
+
+	return nil, allErrors
 }
 
-func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig,
+// connectOne makes one connection attempt to a single host.
+func connectOne(ctx context.Context, config *Config, connectConfig *connectOneConfig,
 	ignoreNotPreferredErr bool,
 ) (*PgConn, error) {
 	pgConn := new(PgConn)
@@ -283,20 +306,26 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 	pgConn.customData = make(map[string]any)
 
 	var err error
-	network, address := NetworkAddress(fallbackConfig.Host, fallbackConfig.Port)
-	pgConn.conn, err = config.DialFunc(ctx, network, address)
+
+	newPerDialConnectError := func(msg string, err error) *perDialConnectError {
+		err = normalizeTimeoutError(ctx, err)
+		e := &perDialConnectError{address: connectConfig.address, originalHostname: connectConfig.originalHostname, err: fmt.Errorf("%s: %w", msg, err)}
+		return e
+	}
+
+	pgConn.conn, err = config.DialFunc(ctx, connectConfig.network, connectConfig.address)
 	if err != nil {
-		return nil, &ConnectError{Config: config, msg: "dial error", err: normalizeTimeoutError(ctx, err)}
+		return nil, newPerDialConnectError("dial error", err)
 	}
 
-	if fallbackConfig.TLSConfig != nil {
+	if connectConfig.tlsConfig != nil {
 		pgConn.contextWatcher = ctxwatch.NewContextWatcher(&DeadlineContextWatcherHandler{Conn: pgConn.conn})
 		pgConn.contextWatcher.Watch(ctx)
-		tlsConn, err := startTLS(pgConn.conn, fallbackConfig.TLSConfig)
+		tlsConn, err := startTLS(pgConn.conn, connectConfig.tlsConfig)
 		pgConn.contextWatcher.Unwatch() // Always unwatch `netConn` after TLS.
 		if err != nil {
 			pgConn.conn.Close()
-			return nil, &ConnectError{Config: config, msg: "tls error", err: normalizeTimeoutError(ctx, err)}
+			return nil, newPerDialConnectError("tls error", err)
 		}
 
 		pgConn.conn = tlsConn
@@ -337,7 +366,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 	pgConn.frontend.Send(&startupMsg)
 	if err := pgConn.flushWithPotentialWriteReadDeadlock(); err != nil {
 		pgConn.conn.Close()
-		return nil, &ConnectError{Config: config, msg: "failed to write startup message", err: normalizeTimeoutError(ctx, err)}
+		return nil, newPerDialConnectError("failed to write startup message", err)
 	}
 
 	for {
@@ -345,9 +374,9 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 		if err != nil {
 			pgConn.conn.Close()
 			if err, ok := err.(*PgError); ok {
-				return nil, err
+				return nil, newPerDialConnectError("server error", err)
 			}
-			return nil, &ConnectError{Config: config, msg: "failed to receive message", err: normalizeTimeoutError(ctx, err)}
+			return nil, newPerDialConnectError("failed to receive message", err)
 		}
 
 		switch msg := msg.(type) {
@@ -360,26 +389,26 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 			err = pgConn.txPasswordMessage(pgConn.config.Password)
 			if err != nil {
 				pgConn.conn.Close()
-				return nil, &ConnectError{Config: config, msg: "failed to write password message", err: err}
+				return nil, newPerDialConnectError("failed to write password message", err)
 			}
 		case *pgproto3.AuthenticationMD5Password:
 			digestedPassword := "md5" + hexMD5(hexMD5(pgConn.config.Password+pgConn.config.User)+string(msg.Salt[:]))
 			err = pgConn.txPasswordMessage(digestedPassword)
 			if err != nil {
 				pgConn.conn.Close()
-				return nil, &ConnectError{Config: config, msg: "failed to write password message", err: err}
+				return nil, newPerDialConnectError("failed to write password message", err)
 			}
 		case *pgproto3.AuthenticationSASL:
 			err = pgConn.scramAuth(msg.AuthMechanisms)
 			if err != nil {
 				pgConn.conn.Close()
-				return nil, &ConnectError{Config: config, msg: "failed SASL auth", err: err}
+				return nil, newPerDialConnectError("failed SASL auth", err)
 			}
 		case *pgproto3.AuthenticationGSS:
 			err = pgConn.gssAuth()
 			if err != nil {
 				pgConn.conn.Close()
-				return nil, &ConnectError{Config: config, msg: "failed GSS auth", err: err}
+				return nil, newPerDialConnectError("failed GSS auth", err)
 			}
 		case *pgproto3.ReadyForQuery:
 			pgConn.status = connStatusIdle
@@ -397,7 +426,7 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 						return pgConn, nil
 					}
 					pgConn.conn.Close()
-					return nil, &ConnectError{Config: config, msg: "ValidateConnect failed", err: err}
+					return nil, newPerDialConnectError("ValidateConnect failed", err)
 				}
 			}
 			return pgConn, nil
@@ -405,10 +434,10 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 			// handled by ReceiveMessage
 		case *pgproto3.ErrorResponse:
 			pgConn.conn.Close()
-			return nil, ErrorResponseToPgError(msg)
+			return nil, newPerDialConnectError("server error", ErrorResponseToPgError(msg))
 		default:
 			pgConn.conn.Close()
-			return nil, &ConnectError{Config: config, msg: "received unexpected message", err: err}
+			return nil, newPerDialConnectError("received unexpected message", err)
 		}
 	}
 }
diff --git a/pgtype/time.go b/pgtype/time.go
index a3d0ab1a..61a3abdf 100644
--- a/pgtype/time.go
+++ b/pgtype/time.go
@@ -141,6 +141,8 @@ func (TimeCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan
 		switch target.(type) {
 		case TimeScanner:
 			return scanPlanBinaryTimeToTimeScanner{}
+		case TextScanner:
+			return scanPlanBinaryTimeToTextScanner{}
 		}
 	case TextFormatCode:
 		switch target.(type) {
@@ -170,6 +172,34 @@ func (scanPlanBinaryTimeToTimeScanner) Scan(src []byte, dst any) error {
 	return scanner.ScanTime(Time{Microseconds: usec, Valid: true})
 }
 
+type scanPlanBinaryTimeToTextScanner struct{}
+
+func (scanPlanBinaryTimeToTextScanner) Scan(src []byte, dst any) error {
+	ts, ok := (dst).(TextScanner)
+	if !ok {
+		return ErrScanTargetTypeChanged
+	}
+
+	if src == nil {
+		return ts.ScanText(Text{})
+	}
+
+	if len(src) != 8 {
+		return fmt.Errorf("invalid length for time: %v", len(src))
+	}
+
+	usec := int64(binary.BigEndian.Uint64(src))
+
+	tim := Time{Microseconds: usec, Valid: true}
+
+	buf, err := TimeCodec{}.PlanEncode(nil, 0, TextFormatCode, tim).Encode(tim, nil)
+	if err != nil {
+		return err
+	}
+
+	return ts.ScanText(Text{String: string(buf), Valid: true})
+}
+
 type scanPlanTextAnyToTimeScanner struct{}
 
 func (scanPlanTextAnyToTimeScanner) Scan(src []byte, dst any) error {
diff --git a/pgxpool/conn.go b/pgxpool/conn.go
index 36f90969..38c90f3d 100644
--- a/pgxpool/conn.go
+++ b/pgxpool/conn.go
@@ -26,6 +26,10 @@ func (c *Conn) Release() {
 	res := c.res
 	c.res = nil
 
+	if c.p.releaseTracer != nil {
+		c.p.releaseTracer.TraceRelease(c.p, TraceReleaseData{Conn: conn})
+	}
+
 	if conn.IsClosed() || conn.PgConn().IsBusy() || conn.PgConn().TxStatus() != 'I' {
 		res.Destroy()
 		// Signal to the health check to run since we just destroyed a connections
diff --git a/pgxpool/doc.go b/pgxpool/doc.go
index 06cc63d5..099443bc 100644
--- a/pgxpool/doc.go
+++ b/pgxpool/doc.go
@@ -8,7 +8,7 @@ The primary way of creating a pool is with [pgxpool.New]:
 
     pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
 
-The database connection string can be in URL or DSN format. PostgreSQL settings, pgx settings, and pool settings can be
+The database connection string can be in URL or keyword/value format. PostgreSQL settings, pgx settings, and pool settings can be
 specified here. In addition, a config struct can be created by [ParseConfig].
 
     config, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
diff --git a/pgxpool/pool.go b/pgxpool/pool.go
index 9f74805e..fdcba724 100644
--- a/pgxpool/pool.go
+++ b/pgxpool/pool.go
@@ -95,6 +95,9 @@ type Pool struct {
 
 	healthCheckChan chan struct{}
 
+	acquireTracer AcquireTracer
+	releaseTracer ReleaseTracer
+
 	closeOnce sync.Once
 	closeChan chan struct{}
 }
@@ -195,6 +198,14 @@ func NewWithConfig(ctx context.Context, config *Config) (*Pool, error) {
 		closeChan:             make(chan struct{}),
 	}
 
+	if t, ok := config.ConnConfig.Tracer.(AcquireTracer); ok {
+		p.acquireTracer = t
+	}
+
+	if t, ok := config.ConnConfig.Tracer.(ReleaseTracer); ok {
+		p.releaseTracer = t
+	}
+
 	var err error
 	p.p, err = puddle.NewPool(
 		&puddle.Config[*connResource]{
@@ -279,7 +290,7 @@ func NewWithConfig(ctx context.Context, config *Config) (*Pool, error) {
 //
 // See Config for definitions of these arguments.
 //
-//	# Example DSN
+//	# Example Keyword/Value
 //	user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca pool_max_conns=10
 //
 //	# Example URL
@@ -498,7 +509,18 @@ func (p *Pool) createIdleResources(parentCtx context.Context, targetResources in
 }
 
 // Acquire returns a connection (*Conn) from the Pool
-func (p *Pool) Acquire(ctx context.Context) (*Conn, error) {
+func (p *Pool) Acquire(ctx context.Context) (c *Conn, err error) {
+	if p.acquireTracer != nil {
+		ctx = p.acquireTracer.TraceAcquireStart(ctx, p, TraceAcquireStartData{})
+		defer func() {
+			var conn *pgx.Conn
+			if c != nil {
+				conn = c.Conn()
+			}
+			p.acquireTracer.TraceAcquireEnd(ctx, p, TraceAcquireEndData{Conn: conn, Err: err})
+		}()
+	}
+
 	for {
 		res, err := p.p.Acquire(ctx)
 		if err != nil {
diff --git a/pgxpool/tracer.go b/pgxpool/tracer.go
new file mode 100644
index 00000000..78b9d15a
--- /dev/null
+++ b/pgxpool/tracer.go
@@ -0,0 +1,33 @@
+package pgxpool
+
+import (
+	"context"
+
+	"github.com/jackc/pgx/v5"
+)
+
+// AcquireTracer traces Acquire.
+type AcquireTracer interface {
+	// TraceAcquireStart is called at the beginning of Acquire.
+	// The returned context is used for the rest of the call and will be passed to the TraceAcquireEnd.
+	TraceAcquireStart(ctx context.Context, pool *Pool, data TraceAcquireStartData) context.Context
+	// TraceAcquireEnd is called when a connection has been acquired.
+	TraceAcquireEnd(ctx context.Context, pool *Pool, data TraceAcquireEndData)
+}
+
+type TraceAcquireStartData struct{}
+
+type TraceAcquireEndData struct {
+	Conn *pgx.Conn
+	Err  error
+}
+
+// ReleaseTracer traces Release.
+type ReleaseTracer interface {
+	// TraceRelease is called at the beginning of Release.
+	TraceRelease(pool *Pool, data TraceReleaseData)
+}
+
+type TraceReleaseData struct {
+	Conn *pgx.Conn
+}
diff --git a/stdlib/sql.go b/stdlib/sql.go
index 3d65e23a..29cd3fbb 100644
--- a/stdlib/sql.go
+++ b/stdlib/sql.go
@@ -7,7 +7,7 @@
 //	  return err
 //	}
 //
-// Or from a DSN string.
+// Or from a keyword/value string.
 //
 //	db, err := sql.Open("pgx", "user=postgres password=secret host=localhost port=5432 database=pgx_test sslmode=disable")
 //	if err != nil {


Test patch:
diff --git a/pgconn/config_test.go b/pgconn/config_test.go
index 99cc6e1a..2a45bf71 100644
--- a/pgconn/config_test.go
+++ b/pgconn/config_test.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"os"
 	"os/user"
+	"path/filepath"
 	"runtime"
 	"strconv"
 	"strings"
@@ -336,7 +337,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN everything",
+			name:       "Key/value everything",
 			connString: "user=jack password=secret host=localhost port=5432 dbname=mydb sslmode=disable application_name=pgxtest search_path=myschema connect_timeout=5",
 			config: &pgconn.Config{
 				User:           "jack",
@@ -353,7 +354,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN with escaped single quote",
+			name:       "Key/value with escaped single quote",
 			connString: "user=jack\\'s password=secret host=localhost port=5432 dbname=mydb sslmode=disable",
 			config: &pgconn.Config{
 				User:          "jack's",
@@ -366,7 +367,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN with escaped backslash",
+			name:       "Key/value with escaped backslash",
 			connString: "user=jack password=sooper\\\\secret host=localhost port=5432 dbname=mydb sslmode=disable",
 			config: &pgconn.Config{
 				User:          "jack",
@@ -379,7 +380,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN with single quoted values",
+			name:       "Key/value with single quoted values",
 			connString: "user='jack' host='localhost' dbname='mydb' sslmode='disable'",
 			config: &pgconn.Config{
 				User:          "jack",
@@ -391,7 +392,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN with single quoted value with escaped single quote",
+			name:       "Key/value with single quoted value with escaped single quote",
 			connString: "user='jack\\'s' host='localhost' dbname='mydb' sslmode='disable'",
 			config: &pgconn.Config{
 				User:          "jack's",
@@ -403,7 +404,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN with empty single quoted value",
+			name:       "Key/value with empty single quoted value",
 			connString: "user='jack' password='' host='localhost' dbname='mydb' sslmode='disable'",
 			config: &pgconn.Config{
 				User:          "jack",
@@ -415,7 +416,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN with space between key and value",
+			name:       "Key/value with space between key and value",
 			connString: "user = 'jack' password = '' host = 'localhost' dbname = 'mydb' sslmode='disable'",
 			config: &pgconn.Config{
 				User:          "jack",
@@ -491,7 +492,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN multiple hosts one port",
+			name:       "Key/value multiple hosts one port",
 			connString: "user=jack password=secret host=foo,bar,baz port=5432 dbname=mydb sslmode=disable",
 			config: &pgconn.Config{
 				User:          "jack",
@@ -516,7 +517,7 @@ func TestParseConfig(t *testing.T) {
 			},
 		},
 		{
-			name:       "DSN multiple hosts multiple ports",
+			name:       "Key/value multiple hosts multiple ports",
 			connString: "user=jack password=secret host=foo,bar,baz port=1,2,3 dbname=mydb sslmode=disable",
 			config: &pgconn.Config{
 				User:          "jack",
@@ -772,18 +773,18 @@ func TestParseConfig(t *testing.T) {
 }
 
 // https://github.com/jackc/pgconn/issues/47
-func TestParseConfigDSNWithTrailingEmptyEqualDoesNotPanic(t *testing.T) {
+func TestParseConfigKVWithTrailingEmptyEqualDoesNotPanic(t *testing.T) {
 	_, err := pgconn.ParseConfig("host= user= password= port= database=")
 	require.NoError(t, err)
 }
 
-func TestParseConfigDSNLeadingEqual(t *testing.T) {
+func TestParseConfigKVLeadingEqual(t *testing.T) {
 	_, err := pgconn.ParseConfig("= user=jack")
 	require.Error(t, err)
 }
 
 // https://github.com/jackc/pgconn/issues/49
-func TestParseConfigDSNTrailingBackslash(t *testing.T) {
+func TestParseConfigKVTrailingBackslash(t *testing.T) {
 	_, err := pgconn.ParseConfig(`x=x\`)
 	require.Error(t, err)
 	assert.Contains(t, err.Error(), "invalid backslash")
@@ -933,19 +934,6 @@ func TestParseConfigEnvLibpq(t *testing.T) {
 
 	pgEnvvars := []string{"PGHOST", "PGPORT", "PGDATABASE", "PGUSER", "PGPASSWORD", "PGAPPNAME", "PGSSLMODE", "PGCONNECT_TIMEOUT", "PGSSLSNI"}
 
-	savedEnv := make(map[string]string)
-	for _, n := range pgEnvvars {
-		savedEnv[n] = os.Getenv(n)
-	}
-	defer func() {
-		for k, v := range savedEnv {
-			err := os.Setenv(k, v)
-			if err != nil {
-				t.Fatalf("Unable to restore environment: %v", err)
-			}
-		}
-	}()
-
 	tests := []struct {
 		name    string
 		envvars map[string]string
@@ -1021,8 +1009,7 @@ func TestParseConfigEnvLibpq(t *testing.T) {
 		}
 
 		for k, v := range tt.envvars {
-			err := os.Setenv(k, v)
-			require.NoError(t, err)
+			t.Setenv(k, v)
 		}
 
 		config, err := pgconn.ParseConfig("")
@@ -1038,16 +1025,11 @@ func TestParseConfigReadsPgPassfile(t *testing.T) {
 	skipOnWindows(t)
 	t.Parallel()
 
-	tf, err := os.CreateTemp("", "")
+	tfName := filepath.Join(t.TempDir(), "config")
+	err := os.WriteFile(tfName, []byte("test1:5432:curlydb:curly:nyuknyuknyuk"), 0600)
 	require.NoError(t, err)
 
-	defer tf.Close()
-	defer os.Remove(tf.Name())
-
-	_, err = tf.Write([]byte("test1:5432:curlydb:curly:nyuknyuknyuk"))
-	require.NoError(t, err)
-
-	connString := fmt.Sprintf("postgres://curly@test1:5432/curlydb?sslmode=disable&passfile=%s", tf.Name())
+	connString := fmt.Sprintf("postgres://curly@test1:5432/curlydb?sslmode=disable&passfile=%s", tfName)
 	expected := &pgconn.Config{
 		User:          "curly",
 		Password:      "nyuknyuknyuk",
@@ -1068,13 +1050,9 @@ func TestParseConfigReadsPgServiceFile(t *testing.T) {
 	skipOnWindows(t)
 	t.Parallel()
 
-	tf, err := os.CreateTemp("", "")
-	require.NoError(t, err)
-
-	defer tf.Close()
-	defer os.Remove(tf.Name())
+	tfName := filepath.Join(t.TempDir(), "config")
 
-	_, err = tf.Write([]byte(`
+	err := os.WriteFile(tfName, []byte(`
 [abc]
 host=abc.example.com
 port=9999
@@ -1086,7 +1064,7 @@ host = def.example.com
 dbname = defdb
 user = defuser
 application_name = spaced string
-`))
+`), 0600)
 	require.NoError(t, err)
 
 	defaultPort := getDefaultPort(t)
@@ -1098,7 +1076,7 @@ application_name = spaced string
 	}{
 		{
 			name:       "abc",
-			connString: fmt.Sprintf("postgres:///?servicefile=%s&service=%s", tf.Name(), "abc"),
+			connString: fmt.Sprintf("postgres:///?servicefile=%s&service=%s", tfName, "abc"),
 			config: &pgconn.Config{
 				Host:     "abc.example.com",
 				Database: "abcdb",
@@ -1120,7 +1098,7 @@ application_name = spaced string
 		},
 		{
 			name:       "def",
-			connString: fmt.Sprintf("postgres:///?servicefile=%s&service=%s", tf.Name(), "def"),
+			connString: fmt.Sprintf("postgres:///?servicefile=%s&service=%s", tfName, "def"),
 			config: &pgconn.Config{
 				Host:     "def.example.com",
 				Port:     defaultPort,
@@ -1142,7 +1120,7 @@ application_name = spaced string
 		},
 		{
 			name:       "conn string has precedence",
-			connString: fmt.Sprintf("postgres://other.example.com:7777/?servicefile=%s&service=%s&sslmode=disable", tf.Name(), "abc"),
+			connString: fmt.Sprintf("postgres://other.example.com:7777/?servicefile=%s&service=%s&sslmode=disable", tfName, "abc"),
 			config: &pgconn.Config{
 				Host:          "other.example.com",
 				Database:      "abcdb",
diff --git a/pgconn/errors_test.go b/pgconn/errors_test.go
index 171445cc..bbbfeb3c 100644
--- a/pgconn/errors_test.go
+++ b/pgconn/errors_test.go
@@ -19,12 +19,12 @@ func TestConfigError(t *testing.T) {
 			expectedMsg: "cannot parse `postgresql://foo:xxxxx@host`: msg",
 		},
 		{
-			name:        "dsn with password unquoted",
+			name:        "keyword/value with password unquoted",
 			err:         pgconn.NewParseConfigError("host=host password=password user=user", "msg", nil),
 			expectedMsg: "cannot parse `host=host password=xxxxx user=user`: msg",
 		},
 		{
-			name:        "dsn with password quoted",
+			name:        "keyword/value with password quoted",
 			err:         pgconn.NewParseConfigError("host=host password='pass word' user=user", "msg", nil),
 			expectedMsg: "cannot parse `host=host password=xxxxx user=user`: msg",
 		},
diff --git a/pgconn/pgconn_test.go b/pgconn/pgconn_test.go
index 285730d9..a53061d1 100644
--- a/pgconn/pgconn_test.go
+++ b/pgconn/pgconn_test.go
@@ -357,10 +357,8 @@ func TestConnectInvalidUser(t *testing.T) {
 
 	_, err = pgconn.ConnectConfig(ctx, config)
 	require.Error(t, err)
-	pgErr, ok := errors.Unwrap(err).(*pgconn.PgError)
-	if !ok {
-		t.Fatalf("Expected to receive a wrapped PgError, instead received: %v", err)
-	}
+	var pgErr *pgconn.PgError
+	require.ErrorAs(t, err, &pgErr)
 	if pgErr.Code != "28000" && pgErr.Code != "28P01" {
 		t.Fatalf("Expected to receive a PgError with code 28000 or 28P01, instead received: %v", pgErr)
 	}
@@ -529,6 +527,22 @@ func TestConnectWithFallback(t *testing.T) {
 	closeConn(t, conn)
 }
 
+func TestConnectFailsWithResolveFailureAndFailedConnectionAttempts(t *testing.T) {
+	t.Parallel()
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	conn, err := pgconn.Connect(ctx, "host=localhost,127.0.0.1,foo.invalid port=1,2,3 sslmode=disable")
+	require.Error(t, err)
+	require.Nil(t, conn)
+
+	require.ErrorContains(t, err, "lookup foo.invalid")
+	// Not testing the entire string as depending on IPv4 or IPv6 support localhost may resolve to 127.0.0.1 or ::1.
+	require.ErrorContains(t, err, ":1 (localhost): dial error:")
+	require.ErrorContains(t, err, ":2 (127.0.0.1): dial error:")
+}
+
 func TestConnectWithValidateConnect(t *testing.T) {
 	t.Parallel()
 
@@ -2700,7 +2714,7 @@ func TestPipelinePrepare(t *testing.T) {
 	sd, ok := results.(*pgconn.StatementDescription)
 	require.Truef(t, ok, "expected StatementDescription, got: %#v", results)
 	require.Len(t, sd.Fields, 1)
-	require.Equal(t, string(sd.Fields[0].Name), "a")
+	require.Equal(t, "a", string(sd.Fields[0].Name))
 	require.Equal(t, []uint32{pgtype.Int8OID}, sd.ParamOIDs)
 
 	results, err = pipeline.GetResults()
@@ -2708,7 +2722,7 @@ func TestPipelinePrepare(t *testing.T) {
 	sd, ok = results.(*pgconn.StatementDescription)
 	require.Truef(t, ok, "expected StatementDescription, got: %#v", results)
 	require.Len(t, sd.Fields, 1)
-	require.Equal(t, string(sd.Fields[0].Name), "b")
+	require.Equal(t, "b", string(sd.Fields[0].Name))
 	require.Equal(t, []uint32{pgtype.TextOID}, sd.ParamOIDs)
 
 	results, err = pipeline.GetResults()
@@ -2716,7 +2730,7 @@ func TestPipelinePrepare(t *testing.T) {
 	sd, ok = results.(*pgconn.StatementDescription)
 	require.Truef(t, ok, "expected StatementDescription, got: %#v", results)
 	require.Len(t, sd.Fields, 1)
-	require.Equal(t, string(sd.Fields[0].Name), "c")
+	require.Equal(t, "c", string(sd.Fields[0].Name))
 	require.Equal(t, []uint32{}, sd.ParamOIDs)
 
 	results, err = pipeline.GetResults()
@@ -2770,7 +2784,7 @@ func TestPipelinePrepareError(t *testing.T) {
 	sd, ok := results.(*pgconn.StatementDescription)
 	require.Truef(t, ok, "expected StatementDescription, got: %#v", results)
 	require.Len(t, sd.Fields, 1)
-	require.Equal(t, string(sd.Fields[0].Name), "a")
+	require.Equal(t, "a", string(sd.Fields[0].Name))
 	require.Equal(t, []uint32{pgtype.Int8OID}, sd.ParamOIDs)
 
 	results, err = pipeline.GetResults()
@@ -2814,7 +2828,7 @@ func TestPipelinePrepareAndDeallocate(t *testing.T) {
 	sd, ok := results.(*pgconn.StatementDescription)
 	require.Truef(t, ok, "expected StatementDescription, got: %#v", results)
 	require.Len(t, sd.Fields, 1)
-	require.Equal(t, string(sd.Fields[0].Name), "a")
+	require.Equal(t, "a", string(sd.Fields[0].Name))
 	require.Equal(t, []uint32{pgtype.Int8OID}, sd.ParamOIDs)
 
 	results, err = pipeline.GetResults()
@@ -2951,7 +2965,7 @@ func TestPipelinePrepareQuery(t *testing.T) {
 	sd, ok := results.(*pgconn.StatementDescription)
 	require.Truef(t, ok, "expected StatementDescription, got: %#v", results)
 	require.Len(t, sd.Fields, 1)
-	require.Equal(t, string(sd.Fields[0].Name), "msg")
+	require.Equal(t, "msg", string(sd.Fields[0].Name))
 	require.Equal(t, []uint32{pgtype.TextOID}, sd.ParamOIDs)
 
 	results, err = pipeline.GetResults()
@@ -3413,9 +3427,9 @@ func TestSNISupport(t *testing.T) {
 			select {
 			case sniHost := <-serverSNINameChan:
 				if tt.sni_set {
-					require.Equal(t, sniHost, "localhost")
+					require.Equal(t, "localhost", sniHost)
 				} else {
-					require.Equal(t, sniHost, "")
+					require.Equal(t, "", sniHost)
 				}
 			case err = <-serverErrChan:
 				t.Fatalf("server failed with error: %+v", err)
diff --git a/pgtype/bytea_test.go b/pgtype/bytea_test.go
index 4985e7a9..ccd147f6 100644
--- a/pgtype/bytea_test.go
+++ b/pgtype/bytea_test.go
@@ -92,7 +92,7 @@ func TestPreallocBytes(t *testing.T) {
 
 		require.Len(t, buf, 2)
 		require.Equal(t, 4, cap(buf))
-		require.Equal(t, buf, []byte{1, 2})
+		require.Equal(t, []byte{1, 2}, buf)
 
 		require.Equal(t, []byte{1, 2, 7, 8}, origBuf)
 
@@ -112,7 +112,7 @@ func TestUndecodedBytes(t *testing.T) {
 		require.NoError(t, err)
 
 		require.Len(t, buf, 4)
-		require.Equal(t, buf, []byte{0, 0, 0, 1})
+		require.Equal(t, []byte{0, 0, 0, 1}, buf)
 	})
 }
 
@@ -132,6 +132,6 @@ func TestByteaCodecDecodeDatabaseSQLValue(t *testing.T) {
 		require.NoError(t, err)
 
 		require.Len(t, buf, 4)
-		require.Equal(t, buf, []byte{0xa1, 0xb2, 0xc3, 0xd4})
+		require.Equal(t, []byte{0xa1, 0xb2, 0xc3, 0xd4}, buf)
 	})
 }
diff --git a/pgtype/enum_codec_test.go b/pgtype/enum_codec_test.go
index d064d49c..02f13ebe 100644
--- a/pgtype/enum_codec_test.go
+++ b/pgtype/enum_codec_test.go
@@ -64,6 +64,6 @@ create type enum_test as enum ('foo', 'bar', 'baz');`)
 		require.True(t, rows.Next())
 		values, err := rows.Values()
 		require.NoError(t, err)
-		require.Equal(t, values, []any{"foo"})
+		require.Equal(t, []any{"foo"}, values)
 	})
 }
diff --git a/pgtype/pgtype_test.go b/pgtype/pgtype_test.go
index b670e92b..e8613aee 100644
--- a/pgtype/pgtype_test.go
+++ b/pgtype/pgtype_test.go
@@ -309,7 +309,7 @@ func TestPointerPointerStructScan(t *testing.T) {
 	plan := m.PlanScan(pgt.OID, pgtype.TextFormatCode, &c)
 	err := plan.Scan([]byte("(1)"), &c)
 	require.NoError(t, err)
-	require.Equal(t, c.ID, 1)
+	require.Equal(t, 1, c.ID)
 }
 
 // https://github.com/jackc/pgx/issues/1263
diff --git a/pgxpool/tracer_test.go b/pgxpool/tracer_test.go
new file mode 100644
index 00000000..10724d94
--- /dev/null
+++ b/pgxpool/tracer_test.go
@@ -0,0 +1,130 @@
+package pgxpool_test
+
+import (
+	"context"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/pgxpool"
+	"github.com/stretchr/testify/require"
+)
+
+type testTracer struct {
+	traceAcquireStart func(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireStartData) context.Context
+	traceAcquireEnd   func(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireEndData)
+	traceRelease      func(pool *pgxpool.Pool, data pgxpool.TraceReleaseData)
+}
+
+type ctxKey string
+
+func (tt *testTracer) TraceAcquireStart(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireStartData) context.Context {
+	if tt.traceAcquireStart != nil {
+		return tt.traceAcquireStart(ctx, pool, data)
+	}
+	return ctx
+}
+
+func (tt *testTracer) TraceAcquireEnd(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireEndData) {
+	if tt.traceAcquireEnd != nil {
+		tt.traceAcquireEnd(ctx, pool, data)
+	}
+}
+
+func (tt *testTracer) TraceRelease(pool *pgxpool.Pool, data pgxpool.TraceReleaseData) {
+	if tt.traceRelease != nil {
+		tt.traceRelease(pool, data)
+	}
+}
+
+func (tt *testTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context {
+	return ctx
+}
+
+func (tt *testTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) {
+}
+
+func TestTraceAcquire(t *testing.T) {
+	t.Parallel()
+
+	tracer := &testTracer{}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
+	defer cancel()
+
+	config, err := pgxpool.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+	require.NoError(t, err)
+	config.ConnConfig.Tracer = tracer
+
+	pool, err := pgxpool.NewWithConfig(ctx, config)
+	require.NoError(t, err)
+	defer pool.Close()
+
+	traceAcquireStartCalled := false
+	tracer.traceAcquireStart = func(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireStartData) context.Context {
+		traceAcquireStartCalled = true
+		require.NotNil(t, pool)
+		return context.WithValue(ctx, ctxKey("fromTraceAcquireStart"), "foo")
+	}
+
+	traceAcquireEndCalled := false
+	tracer.traceAcquireEnd = func(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireEndData) {
+		traceAcquireEndCalled = true
+		require.Equal(t, "foo", ctx.Value(ctxKey("fromTraceAcquireStart")))
+		require.NotNil(t, pool)
+		require.NotNil(t, data.Conn)
+		require.NoError(t, data.Err)
+	}
+
+	c, err := pool.Acquire(ctx)
+	require.NoError(t, err)
+	defer c.Release()
+	require.True(t, traceAcquireStartCalled)
+	require.True(t, traceAcquireEndCalled)
+
+	traceAcquireStartCalled = false
+	traceAcquireEndCalled = false
+	tracer.traceAcquireEnd = func(ctx context.Context, pool *pgxpool.Pool, data pgxpool.TraceAcquireEndData) {
+		traceAcquireEndCalled = true
+		require.NotNil(t, pool)
+		require.Nil(t, data.Conn)
+		require.Error(t, data.Err)
+	}
+
+	ctx, cancel = context.WithCancel(ctx)
+	cancel()
+	_, err = pool.Acquire(ctx)
+	require.ErrorIs(t, err, context.Canceled)
+	require.True(t, traceAcquireStartCalled)
+	require.True(t, traceAcquireEndCalled)
+}
+
+func TestTraceRelease(t *testing.T) {
+	t.Parallel()
+
+	tracer := &testTracer{}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
+	defer cancel()
+
+	config, err := pgxpool.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+	require.NoError(t, err)
+	config.ConnConfig.Tracer = tracer
+
+	pool, err := pgxpool.NewWithConfig(ctx, config)
+	require.NoError(t, err)
+	defer pool.Close()
+
+	traceReleaseCalled := false
+	tracer.traceRelease = func(pool *pgxpool.Pool, data pgxpool.TraceReleaseData) {
+		traceReleaseCalled = true
+		require.NotNil(t, pool)
+		require.NotNil(t, data.Conn)
+	}
+
+	c, err := pool.Acquire(ctx)
+	require.NoError(t, err)
+	c.Release()
+	require.True(t, traceReleaseCalled)
+}