Repository: jackc/pgx
Pull request #1377 (2022-11-10 12:49:38)

Add DeallocateAll to pgx.Conn to clear prepared statement cache
Please provide feedback to the PR. Related [discussion](https://github.com/jackc/pgx/discussions/1370).


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3bfe728d..d6ce7716 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,9 +18,9 @@ jobs:
     strategy:
       matrix:
         go-version: [1.18]
-        pg-version: [10, 11, 12, 13, 14, cockroachdb]
+        pg-version: [11, 12, 13, 14, 15, cockroachdb]
         include:
-          - pg-version: 10
+          - pg-version: 11
             pgx-test-database: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
             pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test"
             pgx-test-tcp-conn-string: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
@@ -30,7 +30,7 @@ jobs:
             pgx-test-tls-conn-string: "host=localhost user=pgx_ssl password=secret sslmode=verify-full sslrootcert=/tmp/ca.pem dbname=pgx_test"
             pgx-ssl-password: certpw
             pgx-test-tls-client-conn-string: "host=localhost user=pgx_sslcert sslmode=verify-full sslrootcert=/tmp/ca.pem sslcert=/tmp/pgx_sslcert.crt sslkey=/tmp/pgx_sslcert.key dbname=pgx_test"
-          - pg-version: 11
+          - pg-version: 12
             pgx-test-database: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
             pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test"
             pgx-test-tcp-conn-string: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
@@ -40,7 +40,7 @@ jobs:
             pgx-test-tls-conn-string: "host=localhost user=pgx_ssl password=secret sslmode=verify-full sslrootcert=/tmp/ca.pem dbname=pgx_test"
             pgx-ssl-password: certpw
             pgx-test-tls-client-conn-string: "host=localhost user=pgx_sslcert sslmode=verify-full sslrootcert=/tmp/ca.pem sslcert=/tmp/pgx_sslcert.crt sslkey=/tmp/pgx_sslcert.key dbname=pgx_test"
-          - pg-version: 12
+          - pg-version: 13
             pgx-test-database: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
             pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test"
             pgx-test-tcp-conn-string: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
@@ -50,7 +50,7 @@ jobs:
             pgx-test-tls-conn-string: "host=localhost user=pgx_ssl password=secret sslmode=verify-full sslrootcert=/tmp/ca.pem dbname=pgx_test"
             pgx-ssl-password: certpw
             pgx-test-tls-client-conn-string: "host=localhost user=pgx_sslcert sslmode=verify-full sslrootcert=/tmp/ca.pem sslcert=/tmp/pgx_sslcert.crt sslkey=/tmp/pgx_sslcert.key dbname=pgx_test"
-          - pg-version: 13
+          - pg-version: 14
             pgx-test-database: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
             pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test"
             pgx-test-tcp-conn-string: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
@@ -60,7 +60,7 @@ jobs:
             pgx-test-tls-conn-string: "host=localhost user=pgx_ssl password=secret sslmode=verify-full sslrootcert=/tmp/ca.pem dbname=pgx_test"
             pgx-ssl-password: certpw
             pgx-test-tls-client-conn-string: "host=localhost user=pgx_sslcert sslmode=verify-full sslrootcert=/tmp/ca.pem sslcert=/tmp/pgx_sslcert.crt sslkey=/tmp/pgx_sslcert.key dbname=pgx_test"
-          - pg-version: 14
+          - pg-version: 15
             pgx-test-database: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
             pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test"
             pgx-test-tcp-conn-string: "host=127.0.0.1 user=pgx_md5 password=secret dbname=pgx_test"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8a3f8dbd..3eb0da5b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,5 +1,15 @@
 # Contributing
 
+## Discuss Significant Changes
+
+Before you invest a significant amount of time on a change, please create a discussion or issue describing your
+proposal. This will help to ensure your proposed change has a reasonable chance of being merged.
+
+## Avoid Dependencies
+
+Adding a dependency is a big deal. While on occasion a new dependency may be accepted, the default answer to any change
+that adds a dependency is no.
+
 ## Development Environment Setup
 
 pgx tests naturally require a PostgreSQL database. It will connect to the database specified in the `PGX_TEST_DATABASE`
diff --git a/README.md b/README.md
index 58a39b87..c21a182a 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![](https://godoc.org/github.com/jackc/pgx?status.svg)](https://pkg.go.dev/github.com/jackc/pgx/v5)
+[![Go Reference](https://pkg.go.dev/badge/github.com/jackc/pgx/v5.svg)](https://pkg.go.dev/github.com/jackc/pgx/v5)
 ![Build Status](https://github.com/jackc/pgx/actions/workflows/ci.yml/badge.svg)
 
 # pgx - PostgreSQL Driver and Toolkit
@@ -88,7 +88,7 @@ See CONTRIBUTING.md for setup instructions.
 
 ## Supported Go and PostgreSQL Versions
 
-pgx supports the same versions of Go and PostgreSQL that are supported by their respective teams. For [Go](https://golang.org/doc/devel/release.html#policy) that is the two most recent major releases and for [PostgreSQL](https://www.postgresql.org/support/versioning/) the major releases in the last 5 years. This means pgx supports Go 1.18 and higher and PostgreSQL 10 and higher. pgx also is tested against the latest version of [CockroachDB](https://www.cockroachlabs.com/product/).
+pgx supports the same versions of Go and PostgreSQL that are supported by their respective teams. For [Go](https://golang.org/doc/devel/release.html#policy) that is the two most recent major releases and for [PostgreSQL](https://www.postgresql.org/support/versioning/) the major releases in the last 5 years. This means pgx supports Go 1.18 and higher and PostgreSQL 11 and higher. pgx also is tested against the latest version of [CockroachDB](https://www.cockroachlabs.com/product/).
 
 ## Version Policy
 
diff --git a/conn.go b/conn.go
index cb5034bc..91b917cb 100644
--- a/conn.go
+++ b/conn.go
@@ -326,6 +326,19 @@ func (c *Conn) Deallocate(ctx context.Context, name string) error {
 	return err
 }
 
+// DeallocateAll releases all previously prepared statements from the server and client, where it also resets the statement and description cache.
+func (c *Conn) DeallocateAll(ctx context.Context, name string) error {
+	c.preparedStatements = map[string]*pgconn.StatementDescription{}
+	if c.config.StatementCacheCapacity > 0 {
+		c.statementCache = stmtcache.NewLRUCache(c.config.StatementCacheCapacity)
+	}
+	if c.config.DescriptionCacheCapacity > 0 {
+		c.descriptionCache = stmtcache.NewLRUCache(c.config.DescriptionCacheCapacity)
+	}
+	_, err := c.pgConn.Exec(ctx, "deallocate all").ReadAll()
+	return err
+}
+
 func (c *Conn) bufferNotifications(_ *pgconn.PgConn, n *pgconn.Notification) {
 	c.notifications = append(c.notifications, n)
 }
@@ -1147,8 +1160,9 @@ func (c *Conn) LoadType(ctx context.Context, typeName string) (*pgtype.Type, err
 	}
 
 	var typtype string
+	var typbasetype uint32
 
-	err = c.QueryRow(ctx, "select typtype::text from pg_type where oid=$1", oid).Scan(&typtype)
+	err = c.QueryRow(ctx, "select typtype::text, typbasetype from pg_type where oid=$1", oid).Scan(&typtype, &typbasetype)
 	if err != nil {
 		return nil, err
 	}
@@ -1173,6 +1187,13 @@ func (c *Conn) LoadType(ctx context.Context, typeName string) (*pgtype.Type, err
 		}
 
 		return &pgtype.Type{Name: typeName, OID: oid, Codec: &pgtype.CompositeCodec{Fields: fields}}, nil
+	case "d": // domain
+		dt, ok := c.TypeMap().TypeForOID(typbasetype)
+		if !ok {
+			return nil, errors.New("domain base type OID not registered")
+		}
+
+		return &pgtype.Type{Name: typeName, OID: oid, Codec: dt.Codec}, nil
 	case "e": // enum
 		return &pgtype.Type{Name: typeName, OID: oid, Codec: &pgtype.EnumCodec{}}, nil
 	default:
diff --git a/go.mod b/go.mod
index 89c048bf..542bb1d6 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.18
 require (
 	github.com/jackc/pgpassfile v1.0.0
 	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b
-	github.com/jackc/puddle/v2 v2.0.1
+	github.com/jackc/puddle/v2 v2.1.2
 	github.com/stretchr/testify v1.8.0
 	golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
 	golang.org/x/text v0.3.8
@@ -15,6 +15,8 @@ require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/kr/pretty v0.3.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	go.uber.org/atomic v1.10.0 // indirect
+	golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 9e4b3322..63f3bde2 100644
--- a/go.sum
+++ b/go.sum
@@ -8,6 +8,8 @@ github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHF
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
 github.com/jackc/puddle/v2 v2.0.1 h1:s8ZKO/+G8DLJNzPY8N58zA34IM1R9OjTel1pdTAunVk=
 github.com/jackc/puddle/v2 v2.0.1/go.mod h1:itE7ZJY8xnoo0JqJEpSMprN0f+NQkMCuEV/N9j8h0oc=
+github.com/jackc/puddle/v2 v2.1.2 h1:0f7vaaXINONKTsxYDn4otOAiJanX/BMeAtY//BXqzlg=
+github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -27,8 +29,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
+go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
 golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc=
+golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/pgproto3/error_response.go b/pgproto3/error_response.go
index ec51e019..45c9a981 100644
--- a/pgproto3/error_response.go
+++ b/pgproto3/error_response.go
@@ -215,7 +215,6 @@ func (src *ErrorResponse) marshalBinary(typeByte byte) []byte {
 
 	for k, v := range src.UnknownFields {
 		buf.WriteByte(k)
-		buf.WriteByte(0)
 		buf.WriteString(v)
 		buf.WriteByte(0)
 	}
diff --git a/pgtype/date.go b/pgtype/date.go
index 24ed20cc..009fc0db 100644
--- a/pgtype/date.go
+++ b/pgtype/date.go
@@ -5,6 +5,7 @@ import (
 	"encoding/binary"
 	"encoding/json"
 	"fmt"
+	"regexp"
 	"strconv"
 	"time"
 
@@ -193,7 +194,11 @@ func (encodePlanDateCodecText) Encode(value any, buf []byte) (newBuf []byte, err
 			bc = true
 		}
 
-		buf = strconv.AppendInt(buf, int64(year), 10)
+		yearBytes := strconv.AppendInt(make([]byte, 0, 6), int64(year), 10)
+		for i := len(yearBytes); i < 4; i++ {
+			buf = append(buf, '0')
+		}
+		buf = append(buf, yearBytes...)
 		buf = append(buf, '-')
 		if date.Time.Month() < 10 {
 			buf = append(buf, '0')
@@ -263,6 +268,8 @@ func (scanPlanBinaryDateToDateScanner) Scan(src []byte, dst any) error {
 
 type scanPlanTextAnyToDateScanner struct{}
 
+var dateRegexp = regexp.MustCompile(`^(\d{4,})-(\d\d)-(\d\d)( BC)?$`)
+
 func (scanPlanTextAnyToDateScanner) Scan(src []byte, dst any) error {
 	scanner := (dst).(DateScanner)
 
@@ -271,40 +278,58 @@ func (scanPlanTextAnyToDateScanner) Scan(src []byte, dst any) error {
 	}
 
 	sbuf := string(src)
+	match := dateRegexp.FindStringSubmatch(sbuf)
+	if match != nil {
+		year, err := strconv.ParseInt(match[1], 10, 32)
+		if err != nil {
+			return fmt.Errorf("BUG: cannot parse date that regexp matched (year): %v", err)
+		}
+
+		month, err := strconv.ParseInt(match[2], 10, 32)
+		if err != nil {
+			return fmt.Errorf("BUG: cannot parse date that regexp matched (month): %v", err)
+		}
+
+		day, err := strconv.ParseInt(match[3], 10, 32)
+		if err != nil {
+			return fmt.Errorf("BUG: cannot parse date that regexp matched (month): %v", err)
+		}
+
+		// BC matched
+		if len(match[4]) > 0 {
+			year = -year + 1
+		}
+
+		t := time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC)
+		return scanner.ScanDate(Date{Time: t, Valid: true})
+	}
+
 	switch sbuf {
 	case "infinity":
 		return scanner.ScanDate(Date{InfinityModifier: Infinity, Valid: true})
 	case "-infinity":
 		return scanner.ScanDate(Date{InfinityModifier: -Infinity, Valid: true})
 	default:
-		if len(sbuf) >= 10 {
-			year, err := strconv.ParseInt(sbuf[0:4], 10, 32)
-			if err != nil {
-				return fmt.Errorf("cannot parse year: %v", err)
-			}
-			month, err := strconv.ParseInt(sbuf[5:7], 10, 32)
-			if err != nil {
-				return fmt.Errorf("cannot parse month: %v", err)
-			}
-			day, err := strconv.ParseInt(sbuf[8:10], 10, 32)
-			if err != nil {
-				return fmt.Errorf("cannot parse day: %v", err)
-			}
-
-			if len(sbuf) == 13 && sbuf[11:] == "BC" {
-				year = -year + 1
-			}
-
-			t := time.Date(int(year), time.Month(month), int(day), 0, 0, 0, 0, time.UTC)
-			return scanner.ScanDate(Date{Time: t, Valid: true})
-		} else {
-			return fmt.Errorf("date too short")
-		}
+		return fmt.Errorf("invalid date format")
 	}
 }
 
 func (c DateCodec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
-	return codecDecodeToTextFormat(c, m, oid, format, src)
+	if src == nil {
+		return nil, nil
+	}
+
+	var date Date
+	err := codecScan(c, m, oid, format, src, &date)
+	if err != nil {
+		return nil, err
+	}
+
+	if date.InfinityModifier != Finite {
+		return date.InfinityModifier.String(), nil
+	}
+
+	return date.Time, nil
 }
 
 func (c DateCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
@@ -318,16 +343,9 @@ func (c DateCodec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (an
 		return nil, err
 	}
 
-	if date.Valid {
-		switch date.InfinityModifier {
-		case Finite:
-			return date.Time, nil
-		case Infinity:
-			return "infinity", nil
-		case NegativeInfinity:
-			return "-infinity", nil
-		}
+	if date.InfinityModifier != Finite {
+		return date.InfinityModifier, nil
 	}
 
-	return nil, nil
+	return date.Time, nil
 }
diff --git a/rows.go b/rows.go
index 5fea9883..409885e0 100644
--- a/rows.go
+++ b/rows.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"reflect"
+	"strings"
 	"time"
 
 	"github.com/jackc/pgx/v5/internal/stmtcache"
@@ -544,3 +545,105 @@ func (rs *positionalStructRowScanner) appendScanTargets(dstElemValue reflect.Val
 
 	return scanTargets
 }
+
+// RowToStructByName returns a T scanned from row. T must be a struct. T must have the same number of named public
+// fields as row has fields. The row and T fields will by matched by name. The match is case-insensitive. The database
+// 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})
+	return value, err
+}
+
+// RowToAddrOfStructByPos returns the address of a T scanned from row. T must be a struct. T must have the same number
+// of named public fields as row has fields. The row and T fields will by matched by name. The match is
+// case-insensitive. The database column name can be overridden with a "db" struct tag. If the "db" struct tag is "-"
+// then the field will be ignored.
+func RowToAddrOfStructByName[T any](row CollectableRow) (*T, error) {
+	var value T
+	err := row.Scan(&namedStructRowScanner{ptrToStruct: &value})
+	return &value, err
+}
+
+type namedStructRowScanner struct {
+	ptrToStruct any
+}
+
+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())
+
+	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)
+		}
+	}
+
+	return rows.Scan(scanTargets...)
+}
+
+const structTagKey = "db"
+
+func fieldPosByName(fldDescs []pgconn.FieldDescription, field string) (i int) {
+	i = -1
+	for i, desc := range fldDescs {
+		if strings.EqualFold(desc.Name, field) {
+			return i
+		}
+	}
+	return
+}
+
+func (rs *namedStructRowScanner) appendScanTargets(dstElemValue reflect.Value, scanTargets []any, fldDescs []pgconn.FieldDescription) ([]any, error) {
+	var err error
+	dstElemType := dstElemValue.Type()
+
+	if scanTargets == nil {
+		scanTargets = make([]any, len(fldDescs))
+	}
+
+	for i := 0; i < dstElemType.NumField(); i++ {
+		sf := dstElemType.Field(i)
+		if sf.PkgPath != "" && !sf.Anonymous {
+			// Field is unexported, skip it.
+			continue
+		}
+		// Handle anoymous 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
+			}
+		} else {
+			dbTag, dbTagPresent := sf.Tag.Lookup(structTagKey)
+			if dbTagPresent {
+				dbTag = strings.Split(dbTag, ",")[0]
+			}
+			if dbTag == "-" {
+				// Field is ignored, skip it.
+				continue
+			}
+			colName := dbTag
+			if !dbTagPresent {
+				colName = sf.Name
+			}
+			fpos := fieldPosByName(fldDescs, colName)
+			if fpos == -1 || fpos >= len(scanTargets) {
+				return nil, fmt.Errorf("cannot find field %s in returned row", colName)
+			}
+			scanTargets[fpos] = dstElemValue.Field(i).Addr().Interface()
+		}
+	}
+
+	return scanTargets, err
+}


Test patch:
diff --git a/conn_test.go b/conn_test.go
index 204ff615..0fafeb18 100644
--- a/conn_test.go
+++ b/conn_test.go
@@ -424,7 +424,7 @@ func TestPrepare(t *testing.T) {
 		t.Errorf("Prepared statement did not return expected value: %v", s)
 	}
 
-	err = conn.Deallocate(context.Background(), "test")
+	err = conn.DeallocateAll(context.Background(), "test")
 	if err != nil {
 		t.Errorf("conn.Deallocate failed: %v", err)
 	}
@@ -837,24 +837,21 @@ func TestDomainType(t *testing.T) {
 		// uint64 but a result OID of the underlying numeric.
 
 		var s string
-		err := conn.QueryRow(context.Background(), "select $1::uint64", "24").Scan(&s)
+		err := conn.QueryRow(ctx, "select $1::uint64", "24").Scan(&s)
 		require.NoError(t, err)
 		require.Equal(t, "24", s)
 
 		// Register type
-		var uint64OID uint32
-		err = conn.QueryRow(context.Background(), "select t.oid from pg_type t where t.typname='uint64';").Scan(&uint64OID)
-		if err != nil {
-			t.Fatalf("did not find uint64 OID, %v", err)
-		}
-		conn.TypeMap().RegisterType(&pgtype.Type{Name: "uint64", OID: uint64OID, Codec: pgtype.NumericCodec{}})
+		uint64Type, err := conn.LoadType(ctx, "uint64")
+		require.NoError(t, err)
+		conn.TypeMap().RegisterType(uint64Type)
 
 		var n uint64
-		err = conn.QueryRow(context.Background(), "select $1::uint64", uint64(24)).Scan(&n)
+		err = conn.QueryRow(ctx, "select $1::uint64", uint64(24)).Scan(&n)
 		require.NoError(t, err)
 
 		// String is still an acceptable argument after registration
-		err = conn.QueryRow(context.Background(), "select $1::uint64", "7").Scan(&n)
+		err = conn.QueryRow(ctx, "select $1::uint64", "7").Scan(&n)
 		if err != nil {
 			t.Fatal(err)
 		}
diff --git a/pgtype/date_test.go b/pgtype/date_test.go
index de61fd72..c7620fcf 100644
--- a/pgtype/date_test.go
+++ b/pgtype/date_test.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/jackc/pgx/v5/pgtype"
 	"github.com/jackc/pgx/v5/pgxtest"
+	"github.com/stretchr/testify/assert"
 )
 
 func isExpectedEqTime(a any) func(any) bool {
@@ -30,6 +31,7 @@ func TestDateCodec(t *testing.T) {
 		{time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), new(time.Time), isExpectedEqTime(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))},
 		{time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC), new(time.Time), isExpectedEqTime(time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC))},
 		{time.Date(2200, 1, 1, 0, 0, 0, 0, time.UTC), new(time.Time), isExpectedEqTime(time.Date(2200, 1, 1, 0, 0, 0, 0, time.UTC))},
+		{time.Date(12200, 1, 2, 0, 0, 0, 0, time.UTC), new(time.Time), isExpectedEqTime(time.Date(12200, 1, 2, 0, 0, 0, 0, time.UTC))},
 		{pgtype.Date{InfinityModifier: pgtype.Infinity, Valid: true}, new(pgtype.Date), isExpectedEq(pgtype.Date{InfinityModifier: pgtype.Infinity, Valid: true})},
 		{pgtype.Date{InfinityModifier: pgtype.NegativeInfinity, Valid: true}, new(pgtype.Date), isExpectedEq(pgtype.Date{InfinityModifier: pgtype.NegativeInfinity, Valid: true})},
 		{pgtype.Date{}, new(pgtype.Date), isExpectedEq(pgtype.Date{})},
@@ -37,6 +39,30 @@ func TestDateCodec(t *testing.T) {
 	})
 }
 
+func TestDateCodecTextEncode(t *testing.T) {
+	m := pgtype.NewMap()
+
+	successfulTests := []struct {
+		source pgtype.Date
+		result string
+	}{
+		{source: pgtype.Date{Time: time.Date(2012, 3, 29, 0, 0, 0, 0, time.UTC), Valid: true}, result: "2012-03-29"},
+		{source: pgtype.Date{Time: time.Date(2012, 3, 29, 10, 5, 45, 0, time.FixedZone("", -6*60*60)), Valid: true}, result: "2012-03-29"},
+		{source: pgtype.Date{Time: time.Date(2012, 3, 29, 10, 5, 45, 555*1000*1000, time.FixedZone("", -6*60*60)), Valid: true}, result: "2012-03-29"},
+		{source: pgtype.Date{Time: time.Date(789, 1, 2, 0, 0, 0, 0, time.UTC), Valid: true}, result: "0789-01-02"},
+		{source: pgtype.Date{Time: time.Date(89, 1, 2, 0, 0, 0, 0, time.UTC), Valid: true}, result: "0089-01-02"},
+		{source: pgtype.Date{Time: time.Date(9, 1, 2, 0, 0, 0, 0, time.UTC), Valid: true}, result: "0009-01-02"},
+		{source: pgtype.Date{Time: time.Date(12200, 1, 2, 0, 0, 0, 0, time.UTC), Valid: true}, result: "12200-01-02"},
+		{source: pgtype.Date{InfinityModifier: pgtype.Infinity, Valid: true}, result: "infinity"},
+		{source: pgtype.Date{InfinityModifier: pgtype.NegativeInfinity, Valid: true}, result: "-infinity"},
+	}
+	for i, tt := range successfulTests {
+		buf, err := m.Encode(pgtype.DateOID, pgtype.TextFormatCode, tt.source, nil)
+		assert.NoErrorf(t, err, "%d", i)
+		assert.Equalf(t, tt.result, string(buf), "%d", i)
+	}
+}
+
 func TestDateMarshalJSON(t *testing.T) {
 	successfulTests := []struct {
 		source pgtype.Date
diff --git a/rows_test.go b/rows_test.go
index 60cf1cd4..ca9c3010 100644
--- a/rows_test.go
+++ b/rows_test.go
@@ -508,3 +508,130 @@ func TestRowToAddrOfStructPos(t *testing.T) {
 		}
 	})
 }
+
+func TestRowToStructByName(t *testing.T) {
+	type person struct {
+		Last  string
+		First string
+		Age   int32
+	}
+
+	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
+		rows, _ := conn.Query(ctx, `select 'John' as first, 'Smith' as last, n as age from generate_series(0, 9) n`)
+		slice, err := pgx.CollectRows(rows, pgx.RowToStructByName[person])
+		assert.NoError(t, err)
+
+		assert.Len(t, slice, 10)
+		for i := range slice {
+			assert.Equal(t, "Smith", slice[i].Last)
+			assert.Equal(t, "John", slice[i].First)
+			assert.EqualValues(t, i, slice[i].Age)
+		}
+
+		// check missing fields in a returned row
+		rows, _ = conn.Query(ctx, `select 'Smith' as last, n as age from generate_series(0, 9) n`)
+		_, err = pgx.CollectRows(rows, pgx.RowToStructByName[person])
+		assert.ErrorContains(t, err, "cannot find field First in returned row")
+
+		// check missing field in a destination struct
+		rows, _ = conn.Query(ctx, `select 'John' as first, 'Smith' as last, n as age, null as ignore from generate_series(0, 9) n`)
+		_, err = pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[person])
+		assert.ErrorContains(t, err, "struct doesn't have corresponding row field ignore")
+	})
+}
+
+func TestRowToStructByNameEmbeddedStruct(t *testing.T) {
+	type Name struct {
+		Last  string `db:"last_name"`
+		First string `db:"first_name"`
+	}
+
+	type person struct {
+		Ignore bool `db:"-"`
+		Name
+		Age int32
+	}
+
+	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
+		rows, _ := conn.Query(ctx, `select 'John' as first_name, 'Smith' as last_name, n as age from generate_series(0, 9) n`)
+		slice, err := pgx.CollectRows(rows, pgx.RowToStructByName[person])
+		assert.NoError(t, err)
+
+		assert.Len(t, slice, 10)
+		for i := range slice {
+			assert.Equal(t, "Smith", slice[i].Name.Last)
+			assert.Equal(t, "John", slice[i].Name.First)
+			assert.EqualValues(t, i, slice[i].Age)
+		}
+
+		// check missing fields in a returned row
+		rows, _ = conn.Query(ctx, `select 'Smith' as last_name, n as age from generate_series(0, 9) n`)
+		_, err = pgx.CollectRows(rows, pgx.RowToStructByName[person])
+		assert.ErrorContains(t, err, "cannot find field first_name in returned row")
+
+		// check missing field in a destination struct
+		rows, _ = conn.Query(ctx, `select 'John' as first_name, 'Smith' as last_name, n as age, null as ignore from generate_series(0, 9) n`)
+		_, err = pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[person])
+		assert.ErrorContains(t, err, "struct doesn't have corresponding row field ignore")
+	})
+}
+
+func ExampleRowToStructByName() {
+	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+	defer cancel()
+
+	conn, err := pgx.Connect(ctx, os.Getenv("PGX_TEST_DATABASE"))
+	if err != nil {
+		fmt.Printf("Unable to establish connection: %v", err)
+		return
+	}
+
+	if conn.PgConn().ParameterStatus("crdb_version") != "" {
+		// Skip test / example when running on CockroachDB. Since an example can't be skipped fake success instead.
+		fmt.Println(`Cheeseburger: $10
+Fries: $5
+Soft Drink: $3`)
+		return
+	}
+
+	// Setup example schema and data.
+	_, err = conn.Exec(ctx, `
+create temporary table products (
+	id int primary key generated by default as identity,
+	name varchar(100) not null,
+	price int not null
+);
+
+insert into products (name, price) values
+	('Cheeseburger', 10),
+	('Double Cheeseburger', 14),
+	('Fries', 5),
+	('Soft Drink', 3);
+`)
+	if err != nil {
+		fmt.Printf("Unable to setup example schema and data: %v", err)
+		return
+	}
+
+	type product struct {
+		ID    int32
+		Name  string
+		Price int32
+	}
+
+	rows, _ := conn.Query(ctx, "select * from products where price < $1 order by price desc", 12)
+	products, err := pgx.CollectRows(rows, pgx.RowToStructByName[product])
+	if err != nil {
+		fmt.Printf("CollectRows error: %v", err)
+		return
+	}
+
+	for _, p := range products {
+		fmt.Printf("%s: $%d\n", p.Name, p.Price)
+	}
+
+	// Output:
+	// Cheeseburger: $10
+	// Fries: $5
+	// Soft Drink: $3
+}