Repository: jackc/pgx
Pull request #1948 (2024-03-16 18:20:02)

Add ScanLocation to Timestamp and Timestamptz
This setting allows configuring the `time.Location` of `time.Time` values scanned from PostgreSQL `timestamp` and `timestamptz`.

For `timestamptz` it changes the display location but the instant in time is unchanged. That is, regardless of the value of `ScanLocation`, the results of `time.Time.Equal()` will be the same.

For `timestamp` it changes the location the time is assumed to have been in. That is, changing the value of `ScanLocation` will change the results of `time.Time.Equal()`.




Patch:
diff --git a/conn.go b/conn.go
index fc72c732..a9cb3163 100644
--- a/conn.go
+++ b/conn.go
@@ -624,7 +624,7 @@ const (
 	// to execute. It does not use named prepared statements. But it does use the unnamed prepared statement to get the
 	// statement description on the first round trip and then uses it to execute the query on the second round trip. This
 	// may cause problems with connection poolers that switch the underlying connection between round trips. It is safe
-	// even when the the database schema is modified concurrently.
+	// even when the database schema is modified concurrently.
 	QueryExecModeDescribeExec
 
 	// Assume the PostgreSQL query parameter types based on the Go type of the arguments. This uses the extended protocol
diff --git a/pgconn/config.go b/pgconn/config.go
index 33a72257..95b7c912 100644
--- a/pgconn/config.go
+++ b/pgconn/config.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/jackc/pgpassfile"
 	"github.com/jackc/pgservicefile"
+	"github.com/jackc/pgx/v5/pgconn/ctxwatch"
 	"github.com/jackc/pgx/v5/pgproto3"
 )
 
@@ -39,7 +40,12 @@ type Config struct {
 	DialFunc       DialFunc   // e.g. net.Dialer.DialContext
 	LookupFunc     LookupFunc // e.g. net.Resolver.LookupHost
 	BuildFrontend  BuildFrontendFunc
-	RuntimeParams  map[string]string // Run-time parameters to set on connection as session default values (e.g. search_path or application_name)
+
+	// BuildContextWatcherHandler is called to create a ContextWatcherHandler for a connection. The handler is called
+	// when a context passed to a PgConn method is canceled.
+	BuildContextWatcherHandler func(*PgConn) ctxwatch.Handler
+
+	RuntimeParams map[string]string // Run-time parameters to set on connection as session default values (e.g. search_path or application_name)
 
 	KerberosSrvName string
 	KerberosSpn     string
@@ -70,7 +76,7 @@ type Config struct {
 
 // ParseConfigOptions contains options that control how a config is built such as GetSSLPassword.
 type ParseConfigOptions struct {
-	// GetSSLPassword gets the password to decrypt a SSL client certificate. This is analogous to the the libpq function
+	// GetSSLPassword gets the password to decrypt a SSL client certificate. This is analogous to the libpq function
 	// PQsetSSLKeyPassHook_OpenSSL.
 	GetSSLPassword GetSSLPasswordFunc
 }
@@ -266,6 +272,9 @@ func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Con
 		BuildFrontend: func(r io.Reader, w io.Writer) *pgproto3.Frontend {
 			return pgproto3.NewFrontend(r, w)
 		},
+		BuildContextWatcherHandler: func(pgConn *PgConn) ctxwatch.Handler {
+			return &DeadlineContextWatcherHandler{Conn: pgConn.conn}
+		},
 		OnPgError: func(_ *PgConn, pgErr *PgError) bool {
 			// we want to automatically close any fatal errors
 			if strings.EqualFold(pgErr.Severity, "FATAL") {
diff --git a/pgconn/internal/ctxwatch/context_watcher.go b/pgconn/ctxwatch/context_watcher.go
similarity index 71%
rename from pgconn/internal/ctxwatch/context_watcher.go
rename to pgconn/ctxwatch/context_watcher.go
index b39cb3ee..db8884eb 100644
--- a/pgconn/internal/ctxwatch/context_watcher.go
+++ b/pgconn/ctxwatch/context_watcher.go
@@ -8,9 +8,8 @@ import (
 // ContextWatcher watches a context and performs an action when the context is canceled. It can watch one context at a
 // time.
 type ContextWatcher struct {
-	onCancel             func()
-	onUnwatchAfterCancel func()
-	unwatchChan          chan struct{}
+	handler     Handler
+	unwatchChan chan struct{}
 
 	lock              sync.Mutex
 	watchInProgress   bool
@@ -20,11 +19,10 @@ type ContextWatcher struct {
 // NewContextWatcher returns a ContextWatcher. onCancel will be called when a watched context is canceled.
 // OnUnwatchAfterCancel will be called when Unwatch is called and the watched context had already been canceled and
 // onCancel called.
-func NewContextWatcher(onCancel func(), onUnwatchAfterCancel func()) *ContextWatcher {
+func NewContextWatcher(handler Handler) *ContextWatcher {
 	cw := &ContextWatcher{
-		onCancel:             onCancel,
-		onUnwatchAfterCancel: onUnwatchAfterCancel,
-		unwatchChan:          make(chan struct{}),
+		handler:     handler,
+		unwatchChan: make(chan struct{}),
 	}
 
 	return cw
@@ -46,7 +44,7 @@ func (cw *ContextWatcher) Watch(ctx context.Context) {
 		go func() {
 			select {
 			case <-ctx.Done():
-				cw.onCancel()
+				cw.handler.HandleCancel(ctx)
 				cw.onCancelWasCalled = true
 				<-cw.unwatchChan
 			case <-cw.unwatchChan:
@@ -66,8 +64,17 @@ func (cw *ContextWatcher) Unwatch() {
 	if cw.watchInProgress {
 		cw.unwatchChan <- struct{}{}
 		if cw.onCancelWasCalled {
-			cw.onUnwatchAfterCancel()
+			cw.handler.HandleUnwatchAfterCancel()
 		}
 		cw.watchInProgress = false
 	}
 }
+
+type Handler interface {
+	// HandleCancel is called when the context that a ContextWatcher is currently watching is canceled. canceledCtx is the
+	// context that was canceled.
+	HandleCancel(canceledCtx context.Context)
+
+	// HandleUnwatchAfterCancel is called when a ContextWatcher that called HandleCancel on this Handler is unwatched.
+	HandleUnwatchAfterCancel()
+}
diff --git a/pgconn/doc.go b/pgconn/doc.go
index e3242cf4..8cf8688a 100644
--- a/pgconn/doc.go
+++ b/pgconn/doc.go
@@ -20,13 +20,17 @@ result. The ReadAll method reads all query results into memory.
 
 Pipeline Mode
 
-Pipeline mode allows sending queries without having read the results of previously sent queries. It allows
-control of exactly how many and when network round trips occur.
+Pipeline mode allows sending queries without having read the results of previously sent queries. It allows control of
+exactly how many and when network round trips occur.
 
 Context Support
 
-All potentially blocking operations take a context.Context. If a context is canceled while the method is in progress the
-method immediately returns. In most circumstances, this will close the underlying connection.
+All potentially blocking operations take a context.Context. The default behavior when a context is canceled is for the
+method to immediately return. In most circumstances, this will also close the underlying connection. This behavior can
+be customized by using BuildContextWatcherHandler on the Config to create a ctxwatch.Handler with different behavior.
+This can be especially useful when queries that are frequently canceled and the overhead of creating new connections is
+a problem. DeadlineContextWatcherHandler and CancelRequestContextWatcherHandler can be used to introduce a delay before
+interrupting the query in such a way as to close the connection.
 
 The CancelRequest method may be used to request the PostgreSQL server cancel an in-progress query without forcing the
 client to abort.
diff --git a/pgconn/errors.go b/pgconn/errors.go
index c315739a..6790b9b6 100644
--- a/pgconn/errors.go
+++ b/pgconn/errors.go
@@ -18,7 +18,7 @@ func SafeToRetry(err error) bool {
 	return false
 }
 
-// Timeout checks if err was was caused by a timeout. To be specific, it is true if err was caused within pgconn by a
+// Timeout checks if err was caused by a timeout. To be specific, it is true if err was caused within pgconn by a
 // context.DeadlineExceeded or an implementer of net.Error where Timeout() is true.
 func Timeout(err error) bool {
 	var timeoutErr *errTimeout
@@ -29,23 +29,24 @@ func Timeout(err error) bool {
 // http://www.postgresql.org/docs/11/static/protocol-error-fields.html for
 // detailed field description.
 type PgError struct {
-	Severity         string
-	Code             string
-	Message          string
-	Detail           string
-	Hint             string
-	Position         int32
-	InternalPosition int32
-	InternalQuery    string
-	Where            string
-	SchemaName       string
-	TableName        string
-	ColumnName       string
-	DataTypeName     string
-	ConstraintName   string
-	File             string
-	Line             int32
-	Routine          string
+	Severity            string
+	SeverityUnlocalized string
+	Code                string
+	Message             string
+	Detail              string
+	Hint                string
+	Position            int32
+	InternalPosition    int32
+	InternalQuery       string
+	Where               string
+	SchemaName          string
+	TableName           string
+	ColumnName          string
+	DataTypeName        string
+	ConstraintName      string
+	File                string
+	Line                int32
+	Routine             string
 }
 
 func (pe *PgError) Error() string {
diff --git a/pgconn/pgconn.go b/pgconn/pgconn.go
index 0bf03f33..819380e3 100644
--- a/pgconn/pgconn.go
+++ b/pgconn/pgconn.go
@@ -18,8 +18,8 @@ import (
 
 	"github.com/jackc/pgx/v5/internal/iobufpool"
 	"github.com/jackc/pgx/v5/internal/pgio"
+	"github.com/jackc/pgx/v5/pgconn/ctxwatch"
 	"github.com/jackc/pgx/v5/pgconn/internal/bgreader"
-	"github.com/jackc/pgx/v5/pgconn/internal/ctxwatch"
 	"github.com/jackc/pgx/v5/pgproto3"
 )
 
@@ -281,28 +281,26 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 
 	var err error
 	network, address := NetworkAddress(fallbackConfig.Host, fallbackConfig.Port)
-	netConn, err := config.DialFunc(ctx, network, address)
+	pgConn.conn, err = config.DialFunc(ctx, network, address)
 	if err != nil {
 		return nil, &ConnectError{Config: config, msg: "dial error", err: normalizeTimeoutError(ctx, err)}
 	}
 
-	pgConn.conn = netConn
-	pgConn.contextWatcher = newContextWatcher(netConn)
-	pgConn.contextWatcher.Watch(ctx)
-
 	if fallbackConfig.TLSConfig != nil {
-		nbTLSConn, err := startTLS(netConn, fallbackConfig.TLSConfig)
+		pgConn.contextWatcher = ctxwatch.NewContextWatcher(&DeadlineContextWatcherHandler{Conn: pgConn.conn})
+		pgConn.contextWatcher.Watch(ctx)
+		tlsConn, err := startTLS(pgConn.conn, fallbackConfig.TLSConfig)
 		pgConn.contextWatcher.Unwatch() // Always unwatch `netConn` after TLS.
 		if err != nil {
-			netConn.Close()
+			pgConn.conn.Close()
 			return nil, &ConnectError{Config: config, msg: "tls error", err: normalizeTimeoutError(ctx, err)}
 		}
 
-		pgConn.conn = nbTLSConn
-		pgConn.contextWatcher = newContextWatcher(nbTLSConn)
-		pgConn.contextWatcher.Watch(ctx)
+		pgConn.conn = tlsConn
 	}
 
+	pgConn.contextWatcher = ctxwatch.NewContextWatcher(config.BuildContextWatcherHandler(pgConn))
+	pgConn.contextWatcher.Watch(ctx)
 	defer pgConn.contextWatcher.Unwatch()
 
 	pgConn.parameterStatuses = make(map[string]string)
@@ -412,13 +410,6 @@ func connect(ctx context.Context, config *Config, fallbackConfig *FallbackConfig
 	}
 }
 
-func newContextWatcher(conn net.Conn) *ctxwatch.ContextWatcher {
-	return ctxwatch.NewContextWatcher(
-		func() { conn.SetDeadline(time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC)) },
-		func() { conn.SetDeadline(time.Time{}) },
-	)
-}
-
 func startTLS(conn net.Conn, tlsConfig *tls.Config) (net.Conn, error) {
 	err := binary.Write(conn, binary.BigEndian, []int32{8, 80877103})
 	if err != nil {
@@ -928,23 +919,24 @@ func (pgConn *PgConn) Deallocate(ctx context.Context, name string) error {
 // ErrorResponseToPgError converts a wire protocol error message to a *PgError.
 func ErrorResponseToPgError(msg *pgproto3.ErrorResponse) *PgError {
 	return &PgError{
-		Severity:         msg.Severity,
-		Code:             string(msg.Code),
-		Message:          string(msg.Message),
-		Detail:           string(msg.Detail),
-		Hint:             msg.Hint,
-		Position:         msg.Position,
-		InternalPosition: msg.InternalPosition,
-		InternalQuery:    string(msg.InternalQuery),
-		Where:            string(msg.Where),
-		SchemaName:       string(msg.SchemaName),
-		TableName:        string(msg.TableName),
-		ColumnName:       string(msg.ColumnName),
-		DataTypeName:     string(msg.DataTypeName),
-		ConstraintName:   msg.ConstraintName,
-		File:             string(msg.File),
-		Line:             msg.Line,
-		Routine:          string(msg.Routine),
+		Severity:            msg.Severity,
+		SeverityUnlocalized: msg.SeverityUnlocalized,
+		Code:                string(msg.Code),
+		Message:             string(msg.Message),
+		Detail:              string(msg.Detail),
+		Hint:                msg.Hint,
+		Position:            msg.Position,
+		InternalPosition:    msg.InternalPosition,
+		InternalQuery:       string(msg.InternalQuery),
+		Where:               string(msg.Where),
+		SchemaName:          string(msg.SchemaName),
+		TableName:           string(msg.TableName),
+		ColumnName:          string(msg.ColumnName),
+		DataTypeName:        string(msg.DataTypeName),
+		ConstraintName:      msg.ConstraintName,
+		File:                string(msg.File),
+		Line:                msg.Line,
+		Routine:             string(msg.Routine),
 	}
 }
 
@@ -987,10 +979,7 @@ func (pgConn *PgConn) CancelRequest(ctx context.Context) error {
 	defer cancelConn.Close()
 
 	if ctx != context.Background() {
-		contextWatcher := ctxwatch.NewContextWatcher(
-			func() { cancelConn.SetDeadline(time.Date(1, 1, 1, 1, 1, 1, 1, time.UTC)) },
-			func() { cancelConn.SetDeadline(time.Time{}) },
-		)
+		contextWatcher := ctxwatch.NewContextWatcher(&DeadlineContextWatcherHandler{Conn: cancelConn})
 		contextWatcher.Watch(ctx)
 		defer contextWatcher.Unwatch()
 	}
@@ -1938,7 +1927,7 @@ func Construct(hc *HijackedConn) (*PgConn, error) {
 		cleanupDone: make(chan struct{}),
 	}
 
-	pgConn.contextWatcher = newContextWatcher(pgConn.conn)
+	pgConn.contextWatcher = ctxwatch.NewContextWatcher(hc.Config.BuildContextWatcherHandler(pgConn))
 	pgConn.bgReader = bgreader.New(pgConn.conn)
 	pgConn.slowWriteTimer = time.AfterFunc(time.Duration(math.MaxInt64),
 		func() {
@@ -2245,3 +2234,71 @@ func (p *Pipeline) Close() error {
 
 	return p.err
 }
+
+// DeadlineContextWatcherHandler handles canceled contexts by setting a deadline on a net.Conn.
+type DeadlineContextWatcherHandler struct {
+	Conn net.Conn
+
+	// DeadlineDelay is the delay to set on the deadline set on net.Conn when the context is canceled.
+	DeadlineDelay time.Duration
+}
+
+func (h *DeadlineContextWatcherHandler) HandleCancel(ctx context.Context) {
+	h.Conn.SetDeadline(time.Now().Add(h.DeadlineDelay))
+}
+
+func (h *DeadlineContextWatcherHandler) HandleUnwatchAfterCancel() {
+	h.Conn.SetDeadline(time.Time{})
+}
+
+// CancelRequestContextWatcherHandler handles canceled contexts by sending a cancel request to the server. It also sets
+// a deadline on a net.Conn as a fallback.
+type CancelRequestContextWatcherHandler struct {
+	Conn *PgConn
+
+	// CancelRequestDelay is the delay before sending the cancel request to the server.
+	CancelRequestDelay time.Duration
+
+	// DeadlineDelay is the delay to set on the deadline set on net.Conn when the context is canceled.
+	DeadlineDelay time.Duration
+
+	cancelFinishedChan             chan struct{}
+	handleUnwatchAfterCancelCalled func()
+}
+
+func (h *CancelRequestContextWatcherHandler) HandleCancel(context.Context) {
+	h.cancelFinishedChan = make(chan struct{})
+	var handleUnwatchedAfterCancelCalledCtx context.Context
+	handleUnwatchedAfterCancelCalledCtx, h.handleUnwatchAfterCancelCalled = context.WithCancel(context.Background())
+
+	deadline := time.Now().Add(h.DeadlineDelay)
+	h.Conn.conn.SetDeadline(deadline)
+
+	go func() {
+		defer close(h.cancelFinishedChan)
+
+		select {
+		case <-handleUnwatchedAfterCancelCalledCtx.Done():
+			return
+		case <-time.After(h.CancelRequestDelay):
+		}
+
+		cancelRequestCtx, cancel := context.WithDeadline(handleUnwatchedAfterCancelCalledCtx, deadline)
+		defer cancel()
+		h.Conn.CancelRequest(cancelRequestCtx)
+
+		// CancelRequest is inherently racy. Even though the cancel request has been received by the server at this point,
+		// it hasn't necessarily been delivered to the other connection. If we immediately return and the connection is
+		// immediately used then it is possible the CancelRequest will actually cancel our next query. The
+		// TestCancelRequestContextWatcherHandler Stress test can produce this error without the sleep below. The sleep time
+		// is arbitrary, but should be sufficient to prevent this error case.
+		time.Sleep(100 * time.Millisecond)
+	}()
+}
+
+func (h *CancelRequestContextWatcherHandler) HandleUnwatchAfterCancel() {
+	h.handleUnwatchAfterCancelCalled()
+	<-h.cancelFinishedChan
+
+	h.Conn.conn.SetDeadline(time.Time{})
+}
diff --git a/pgproto3/pgproto3.go b/pgproto3/pgproto3.go
index 480abfc0..128f97f8 100644
--- a/pgproto3/pgproto3.go
+++ b/pgproto3/pgproto3.go
@@ -99,7 +99,7 @@ func getValueFromJSON(v map[string]string) ([]byte, error) {
 	return nil, errors.New("unknown protocol representation")
 }
 
-// beginMessage begines a new message of type t. It appends the message type and a placeholder for the message length to
+// beginMessage begins a new message of type t. It appends the message type and a placeholder for the message length to
 // dst. It returns the new buffer and the position of the message length placeholder.
 func beginMessage(dst []byte, t byte) ([]byte, int) {
 	dst = append(dst, t)
diff --git a/pgtype/pgtype.go b/pgtype/pgtype.go
index 534ef6d1..2be11e82 100644
--- a/pgtype/pgtype.go
+++ b/pgtype/pgtype.go
@@ -41,6 +41,7 @@ const (
 	CircleOID              = 718
 	CircleArrayOID         = 719
 	UnknownOID             = 705
+	Macaddr8OID            = 774
 	MacaddrOID             = 829
 	InetOID                = 869
 	BoolArrayOID           = 1000
@@ -1330,7 +1331,7 @@ func (plan *derefPointerEncodePlan) Encode(value any, buf []byte) (newBuf []byte
 }
 
 // TryWrapDerefPointerEncodePlan tries to dereference a pointer. e.g. If value was of type *string then a wrapper plan
-// would be returned that derefences the value.
+// would be returned that dereferences the value.
 func TryWrapDerefPointerEncodePlan(value any) (plan WrappedEncodePlanNextSetter, nextValue any, ok bool) {
 	if _, ok := value.(driver.Valuer); ok {
 		return nil, nil, false
diff --git a/pgtype/pgtype_default.go b/pgtype/pgtype_default.go
index c21ac081..38093ef4 100644
--- a/pgtype/pgtype_default.go
+++ b/pgtype/pgtype_default.go
@@ -70,6 +70,7 @@ func initDefaultMap() {
 	defaultMap.RegisterType(&Type{Name: "jsonpath", OID: JSONPathOID, Codec: &TextFormatOnlyCodec{TextCodec{}}})
 	defaultMap.RegisterType(&Type{Name: "line", OID: LineOID, Codec: LineCodec{}})
 	defaultMap.RegisterType(&Type{Name: "lseg", OID: LsegOID, Codec: LsegCodec{}})
+	defaultMap.RegisterType(&Type{Name: "macaddr8", OID: Macaddr8OID, Codec: MacaddrCodec{}})
 	defaultMap.RegisterType(&Type{Name: "macaddr", OID: MacaddrOID, Codec: MacaddrCodec{}})
 	defaultMap.RegisterType(&Type{Name: "name", OID: NameOID, Codec: TextCodec{}})
 	defaultMap.RegisterType(&Type{Name: "numeric", OID: NumericOID, Codec: NumericCodec{}})
@@ -81,8 +82,8 @@ func initDefaultMap() {
 	defaultMap.RegisterType(&Type{Name: "text", OID: TextOID, Codec: TextCodec{}})
 	defaultMap.RegisterType(&Type{Name: "tid", OID: TIDOID, Codec: TIDCodec{}})
 	defaultMap.RegisterType(&Type{Name: "time", OID: TimeOID, Codec: TimeCodec{}})
-	defaultMap.RegisterType(&Type{Name: "timestamp", OID: TimestampOID, Codec: TimestampCodec{}})
-	defaultMap.RegisterType(&Type{Name: "timestamptz", OID: TimestamptzOID, Codec: TimestamptzCodec{}})
+	defaultMap.RegisterType(&Type{Name: "timestamp", OID: TimestampOID, Codec: &TimestampCodec{}})
+	defaultMap.RegisterType(&Type{Name: "timestamptz", OID: TimestamptzOID, Codec: &TimestamptzCodec{}})
 	defaultMap.RegisterType(&Type{Name: "unknown", OID: UnknownOID, Codec: TextCodec{}})
 	defaultMap.RegisterType(&Type{Name: "uuid", OID: UUIDOID, Codec: UUIDCodec{}})
 	defaultMap.RegisterType(&Type{Name: "varbit", OID: VarbitOID, Codec: BitsCodec{}})
diff --git a/pgtype/time.go b/pgtype/time.go
index 2eb6ace2..a3d0ab1a 100644
--- a/pgtype/time.go
+++ b/pgtype/time.go
@@ -45,7 +45,12 @@ func (t *Time) Scan(src any) error {
 
 	switch src := src.(type) {
 	case string:
-		return scanPlanTextAnyToTimeScanner{}.Scan([]byte(src), t)
+		err := scanPlanTextAnyToTimeScanner{}.Scan([]byte(src), t)
+		if err != nil {
+			t.Microseconds = 0
+			t.Valid = false
+		}
+		return err
 	}
 
 	return fmt.Errorf("cannot scan %T", src)
@@ -176,7 +181,7 @@ func (scanPlanTextAnyToTimeScanner) Scan(src []byte, dst any) error {
 
 	s := string(src)
 
-	if len(s) < 8 {
+	if len(s) < 8 || s[2] != ':' || s[5] != ':' {
 		return fmt.Errorf("cannot decode %v into Time", s)
 	}
 
@@ -199,6 +204,10 @@ func (scanPlanTextAnyToTimeScanner) Scan(src []byte, dst any) error {
 	usec += seconds * microsecondsPerSecond
 
 	if len(s) > 9 {
+		if s[8] != '.' || len(s) > 15 {
+			return fmt.Errorf("cannot decode %v into Time", s)
+		}
+
 		fraction := s[9:]
 		n, err := strconv.ParseInt(fraction, 10, 64)
 		if err != nil {
diff --git a/pgtype/timestamp.go b/pgtype/timestamp.go
index 35d73956..677a2c6e 100644
--- a/pgtype/timestamp.go
+++ b/pgtype/timestamp.go
@@ -46,7 +46,7 @@ func (ts *Timestamp) Scan(src any) error {
 
 	switch src := src.(type) {
 	case string:
-		return scanPlanTextTimestampToTimestampScanner{}.Scan([]byte(src), ts)
+		return (&scanPlanTextTimestampToTimestampScanner{}).Scan([]byte(src), ts)
 	case time.Time:
 		*ts = Timestamp{Time: src, Valid: true}
 		return nil
@@ -116,17 +116,21 @@ func (ts *Timestamp) UnmarshalJSON(b []byte) error {
 	return nil
 }
 
-type TimestampCodec struct{}
+type TimestampCodec struct {
+	// ScanLocation is the location that the time is assumed to be in for scanning. This is different from
+	// TimestamptzCodec.ScanLocation in that this setting does change the instant in time that the timestamp represents.
+	ScanLocation *time.Location
+}
 
-func (TimestampCodec) FormatSupported(format int16) bool {
+func (*TimestampCodec) FormatSupported(format int16) bool {
 	return format == TextFormatCode || format == BinaryFormatCode
 }
 
-func (TimestampCodec) PreferredFormat() int16 {
+func (*TimestampCodec) PreferredFormat() int16 {
 	return BinaryFormatCode
 }
 
-func (TimestampCodec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan {
+func (*TimestampCodec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan {
 	if _, ok := value.(TimestampValuer); !ok {
 		return nil
 	}
@@ -220,27 +224,27 @@ func discardTimeZone(t time.Time) time.Time {
 	return t
 }
 
-func (TimestampCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan {
+func (c *TimestampCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan {
 
 	switch format {
 	case BinaryFormatCode:
 		switch target.(type) {
 		case TimestampScanner:
-			return scanPlanBinaryTimestampToTimestampScanner{}
+			return &scanPlanBinaryTimestampToTimestampScanner{location: c.ScanLocation}
 		}
 	case TextFormatCode:
 		switch target.(type) {
 		case TimestampScanner:
-			return scanPlanTextTimestampToTimestampScanner{}
+			return &scanPlanTextTimestampToTimestampScanner{location: c.ScanLocation}
 		}
 	}
 
 	return nil
 }
 
-type scanPlanBinaryTimestampToTimestampScanner struct{}
+type scanPlanBinaryTimestampToTimestampScanner struct{ location *time.Location }
 
-func (scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error {
+func (plan *scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error {
 	scanner := (dst).(TimestampScanner)
 
 	if src == nil {
@@ -264,15 +268,18 @@ func (scanPlanBinaryTimestampToTimestampScanner) Scan(src []byte, dst any) error
 			microsecFromUnixEpochToY2K/1000000+microsecSinceY2K/1000000,
 			(microsecFromUnixEpochToY2K%1000000*1000)+(microsecSinceY2K%1000000*1000),
 		).UTC()
+		if plan.location != nil {
+			tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location)
+		}
 		ts = Timestamp{Time: tim, Valid: true}
 	}
 
 	return scanner.ScanTimestamp(ts)
 }
 
-type scanPlanTextTimestampToTimestampScanner struct{}
+type scanPlanTextTimestampToTimestampScanner struct{ location *time.Location }
 
-func (scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error {
+func (plan *scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error {
 	scanner := (dst).(TimestampScanner)
 
 	if src == nil {
@@ -302,13 +309,17 @@ func (scanPlanTextTimestampToTimestampScanner) Scan(src []byte, dst any) error {
 			tim = time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), tim.Location())
 		}
 
+		if plan.location != nil {
+			tim = time.Date(tim.Year(), tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), plan.location)
+		}
+
 		ts = Timestamp{Time: tim, Valid: true}
 	}
 
 	return scanner.ScanTimestamp(ts)
 }
 
-func (c TimestampCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
+func (c *TimestampCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
 	if src == nil {
 		return nil, nil
 	}
@@ -326,7 +337,7 @@ func (c TimestampCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16,
 	return ts.Time, nil
 }
 
-func (c TimestampCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
+func (c *TimestampCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
 	if src == nil {
 		return nil, nil
 	}
diff --git a/pgtype/timestamptz.go b/pgtype/timestamptz.go
index f568fe30..7efbcffd 100644
--- a/pgtype/timestamptz.go
+++ b/pgtype/timestamptz.go
@@ -54,7 +54,7 @@ func (tstz *Timestamptz) Scan(src any) error {
 
 	switch src := src.(type) {
 	case string:
-		return scanPlanTextTimestamptzToTimestamptzScanner{}.Scan([]byte(src), tstz)
+		return (&scanPlanTextTimestamptzToTimestamptzScanner{}).Scan([]byte(src), tstz)
 	case time.Time:
 		*tstz = Timestamptz{Time: src, Valid: true}
 		return nil
@@ -124,17 +124,21 @@ func (tstz *Timestamptz) UnmarshalJSON(b []byte) error {
 	return nil
 }
 
-type TimestamptzCodec struct{}
+type TimestamptzCodec struct {
+	// ScanLocation is the location to return scanned timestamptz values in. This does not change the instant in time that
+	// the timestamptz represents.
+	ScanLocation *time.Location
+}
 
-func (TimestamptzCodec) FormatSupported(format int16) bool {
+func (*TimestamptzCodec) FormatSupported(format int16) bool {
 	return format == TextFormatCode || format == BinaryFormatCode
 }
 
-func (TimestamptzCodec) PreferredFormat() int16 {
+func (*TimestamptzCodec) PreferredFormat() int16 {
 	return BinaryFormatCode
 }
 
-func (TimestamptzCodec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan {
+func (*TimestamptzCodec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan {
 	if _, ok := value.(TimestamptzValuer); !ok {
 		return nil
 	}
@@ -220,27 +224,27 @@ func (encodePlanTimestamptzCodecText) Encode(value any, buf []byte) (newBuf []by
 	return buf, nil
 }
 
-func (TimestamptzCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan {
+func (c *TimestamptzCodec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan {
 
 	switch format {
 	case BinaryFormatCode:
 		switch target.(type) {
 		case TimestamptzScanner:
-			return scanPlanBinaryTimestamptzToTimestamptzScanner{}
+			return &scanPlanBinaryTimestamptzToTimestamptzScanner{location: c.ScanLocation}
 		}
 	case TextFormatCode:
 		switch target.(type) {
 		case TimestamptzScanner:
-			return scanPlanTextTimestamptzToTimestamptzScanner{}
+			return &scanPlanTextTimestamptzToTimestamptzScanner{location: c.ScanLocation}
 		}
 	}
 
 	return nil
 }
 
-type scanPlanBinaryTimestamptzToTimestamptzScanner struct{}
+type scanPlanBinaryTimestamptzToTimestamptzScanner struct{ location *time.Location }
 
-func (scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) error {
+func (plan *scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) error {
 	scanner := (dst).(TimestamptzScanner)
 
 	if src == nil {
@@ -264,15 +268,18 @@ func (scanPlanBinaryTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) e
 			microsecFromUnixEpochToY2K/1000000+microsecSinceY2K/1000000,
 			(microsecFromUnixEpochToY2K%1000000*1000)+(microsecSinceY2K%1000000*1000),
 		)
+		if plan.location != nil {
+			tim = tim.In(plan.location)
+		}
 		tstz = Timestamptz{Time: tim, Valid: true}
 	}
 
 	return scanner.ScanTimestamptz(tstz)
 }
 
-type scanPlanTextTimestamptzToTimestamptzScanner struct{}
+type scanPlanTextTimestamptzToTimestamptzScanner struct{ location *time.Location }
 
-func (scanPlanTextTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) error {
+func (plan *scanPlanTextTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) error {
 	scanner := (dst).(TimestamptzScanner)
 
 	if src == nil {
@@ -312,13 +319,17 @@ func (scanPlanTextTimestamptzToTimestamptzScanner) Scan(src []byte, dst any) err
 			tim = time.Date(year, tim.Month(), tim.Day(), tim.Hour(), tim.Minute(), tim.Second(), tim.Nanosecond(), tim.Location())
 		}
 
+		if plan.location != nil {
+			tim = tim.In(plan.location)
+		}
+
 		tstz = Timestamptz{Time: tim, Valid: true}
 	}
 
 	return scanner.ScanTimestamptz(tstz)
 }
 
-func (c TimestamptzCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
+func (c *TimestamptzCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
 	if src == nil {
 		return nil, nil
 	}
@@ -336,7 +347,7 @@ func (c TimestamptzCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int1
 	return tstz.Time, nil
 }
 
-func (c TimestamptzCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
+func (c *TimestamptzCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
 	if src == nil {
 		return nil, nil
 	}
diff --git a/rows.go b/rows.go
index 78ef5326..78723d6c 100644
--- a/rows.go
+++ b/rows.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"reflect"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/jackc/pgx/v5/pgconn"
@@ -541,7 +542,7 @@ func (rs *mapRowScanner) ScanRow(rows Rows) error {
 // ignored.
 func RowToStructByPos[T any](row CollectableRow) (T, error) {
 	var value T
-	err := row.Scan(&positionalStructRowScanner{ptrToStruct: &value})
+	err := (&positionalStructRowScanner{ptrToStruct: &value}).ScanRow(row)
 	return value, err
 }
 
@@ -550,7 +551,7 @@ func RowToStructByPos[T any](row CollectableRow) (T, error) {
 // the field will be ignored.
 func RowToAddrOfStructByPos[T any](row CollectableRow) (*T, error) {
 	var value T
-	err := row.Scan(&positionalStructRowScanner{ptrToStruct: &value})
+	err := (&positionalStructRowScanner{ptrToStruct: &value}).ScanRow(row)
 	return &value, err
 }
 
@@ -558,46 +559,60 @@ type positionalStructRowScanner struct {
 	ptrToStruct any
 }
 
-func (rs *positionalStructRowScanner) ScanRow(rows Rows) error {
-	dst := rs.ptrToStruct
-	dstValue := reflect.ValueOf(dst)
-	if dstValue.Kind() != reflect.Ptr {
-		return fmt.Errorf("dst not a pointer")
+func (rs *positionalStructRowScanner) ScanRow(rows CollectableRow) error {
+	typ := reflect.TypeOf(rs.ptrToStruct).Elem()
+	fields := lookupStructFields(typ)
+	if len(rows.RawValues()) > len(fields) {
+		return fmt.Errorf(
+			"got %d values, but dst struct has only %d fields",
+			len(rows.RawValues()),
+			len(fields),
+		)
 	}
-
-	dstElemValue := dstValue.Elem()
-	scanTargets := rs.appendScanTargets(dstElemValue, nil)
-
-	if len(rows.RawValues()) > len(scanTargets) {
-		return fmt.Errorf("got %d values, but dst struct has only %d fields", len(rows.RawValues()), len(scanTargets))
-	}
-
+	scanTargets := setupStructScanTargets(rs.ptrToStruct, fields)
 	return rows.Scan(scanTargets...)
 }
 
-func (rs *positionalStructRowScanner) appendScanTargets(dstElemValue reflect.Value, scanTargets []any) []any {
-	dstElemType := dstElemValue.Type()
+// Map from reflect.Type -> []structRowField
+var positionalStructFieldMap sync.Map
 
-	if scanTargets == nil {
-		scanTargets = make([]any, 0, dstElemType.NumField())
+func lookupStructFields(t reflect.Type) []structRowField {
+	if cached, ok := positionalStructFieldMap.Load(t); ok {
+		return cached.([]structRowField)
 	}
 
-	for i := 0; i < dstElemType.NumField(); i++ {
-		sf := dstElemType.Field(i)
+	fieldStack := make([]int, 0, 1)
+	fields := computeStructFields(t, make([]structRowField, 0, t.NumField()), &fieldStack)
+	fieldsIface, _ := positionalStructFieldMap.LoadOrStore(t, fields)
+	return fieldsIface.([]structRowField)
+}
+
+func computeStructFields(
+	t reflect.Type,
+	fields []structRowField,
+	fieldStack *[]int,
+) []structRowField {
+	tail := len(*fieldStack)
+	*fieldStack = append(*fieldStack, 0)
+	for i := 0; i < t.NumField(); i++ {
+		sf := t.Field(i)
+		(*fieldStack)[tail] = i
 		// Handle anonymous struct embedding, but do not try to handle embedded pointers.
 		if sf.Anonymous && sf.Type.Kind() == reflect.Struct {
-			scanTargets = rs.appendScanTargets(dstElemValue.Field(i), scanTargets)
+			fields = computeStructFields(sf.Type, fields, fieldStack)
 		} else if sf.PkgPath == "" {
 			dbTag, _ := sf.Tag.Lookup(structTagKey)
 			if dbTag == "-" {
 				// Field is ignored, skip it.
 				continue
 			}
-			scanTargets = append(scanTargets, dstElemValue.Field(i).Addr().Interface())
+			fields = append(fields, structRowField{
+				path: append([]int(nil), *fieldStack...),
+			})
 		}
 	}
-
-	return scanTargets
+	*fieldStack = (*fieldStack)[:tail]
+	return fields
 }
 
 // RowToStructByName returns a T scanned from row. T must be a struct. T must have the same number of named public
@@ -605,7 +620,7 @@ func (rs *positionalStructRowScanner) appendScanTargets(dstElemValue reflect.Val
 // column name can be overridden with a "db" struct tag. If the "db" struct tag is "-" then the field will be ignored.
 func RowToStructByName[T any](row CollectableRow) (T, error) {
 	var value T
-	err := row.Scan(&namedStructRowScanner{ptrToStruct: &value})
+	err := (&namedStructRowScanner{ptrToStruct: &value}).ScanRow(row)
 	return value, err
 }
 
@@ -615,7 +630,7 @@ func RowToStructByName[T any](row CollectableRow) (T, error) {
 // then the field will be ignored.
 func RowToAddrOfStructByName[T any](row CollectableRow) (*T, error) {
 	var value T
-	err := row.Scan(&namedStructRowScanner{ptrToStruct: &value})
+	err := (&namedStructRowScanner{ptrToStruct: &value}).ScanRow(row)
 	return &value, err
 }
 
@@ -624,7 +639,7 @@ func RowToAddrOfStructByName[T any](row CollectableRow) (*T, error) {
 // column name can be overridden with a "db" struct tag. If the "db" struct tag is "-" then the field will be ignored.
 func RowToStructByNameLax[T any](row CollectableRow) (T, error) {
 	var value T
-	err := row.Scan(&namedStructRowScanner{ptrToStruct: &value, lax: true})
+	err := (&namedStructRowScanner{ptrToStruct: &value, lax: true}).ScanRow(row)
 	return value, err
 }
 
@@ -634,7 +649,7 @@ func RowToStructByNameLax[T any](row CollectableRow) (T, error) {
 // then the field will be ignored.
 func RowToAddrOfStructByNameLax[T any](row CollectableRow) (*T, error) {
 	var value T
-	err := row.Scan(&namedStructRowScanner{ptrToStruct: &value, lax: true})
+	err := (&namedStructRowScanner{ptrToStruct: &value, lax: true}).ScanRow(row)
 	return &value, err
 }
 
@@ -643,64 +658,123 @@ type namedStructRowScanner struct {
 	lax         bool
 }
 
-func (rs *namedStructRowScanner) ScanRow(rows Rows) error {
-	dst := rs.ptrToStruct
-	dstValue := reflect.ValueOf(dst)
-	if dstValue.Kind() != reflect.Ptr {
-		return fmt.Errorf("dst not a pointer")
-	}
-
-	dstElemValue := dstValue.Elem()
-	scanTargets, err := rs.appendScanTargets(dstElemValue, nil, rows.FieldDescriptions())
+func (rs *namedStructRowScanner) ScanRow(rows CollectableRow) error {
+	typ := reflect.TypeOf(rs.ptrToStruct).Elem()
+	fldDescs := rows.FieldDescriptions()
+	namedStructFields, err := lookupNamedStructFields(typ, fldDescs)
 	if err != nil {
 		return err
 	}
-
-	for i, t := range scanTargets {
-		if t == nil {
-			return fmt.Errorf("struct doesn't have corresponding row field %s", rows.FieldDescriptions()[i].Name)
-		}
+	if !rs.lax && namedStructFields.missingField != "" {
+		return fmt.Errorf("cannot find field %s in returned row", namedStructFields.missingField)
 	}
-
+	fields := namedStructFields.fields
+	scanTargets := setupStructScanTargets(rs.ptrToStruct, fields)
 	return rows.Scan(scanTargets...)
 }
 
-const structTagKey = "db"
-
-func fieldPosByName(fldDescs []pgconn.FieldDescription, field string) (i int) {
-	i = -1
-	for i, desc := range fldDescs {
+// Map from namedStructFieldMap -> *namedStructFields
+var namedStructFieldMap sync.Map
 
-		// Snake case support.
-		field = strings.ReplaceAll(field, "_", "")
-		descName := strings.ReplaceAll(desc.Name, "_", "")
+type namedStructFieldsKey struct {
+	t        reflect.Type
+	colNames string
+}
 
-		if strings.EqualFold(descName, field) {
-			return i
-		}
-	}
-	return
+type namedStructFields struct {
+	fields []structRowField
+	// missingField is the first field from the struct without a corresponding row field.
+	// This is used to construct the correct error message for non-lax queries.
+	missingField string
 }
 
-func (rs *namedStructRowScanner) appendScanTargets(dstElemValue reflect.Value, scanTargets []any, fldDescs []pgconn.FieldDescription) ([]any, error) {
-	var err error
-	dstElemType := dstElemValue.Type()
+func lookupNamedStructFields(
+	t reflect.Type,
+	fldDescs []pgconn.FieldDescription,
+) (*namedStructFields, error) {
+	key := namedStructFieldsKey{
+		t:        t,
+		colNames: joinFieldNames(fldDescs),
+	}
+	if cached, ok := namedStructFieldMap.Load(key); ok {
+		return cached.(*namedStructFields), nil
+	}
 
-	if scanTargets == nil {
-		scanTargets = make([]any, len(fldDescs))
+	// We could probably do two-levels of caching, where we compute the key -> fields mapping
+	// for a type only once, cache it by type, then use that to compute the column -> fields
+	// mapping for a given set of columns.
+	fieldStack := make([]int, 0, 1)
+	fields, missingField := computeNamedStructFields(
+		fldDescs,
+		t,
+		make([]structRowField, len(fldDescs)),
+		&fieldStack,
+	)
+	for i, f := range fields {
+		if f.path == nil {
+			return nil, fmt.Errorf(
+				"struct doesn't have corresponding row field %s",
+				fldDescs[i].Name,
+			)
+		}
 	}
 
-	for i := 0; i < dstElemType.NumField(); i++ {
-		sf := dstElemType.Field(i)
+	fieldsIface, _ := namedStructFieldMap.LoadOrStore(
+		key,
+		&namedStructFields{fields: fields, missingField: missingField},
+	)
+	return fieldsIface.(*namedStructFields), nil
+}
+
+func joinFieldNames(fldDescs []pgconn.FieldDescription) string {
+	switch len(fldDescs) {
+	case 0:
+		return ""
+	case 1:
+		return fldDescs[0].Name
+	}
+
+	totalSize := len(fldDescs) - 1 // Space for separator bytes.
+	for _, d := range fldDescs {
+		totalSize += len(d.Name)
+	}
+	var b strings.Builder
+	b.Grow(totalSize)
+	b.WriteString(fldDescs[0].Name)
+	for _, d := range fldDescs[1:] {
+		b.WriteByte(0) // Join with NUL byte as it's (presumably) not a valid column character.
+		b.WriteString(d.Name)
+	}
+	return b.String()
+}
+
+func computeNamedStructFields(
+	fldDescs []pgconn.FieldDescription,
+	t reflect.Type,
+	fields []structRowField,
+	fieldStack *[]int,
+) ([]structRowField, string) {
+	var missingField string
+	tail := len(*fieldStack)
+	*fieldStack = append(*fieldStack, 0)
+	for i := 0; i < t.NumField(); i++ {
+		sf := t.Field(i)
+		(*fieldStack)[tail] = i
 		if sf.PkgPath != "" && !sf.Anonymous {
 			// Field is unexported, skip it.
 			continue
 		}
 		// Handle anonymous struct embedding, but do not try to handle embedded pointers.
 		if sf.Anonymous && sf.Type.Kind() == reflect.Struct {
-			scanTargets, err = rs.appendScanTargets(dstElemValue.Field(i), scanTargets, fldDescs)
-			if err != nil {
-				return nil, err
+			var missingSubField string
+			fields, missingSubField = computeNamedStructFields(
+				fldDescs,
+				sf.Type,
+				fields,
+				fieldStack,
+			)
+			if missingField == "" {
+				missingField = missingSubField
 			}
 		} else {
 			dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey)
@@ -717,17 +791,53 @@ func (rs *namedStructRowScanner) appendScanTargets(dstElemValue reflect.Value, s
 			}
 			fpos := fieldPosByName(fldDescs, colName)
 			if fpos == -1 {
-				if rs.lax {
-					continue
+				if missingField == "" {
+					missingField = colName
 				}
-				return nil, fmt.Errorf("cannot find field %s in returned row", colName)
+				continue
 			}
-			if fpos >= len(scanTargets) && !rs.lax {
-				return nil, fmt.Errorf("cannot find field %s in returned row", colName)
+			fields[fpos] = structRowField{
+				path: append([]int(nil), *fieldStack...),
 			}
-			scanTargets[fpos] = dstElemValue.Field(i).Addr().Interface()
 		}
 	}
+	*fieldStack = (*fieldStack)[:tail]
+
+	return fields, missingField
+}
 
-	return scanTargets, err
+const structTagKey = "db"
+
+func fieldPosByName(fldDescs []pgconn.FieldDescription, field string) (i int) {
+	i = -1
+	for i, desc := range fldDescs {
+
+		// Snake case support.
+		field = strings.ReplaceAll(field, "_", "")
+		descName := strings.ReplaceAll(desc.Name, "_", "")
+
+		if strings.EqualFold(descName, field) {
+			return i
+		}
+	}
+	return
+}
+
+// structRowField describes a field of a struct.
+//
+// TODO: It would be a bit more efficient to track the path using the pointer
+// offset within the (outermost) struct and use unsafe.Pointer arithmetic to
+// construct references when scanning rows. However, it's not clear it's worth
+// using unsafe for this.
+type structRowField struct {
+	path []int
+}
+
+func setupStructScanTargets(receiver any, fields []structRowField) []any {
+	scanTargets := make([]any, len(fields))
+	v := reflect.ValueOf(receiver).Elem()
+	for i, f := range fields {
+		scanTargets[i] = v.FieldByIndex(f.path).Addr().Interface()
+	}
+	return scanTargets
 }


Test patch:
diff --git a/bench_test.go b/bench_test.go
index aa545172..34eab085 100644
--- a/bench_test.go
+++ b/bench_test.go
@@ -1134,6 +1134,100 @@ func BenchmarkSelectRowsPgConnExecParams(b *testing.B) {
 	}
 }
 
+func BenchmarkSelectRowsSimpleCollectRowsRowToStructByPos(b *testing.B) {
+	conn := mustConnectString(b, os.Getenv("PGX_TEST_DATABASE"))
+	defer closeConn(b, conn)
+
+	rowCounts := getSelectRowsCounts(b)
+
+	for _, rowCount := range rowCounts {
+		b.Run(fmt.Sprintf("%d rows", rowCount), func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				rows, _ := conn.Query(context.Background(), "select n, 'Adam', 'Smith ' || n, 'male', '1952-06-16'::date, 258, 72, '2001-01-28 01:02:03-05'::timestamptz from generate_series(100001, 100000 + $1) n", rowCount)
+				benchRows, err := pgx.CollectRows(rows, pgx.RowToStructByPos[BenchRowSimple])
+				if err != nil {
+					b.Fatal(err)
+				}
+				if len(benchRows) != int(rowCount) {
+					b.Fatalf("Expected %d rows, got %d", rowCount, len(benchRows))
+				}
+			}
+		})
+	}
+}
+
+func BenchmarkSelectRowsSimpleAppendRowsRowToStructByPos(b *testing.B) {
+	conn := mustConnectString(b, os.Getenv("PGX_TEST_DATABASE"))
+	defer closeConn(b, conn)
+
+	rowCounts := getSelectRowsCounts(b)
+
+	for _, rowCount := range rowCounts {
+		b.Run(fmt.Sprintf("%d rows", rowCount), func(b *testing.B) {
+			benchRows := make([]BenchRowSimple, 0, rowCount)
+			for i := 0; i < b.N; i++ {
+				benchRows = benchRows[:0]
+				rows, _ := conn.Query(context.Background(), "select n, 'Adam', 'Smith ' || n, 'male', '1952-06-16'::date, 258, 72, '2001-01-28 01:02:03-05'::timestamptz from generate_series(100001, 100000 + $1) n", rowCount)
+				var err error
+				benchRows, err = pgx.AppendRows(benchRows, rows, pgx.RowToStructByPos[BenchRowSimple])
+				if err != nil {
+					b.Fatal(err)
+				}
+				if len(benchRows) != int(rowCount) {
+					b.Fatalf("Expected %d rows, got %d", rowCount, len(benchRows))
+				}
+			}
+		})
+	}
+}
+
+func BenchmarkSelectRowsSimpleCollectRowsRowToStructByName(b *testing.B) {
+	conn := mustConnectString(b, os.Getenv("PGX_TEST_DATABASE"))
+	defer closeConn(b, conn)
+
+	rowCounts := getSelectRowsCounts(b)
+
+	for _, rowCount := range rowCounts {
+		b.Run(fmt.Sprintf("%d rows", rowCount), func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				rows, _ := conn.Query(context.Background(), "select n as id, 'Adam' as first_name, 'Smith ' || n as last_name, 'male' as sex, '1952-06-16'::date as birth_date, 258 as weight, 72 as height, '2001-01-28 01:02:03-05'::timestamptz as update_time from generate_series(100001, 100000 + $1) n", rowCount)
+				benchRows, err := pgx.CollectRows(rows, pgx.RowToStructByName[BenchRowSimple])
+				if err != nil {
+					b.Fatal(err)
+				}
+				if len(benchRows) != int(rowCount) {
+					b.Fatalf("Expected %d rows, got %d", rowCount, len(benchRows))
+				}
+			}
+		})
+	}
+}
+
+func BenchmarkSelectRowsSimpleAppendRowsRowToStructByName(b *testing.B) {
+	conn := mustConnectString(b, os.Getenv("PGX_TEST_DATABASE"))
+	defer closeConn(b, conn)
+
+	rowCounts := getSelectRowsCounts(b)
+
+	for _, rowCount := range rowCounts {
+		b.Run(fmt.Sprintf("%d rows", rowCount), func(b *testing.B) {
+			benchRows := make([]BenchRowSimple, 0, rowCount)
+			for i := 0; i < b.N; i++ {
+				benchRows = benchRows[:0]
+				rows, _ := conn.Query(context.Background(), "select n as id, 'Adam' as first_name, 'Smith ' || n as last_name, 'male' as sex, '1952-06-16'::date as birth_date, 258 as weight, 72 as height, '2001-01-28 01:02:03-05'::timestamptz as update_time from generate_series(100001, 100000 + $1) n", rowCount)
+				var err error
+				benchRows, err = pgx.AppendRows(benchRows, rows, pgx.RowToStructByPos[BenchRowSimple])
+				if err != nil {
+					b.Fatal(err)
+				}
+				if len(benchRows) != int(rowCount) {
+					b.Fatalf("Expected %d rows, got %d", rowCount, len(benchRows))
+				}
+			}
+		})
+	}
+}
+
 func BenchmarkSelectRowsPgConnExecPrepared(b *testing.B) {
 	conn := mustConnectString(b, os.Getenv("PGX_TEST_DATABASE"))
 	defer closeConn(b, conn)
diff --git a/pgconn/internal/ctxwatch/context_watcher_test.go b/pgconn/ctxwatch/context_watcher_test.go
similarity index 66%
rename from pgconn/internal/ctxwatch/context_watcher_test.go
rename to pgconn/ctxwatch/context_watcher_test.go
index d62b97d3..302aabe3 100644
--- a/pgconn/internal/ctxwatch/context_watcher_test.go
+++ b/pgconn/ctxwatch/context_watcher_test.go
@@ -6,17 +6,32 @@ import (
 	"testing"
 	"time"
 
-	"github.com/jackc/pgx/v5/pgconn/internal/ctxwatch"
+	"github.com/jackc/pgx/v5/pgconn/ctxwatch"
 	"github.com/stretchr/testify/require"
 )
 
+type testHandler struct {
+	handleCancel             func(context.Context)
+	handleUnwatchAfterCancel func()
+}
+
+func (h *testHandler) HandleCancel(ctx context.Context) {
+	h.handleCancel(ctx)
+}
+
+func (h *testHandler) HandleUnwatchAfterCancel() {
+	h.handleUnwatchAfterCancel()
+}
+
 func TestContextWatcherContextCancelled(t *testing.T) {
 	canceledChan := make(chan struct{})
 	cleanupCalled := false
-	cw := ctxwatch.NewContextWatcher(func() {
-		canceledChan <- struct{}{}
-	}, func() {
-		cleanupCalled = true
+	cw := ctxwatch.NewContextWatcher(&testHandler{
+		handleCancel: func(context.Context) {
+			canceledChan <- struct{}{}
+		}, handleUnwatchAfterCancel: func() {
+			cleanupCalled = true
+		},
 	})
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -35,10 +50,12 @@ func TestContextWatcherContextCancelled(t *testing.T) {
 }
 
 func TestContextWatcherUnwatchdBeforeContextCancelled(t *testing.T) {
-	cw := ctxwatch.NewContextWatcher(func() {
-		t.Error("cancel func should not have been called")
-	}, func() {
-		t.Error("cleanup func should not have been called")
+	cw := ctxwatch.NewContextWatcher(&testHandler{
+		handleCancel: func(context.Context) {
+			t.Error("cancel func should not have been called")
+		}, handleUnwatchAfterCancel: func() {
+			t.Error("cleanup func should not have been called")
+		},
 	})
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -48,7 +65,7 @@ func TestContextWatcherUnwatchdBeforeContextCancelled(t *testing.T) {
 }
 
 func TestContextWatcherMultipleWatchPanics(t *testing.T) {
-	cw := ctxwatch.NewContextWatcher(func() {}, func() {})
+	cw := ctxwatch.NewContextWatcher(&testHandler{handleCancel: func(context.Context) {}, handleUnwatchAfterCancel: func() {}})
 
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
@@ -61,7 +78,7 @@ func TestContextWatcherMultipleWatchPanics(t *testing.T) {
 }
 
 func TestContextWatcherUnwatchWhenNotWatchingIsSafe(t *testing.T) {
-	cw := ctxwatch.NewContextWatcher(func() {}, func() {})
+	cw := ctxwatch.NewContextWatcher(&testHandler{handleCancel: func(context.Context) {}, handleUnwatchAfterCancel: func() {}})
 	cw.Unwatch() // unwatch when not / never watching
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -72,7 +89,7 @@ func TestContextWatcherUnwatchWhenNotWatchingIsSafe(t *testing.T) {
 }
 
 func TestContextWatcherUnwatchIsConcurrencySafe(t *testing.T) {
-	cw := ctxwatch.NewContextWatcher(func() {}, func() {})
+	cw := ctxwatch.NewContextWatcher(&testHandler{handleCancel: func(context.Context) {}, handleUnwatchAfterCancel: func() {}})
 
 	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
 	defer cancel()
@@ -88,10 +105,12 @@ func TestContextWatcherStress(t *testing.T) {
 	var cancelFuncCalls int64
 	var cleanupFuncCalls int64
 
-	cw := ctxwatch.NewContextWatcher(func() {
-		atomic.AddInt64(&cancelFuncCalls, 1)
-	}, func() {
-		atomic.AddInt64(&cleanupFuncCalls, 1)
+	cw := ctxwatch.NewContextWatcher(&testHandler{
+		handleCancel: func(context.Context) {
+			atomic.AddInt64(&cancelFuncCalls, 1)
+		}, handleUnwatchAfterCancel: func() {
+			atomic.AddInt64(&cleanupFuncCalls, 1)
+		},
 	})
 
 	cycleCount := 100000
@@ -134,7 +153,7 @@ func TestContextWatcherStress(t *testing.T) {
 }
 
 func BenchmarkContextWatcherUncancellable(b *testing.B) {
-	cw := ctxwatch.NewContextWatcher(func() {}, func() {})
+	cw := ctxwatch.NewContextWatcher(&testHandler{handleCancel: func(context.Context) {}, handleUnwatchAfterCancel: func() {}})
 
 	for i := 0; i < b.N; i++ {
 		cw.Watch(context.Background())
@@ -143,7 +162,7 @@ func BenchmarkContextWatcherUncancellable(b *testing.B) {
 }
 
 func BenchmarkContextWatcherCancelled(b *testing.B) {
-	cw := ctxwatch.NewContextWatcher(func() {}, func() {})
+	cw := ctxwatch.NewContextWatcher(&testHandler{handleCancel: func(context.Context) {}, handleUnwatchAfterCancel: func() {}})
 
 	for i := 0; i < b.N; i++ {
 		ctx, cancel := context.WithCancel(context.Background())
@@ -154,7 +173,7 @@ func BenchmarkContextWatcherCancelled(b *testing.B) {
 }
 
 func BenchmarkContextWatcherCancellable(b *testing.B) {
-	cw := ctxwatch.NewContextWatcher(func() {}, func() {})
+	cw := ctxwatch.NewContextWatcher(&testHandler{handleCancel: func(context.Context) {}, handleUnwatchAfterCancel: func() {}})
 
 	ctx, cancel := context.WithCancel(context.Background())
 	defer cancel()
diff --git a/pgconn/errors_test.go b/pgconn/errors_test.go
index 9d559346..171445cc 100644
--- a/pgconn/errors_test.go
+++ b/pgconn/errors_test.go
@@ -30,7 +30,7 @@ func TestConfigError(t *testing.T) {
 		},
 		{
 			name:        "weird url",
-			err:         pgconn.NewParseConfigError("postgresql://foo::pasword@host:1:", "msg", nil),
+			err:         pgconn.NewParseConfigError("postgresql://foo::password@host:1:", "msg", nil),
 			expectedMsg: "cannot parse `postgresql://foo:xxxxx@host:1:`: msg",
 		},
 		{
diff --git a/pgconn/pgconn_test.go b/pgconn/pgconn_test.go
index b77d21c1..3d89cca7 100644
--- a/pgconn/pgconn_test.go
+++ b/pgconn/pgconn_test.go
@@ -24,6 +24,7 @@ import (
 	"github.com/jackc/pgx/v5/internal/pgio"
 	"github.com/jackc/pgx/v5/internal/pgmock"
 	"github.com/jackc/pgx/v5/pgconn"
+	"github.com/jackc/pgx/v5/pgconn/ctxwatch"
 	"github.com/jackc/pgx/v5/pgproto3"
 	"github.com/jackc/pgx/v5/pgtype"
 )
@@ -1556,9 +1557,9 @@ func TestConnOnNotice(t *testing.T) {
 	config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
 	require.NoError(t, err)
 
-	var msg string
-	config.OnNotice = func(c *pgconn.PgConn, notice *pgconn.Notice) {
-		msg = notice.Message
+	var notice *pgconn.Notice
+	config.OnNotice = func(c *pgconn.PgConn, n *pgconn.Notice) {
+		notice = n
 	}
 	config.RuntimeParams["client_min_messages"] = "notice" // Ensure we only get the message we expect.
 
@@ -1576,7 +1577,8 @@ begin
 end$$;`)
 	err = multiResult.Close()
 	require.NoError(t, err)
-	assert.Equal(t, "hello, world", msg)
+	assert.Equal(t, "NOTICE", notice.SeverityUnlocalized)
+	assert.Equal(t, "hello, world", notice.Message)
 
 	ensureConnValid(t, pgConn)
 }
@@ -3479,3 +3481,161 @@ func mustEncode(buf []byte, err error) []byte {
 	}
 	return buf
 }
+
+func TestDeadlineContextWatcherHandler(t *testing.T) {
+	t.Parallel()
+
+	t.Run("DeadlineExceeded with zero DeadlineDelay", func(t *testing.T) {
+		config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+		require.NoError(t, err)
+		config.BuildContextWatcherHandler = func(conn *pgconn.PgConn) ctxwatch.Handler {
+			return &pgconn.DeadlineContextWatcherHandler{Conn: conn.Conn()}
+		}
+		config.ConnectTimeout = 5 * time.Second
+
+		pgConn, err := pgconn.ConnectConfig(context.Background(), config)
+		require.NoError(t, err)
+		defer closeConn(t, pgConn)
+
+		ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+		defer cancel()
+
+		_, err = pgConn.Exec(ctx, "select 1, pg_sleep(1)").ReadAll()
+		require.Error(t, err)
+		require.ErrorIs(t, err, context.DeadlineExceeded)
+		require.True(t, pgConn.IsClosed())
+	})
+
+	t.Run("DeadlineExceeded with DeadlineDelay", func(t *testing.T) {
+		config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+		require.NoError(t, err)
+		config.BuildContextWatcherHandler = func(conn *pgconn.PgConn) ctxwatch.Handler {
+			return &pgconn.DeadlineContextWatcherHandler{Conn: conn.Conn(), DeadlineDelay: 500 * time.Millisecond}
+		}
+		config.ConnectTimeout = 5 * time.Second
+
+		pgConn, err := pgconn.ConnectConfig(context.Background(), config)
+		require.NoError(t, err)
+		defer closeConn(t, pgConn)
+
+		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+		defer cancel()
+
+		_, err = pgConn.Exec(ctx, "select 1, pg_sleep(0.250)").ReadAll()
+		require.NoError(t, err)
+
+		ensureConnValid(t, pgConn)
+	})
+}
+
+func TestCancelRequestContextWatcherHandler(t *testing.T) {
+	t.Parallel()
+
+	t.Run("DeadlineExceeded cancels request after CancelRequestDelay", func(t *testing.T) {
+		config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+		require.NoError(t, err)
+		config.BuildContextWatcherHandler = func(conn *pgconn.PgConn) ctxwatch.Handler {
+			return &pgconn.CancelRequestContextWatcherHandler{
+				Conn:               conn,
+				CancelRequestDelay: 250 * time.Millisecond,
+				DeadlineDelay:      5000 * time.Millisecond,
+			}
+		}
+		config.ConnectTimeout = 5 * time.Second
+
+		pgConn, err := pgconn.ConnectConfig(context.Background(), config)
+		require.NoError(t, err)
+		defer closeConn(t, pgConn)
+
+		ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+		defer cancel()
+
+		_, err = pgConn.Exec(ctx, "select 1, pg_sleep(3)").ReadAll()
+		require.Error(t, err)
+		var pgErr *pgconn.PgError
+		require.ErrorAs(t, err, &pgErr)
+
+		ensureConnValid(t, pgConn)
+	})
+
+	t.Run("DeadlineExceeded - do not send cancel request when query finishes in grace period", func(t *testing.T) {
+		config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+		require.NoError(t, err)
+		config.BuildContextWatcherHandler = func(conn *pgconn.PgConn) ctxwatch.Handler {
+			return &pgconn.CancelRequestContextWatcherHandler{
+				Conn:               conn,
+				CancelRequestDelay: 1000 * time.Millisecond,
+				DeadlineDelay:      5000 * time.Millisecond,
+			}
+		}
+		config.ConnectTimeout = 5 * time.Second
+
+		pgConn, err := pgconn.ConnectConfig(context.Background(), config)
+		require.NoError(t, err)
+		defer closeConn(t, pgConn)
+
+		ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+		defer cancel()
+
+		_, err = pgConn.Exec(ctx, "select 1, pg_sleep(0.250)").ReadAll()
+		require.NoError(t, err)
+
+		ensureConnValid(t, pgConn)
+	})
+
+	t.Run("DeadlineExceeded sets conn deadline with DeadlineDelay", func(t *testing.T) {
+		config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+		require.NoError(t, err)
+		config.BuildContextWatcherHandler = func(conn *pgconn.PgConn) ctxwatch.Handler {
+			return &pgconn.CancelRequestContextWatcherHandler{
+				Conn:               conn,
+				CancelRequestDelay: 5000 * time.Millisecond, // purposely setting this higher than DeadlineDelay to ensure the cancel request never happens.
+				DeadlineDelay:      250 * time.Millisecond,
+			}
+		}
+		config.ConnectTimeout = 5 * time.Second
+
+		pgConn, err := pgconn.ConnectConfig(context.Background(), config)
+		require.NoError(t, err)
+		defer closeConn(t, pgConn)
+
+		ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+		defer cancel()
+
+		_, err = pgConn.Exec(ctx, "select 1, pg_sleep(1)").ReadAll()
+		require.Error(t, err)
+		require.ErrorIs(t, err, context.DeadlineExceeded)
+		require.True(t, pgConn.IsClosed())
+	})
+
+	for i := 0; i < 10; i++ {
+		t.Run(fmt.Sprintf("Stress %d", i), func(t *testing.T) {
+			t.Parallel()
+
+			config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_DATABASE"))
+			require.NoError(t, err)
+			config.BuildContextWatcherHandler = func(conn *pgconn.PgConn) ctxwatch.Handler {
+				return &pgconn.CancelRequestContextWatcherHandler{
+					Conn:               conn,
+					CancelRequestDelay: 5 * time.Millisecond,
+					DeadlineDelay:      1000 * time.Millisecond,
+				}
+			}
+			config.ConnectTimeout = 5 * time.Second
+
+			pgConn, err := pgconn.ConnectConfig(context.Background(), config)
+			require.NoError(t, err)
+			defer closeConn(t, pgConn)
+
+			for i := 0; i < 20; i++ {
+				func() {
+					ctx, cancel := context.WithTimeout(context.Background(), 4*time.Millisecond)
+					defer cancel()
+					pgConn.Exec(ctx, "select 1, pg_sleep(0.010)").ReadAll()
+					time.Sleep(100 * time.Millisecond) // ensure a cancel request that was a little late doesn't interrupt ensureConnValid.
+					ensureConnValid(t, pgConn)
+				}()
+			}
+		})
+	}
+}
diff --git a/pgtype/macaddr_test.go b/pgtype/macaddr_test.go
index 84cf970f..58149c87 100644
--- a/pgtype/macaddr_test.go
+++ b/pgtype/macaddr_test.go
@@ -48,4 +48,23 @@ func TestMacaddrCodec(t *testing.T) {
 		},
 		{nil, new(*net.HardwareAddr), isExpectedEq((*net.HardwareAddr)(nil))},
 	})
+
+	pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, pgxtest.KnownOIDQueryExecModes, "macaddr8", []pgxtest.ValueRoundTripTest{
+		{
+			mustParseMacaddr(t, "01:23:45:67:89:ab:01:08"),
+			new(net.HardwareAddr),
+			isExpectedEqHardwareAddr(mustParseMacaddr(t, "01:23:45:67:89:ab:01:08")),
+		},
+		{
+			"01:23:45:67:89:ab:01:08",
+			new(net.HardwareAddr),
+			isExpectedEqHardwareAddr(mustParseMacaddr(t, "01:23:45:67:89:ab:01:08")),
+		},
+		{
+			mustParseMacaddr(t, "01:23:45:67:89:ab:01:08"),
+			new(string),
+			isExpectedEq("01:23:45:67:89:ab:01:08"),
+		},
+		{nil, new(*net.HardwareAddr), isExpectedEq((*net.HardwareAddr)(nil))},
+	})
 }
diff --git a/pgtype/time_test.go b/pgtype/time_test.go
index 01bcee0f..06970bac 100644
--- a/pgtype/time_test.go
+++ b/pgtype/time_test.go
@@ -2,11 +2,13 @@ package pgtype_test
 
 import (
 	"context"
+	"strconv"
 	"testing"
 	"time"
 
 	"github.com/jackc/pgx/v5/pgtype"
 	"github.com/jackc/pgx/v5/pgxtest"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestTimeCodec(t *testing.T) {
@@ -45,3 +47,69 @@ func TestTimeCodec(t *testing.T) {
 		{nil, new(pgtype.Time), isExpectedEq(pgtype.Time{})},
 	})
 }
+
+func TestTimeTextScanner(t *testing.T) {
+	var pgTime pgtype.Time
+
+	assert.NoError(t, pgTime.Scan("07:37:16"))
+	assert.Equal(t, true, pgTime.Valid)
+	assert.Equal(t, int64(7*time.Hour+37*time.Minute+16*time.Second), pgTime.Microseconds*int64(time.Microsecond))
+
+	assert.NoError(t, pgTime.Scan("15:04:05"))
+	assert.Equal(t, true, pgTime.Valid)
+	assert.Equal(t, int64(15*time.Hour+4*time.Minute+5*time.Second), pgTime.Microseconds*int64(time.Microsecond))
+
+	// parsing of fractional digits
+	assert.NoError(t, pgTime.Scan("15:04:05.00"))
+	assert.Equal(t, true, pgTime.Valid)
+	assert.Equal(t, int64(15*time.Hour+4*time.Minute+5*time.Second), pgTime.Microseconds*int64(time.Microsecond))
+
+	const mirco = "789123"
+	const woFraction = int64(4*time.Hour + 5*time.Minute + 6*time.Second) // time without fraction
+	for i := 0; i <= len(mirco); i++ {
+		assert.NoError(t, pgTime.Scan("04:05:06."+mirco[:i]))
+		assert.Equal(t, true, pgTime.Valid)
+
+		frac, _ := strconv.ParseInt(mirco[:i], 10, 64)
+		for k := i; k < 6; k++ {
+			frac *= 10
+		}
+		assert.Equal(t, woFraction+frac*int64(time.Microsecond), pgTime.Microseconds*int64(time.Microsecond))
+	}
+
+	// parsing of too long fraction errors
+	assert.Error(t, pgTime.Scan("04:05:06.7891234"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+
+	// parsing of timetz errors
+	assert.Error(t, pgTime.Scan("04:05:06.789-08"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+
+	assert.Error(t, pgTime.Scan("04:05:06-08:00"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+
+	// parsing of date errors
+	assert.Error(t, pgTime.Scan("1997-12-17"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+
+	// parsing of text errors
+	assert.Error(t, pgTime.Scan("12345678"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+
+	assert.Error(t, pgTime.Scan("12-34-56"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+
+	assert.Error(t, pgTime.Scan("12:34-56"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+
+	assert.Error(t, pgTime.Scan("12-34:56"))
+	assert.Equal(t, false, pgTime.Valid)
+	assert.Equal(t, int64(0), pgTime.Microseconds)
+}
diff --git a/pgtype/timestamp_test.go b/pgtype/timestamp_test.go
index 24f229d5..31b3ad82 100644
--- a/pgtype/timestamp_test.go
+++ b/pgtype/timestamp_test.go
@@ -38,6 +38,42 @@ func TestTimestampCodec(t *testing.T) {
 	})
 }
 
+func TestTimestampCodecWithScanLocationUTC(t *testing.T) {
+	skipCockroachDB(t, "Server does not support infinite timestamps (see https://github.com/cockroachdb/cockroach/issues/41564)")
+
+	connTestRunner := defaultConnTestRunner
+	connTestRunner.AfterConnect = func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
+		conn.TypeMap().RegisterType(&pgtype.Type{
+			Name:  "timestamp",
+			OID:   pgtype.TimestampOID,
+			Codec: &pgtype.TimestampCodec{ScanLocation: time.UTC},
+		})
+	}
+
+	pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, nil, "timestamp", []pgxtest.ValueRoundTripTest{
+		// Have to use pgtype.Timestamp instead of time.Time as source because otherwise the simple and exec query exec
+		// modes will encode the time for timestamptz. That is, they will convert it from local time zone.
+		{pgtype.Timestamp{Time: time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local), Valid: true}, new(time.Time), isExpectedEq(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))},
+	})
+}
+
+func TestTimestampCodecWithScanLocationLocal(t *testing.T) {
+	skipCockroachDB(t, "Server does not support infinite timestamps (see https://github.com/cockroachdb/cockroach/issues/41564)")
+
+	connTestRunner := defaultConnTestRunner
+	connTestRunner.AfterConnect = func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
+		conn.TypeMap().RegisterType(&pgtype.Type{
+			Name:  "timestamp",
+			OID:   pgtype.TimestampOID,
+			Codec: &pgtype.TimestampCodec{ScanLocation: time.Local},
+		})
+	}
+
+	pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, nil, "timestamp", []pgxtest.ValueRoundTripTest{
+		{time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), new(time.Time), isExpectedEq(time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local))},
+	})
+}
+
 // https://github.com/jackc/pgx/v4/pgtype/pull/128
 func TestTimestampTranscodeBigTimeBinary(t *testing.T) {
 	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
diff --git a/pgtype/timestamptz_test.go b/pgtype/timestamptz_test.go
index 0486ecdb..57248195 100644
--- a/pgtype/timestamptz_test.go
+++ b/pgtype/timestamptz_test.go
@@ -38,6 +38,40 @@ func TestTimestamptzCodec(t *testing.T) {
 	})
 }
 
+func TestTimestamptzCodecWithLocationUTC(t *testing.T) {
+	skipCockroachDB(t, "Server does not support infinite timestamps (see https://github.com/cockroachdb/cockroach/issues/41564)")
+
+	connTestRunner := defaultConnTestRunner
+	connTestRunner.AfterConnect = func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
+		conn.TypeMap().RegisterType(&pgtype.Type{
+			Name:  "timestamptz",
+			OID:   pgtype.TimestamptzOID,
+			Codec: &pgtype.TimestamptzCodec{ScanLocation: time.UTC},
+		})
+	}
+
+	pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, nil, "timestamptz", []pgxtest.ValueRoundTripTest{
+		{time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), new(time.Time), isExpectedEq(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))},
+	})
+}
+
+func TestTimestamptzCodecWithLocationLocal(t *testing.T) {
+	skipCockroachDB(t, "Server does not support infinite timestamps (see https://github.com/cockroachdb/cockroach/issues/41564)")
+
+	connTestRunner := defaultConnTestRunner
+	connTestRunner.AfterConnect = func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
+		conn.TypeMap().RegisterType(&pgtype.Type{
+			Name:  "timestamptz",
+			OID:   pgtype.TimestamptzOID,
+			Codec: &pgtype.TimestamptzCodec{ScanLocation: time.Local},
+		})
+	}
+
+	pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, nil, "timestamptz", []pgxtest.ValueRoundTripTest{
+		{time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local), new(time.Time), isExpectedEq(time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local))},
+	})
+}
+
 // https://github.com/jackc/pgx/v4/pgtype/pull/128
 func TestTimestamptzTranscodeBigTimeBinary(t *testing.T) {
 	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
diff --git a/values_test.go b/values_test.go
index 5369b836..132b79a2 100644
--- a/values_test.go
+++ b/values_test.go
@@ -216,7 +216,7 @@ func testJSONInt16ArrayFailureDueToOverflow(t *testing.T, conn *pgx.Conn, typena
 	var output []int16
 	err := conn.QueryRow(context.Background(), "select $1::"+typename, input).Scan(&output)
 	if err == nil || err.Error() != "can't scan into dest[0]: json: cannot unmarshal number 234432 into Go value of type int16" {
-		t.Errorf("%s: Expected *json.UnmarkalTypeError, but got %v", typename, err)
+		t.Errorf("%s: Expected *json.UnmarshalTypeError, but got %v", typename, err)
 	}
 }