Skip to content

Commit b4911f1

Browse files
authored
Merge pull request #2019 from jackc/fix-encode-driver-valuer-on-pointer
Fix encode driver.Valuer on pointer
2 parents 24c0a5e + 9ca9203 commit b4911f1

File tree

10 files changed

+241
-133
lines changed

10 files changed

+241
-133
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ See the presentation at Golang Estonia, [PGX Top to Bottom](https://www.youtube.
9292

9393
## Supported Go and PostgreSQL Versions
9494

95-
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.20 and higher and PostgreSQL 12 and higher. pgx also is tested against the latest version of [CockroachDB](https://www.cockroachlabs.com/product/).
95+
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.21 and higher and PostgreSQL 12 and higher. pgx also is tested against the latest version of [CockroachDB](https://www.cockroachlabs.com/product/).
9696

9797
## Version Policy
9898

conn.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"strings"
1111
"time"
1212

13-
"github.com/jackc/pgx/v5/internal/anynil"
1413
"github.com/jackc/pgx/v5/internal/sanitize"
1514
"github.com/jackc/pgx/v5/internal/stmtcache"
1615
"github.com/jackc/pgx/v5/pgconn"
@@ -755,7 +754,6 @@ optionLoop:
755754
}
756755

757756
c.eqb.reset()
758-
anynil.NormalizeSlice(args)
759757
rows := c.getRows(ctx, sql, args)
760758

761759
var err error

extended_query_builder.go

Lines changed: 8 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package pgx
22

33
import (
4-
"database/sql/driver"
54
"fmt"
65

7-
"github.com/jackc/pgx/v5/internal/anynil"
86
"github.com/jackc/pgx/v5/pgconn"
97
"github.com/jackc/pgx/v5/pgtype"
108
)
@@ -23,10 +21,15 @@ type ExtendedQueryBuilder struct {
2321
func (eqb *ExtendedQueryBuilder) Build(m *pgtype.Map, sd *pgconn.StatementDescription, args []any) error {
2422
eqb.reset()
2523

26-
anynil.NormalizeSlice(args)
27-
2824
if sd == nil {
29-
return eqb.appendParamsForQueryExecModeExec(m, args)
25+
for i := range args {
26+
err := eqb.appendParam(m, 0, pgtype.TextFormatCode, args[i])
27+
if err != nil {
28+
err = fmt.Errorf("failed to encode args[%d]: %w", i, err)
29+
return err
30+
}
31+
}
32+
return nil
3033
}
3134

3235
if len(sd.ParamOIDs) != len(args) {
@@ -113,10 +116,6 @@ func (eqb *ExtendedQueryBuilder) reset() {
113116
}
114117

115118
func (eqb *ExtendedQueryBuilder) encodeExtendedParamValue(m *pgtype.Map, oid uint32, formatCode int16, arg any) ([]byte, error) {
116-
if anynil.Is(arg) {
117-
return nil, nil
118-
}
119-
120119
if eqb.paramValueBytes == nil {
121120
eqb.paramValueBytes = make([]byte, 0, 128)
122121
}
@@ -145,74 +144,3 @@ func (eqb *ExtendedQueryBuilder) chooseParameterFormatCode(m *pgtype.Map, oid ui
145144

146145
return m.FormatCodeForOID(oid)
147146
}
148-
149-
// appendParamsForQueryExecModeExec appends the args to eqb.
150-
//
151-
// Parameters must be encoded in the text format because of differences in type conversion between timestamps and
152-
// dates. In QueryExecModeExec we don't know what the actual PostgreSQL type is. To determine the type we use the
153-
// Go type to OID type mapping registered by RegisterDefaultPgType. However, the Go time.Time represents both
154-
// PostgreSQL timestamp[tz] and date. To use the binary format we would need to also specify what the PostgreSQL
155-
// type OID is. But that would mean telling PostgreSQL that we have sent a timestamp[tz] when what is needed is a date.
156-
// This means that the value is converted from text to timestamp[tz] to date. This means it does a time zone conversion
157-
// before converting it to date. This means that dates can be shifted by one day. In text format without that double
158-
// type conversion it takes the date directly and ignores time zone (i.e. it works).
159-
//
160-
// Given that the whole point of QueryExecModeExec is to operate without having to know the PostgreSQL types there is
161-
// no way to safely use binary or to specify the parameter OIDs.
162-
func (eqb *ExtendedQueryBuilder) appendParamsForQueryExecModeExec(m *pgtype.Map, args []any) error {
163-
for _, arg := range args {
164-
if arg == nil {
165-
err := eqb.appendParam(m, 0, TextFormatCode, arg)
166-
if err != nil {
167-
return err
168-
}
169-
} else {
170-
dt, ok := m.TypeForValue(arg)
171-
if !ok {
172-
var tv pgtype.TextValuer
173-
if tv, ok = arg.(pgtype.TextValuer); ok {
174-
t, err := tv.TextValue()
175-
if err != nil {
176-
return err
177-
}
178-
179-
dt, ok = m.TypeForOID(pgtype.TextOID)
180-
if ok {
181-
arg = t
182-
}
183-
}
184-
}
185-
if !ok {
186-
var dv driver.Valuer
187-
if dv, ok = arg.(driver.Valuer); ok {
188-
v, err := dv.Value()
189-
if err != nil {
190-
return err
191-
}
192-
dt, ok = m.TypeForValue(v)
193-
if ok {
194-
arg = v
195-
}
196-
}
197-
}
198-
if !ok {
199-
var str fmt.Stringer
200-
if str, ok = arg.(fmt.Stringer); ok {
201-
dt, ok = m.TypeForOID(pgtype.TextOID)
202-
if ok {
203-
arg = str.String()
204-
}
205-
}
206-
}
207-
if !ok {
208-
return &unknownArgumentTypeQueryExecModeExecError{arg: arg}
209-
}
210-
err := eqb.appendParam(m, dt.OID, TextFormatCode, arg)
211-
if err != nil {
212-
return err
213-
}
214-
}
215-
}
216-
217-
return nil
218-
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/jackc/pgx/v5
22

3-
go 1.19
3+
go 1.20
44

55
require (
66
github.com/jackc/pgpassfile v1.0.0

internal/anynil/anynil.go

Lines changed: 0 additions & 36 deletions
This file was deleted.

pgtype/array_codec.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"reflect"
88

9-
"github.com/jackc/pgx/v5/internal/anynil"
109
"github.com/jackc/pgx/v5/internal/pgio"
1110
)
1211

@@ -230,7 +229,7 @@ func (c *ArrayCodec) PlanScan(m *Map, oid uint32, format int16, target any) Scan
230229

231230
// target / arrayScanner might be a pointer to a nil. If it is create one so we can call ScanIndexType to plan the
232231
// scan of the elements.
233-
if anynil.Is(target) {
232+
if isNil, _ := isNilDriverValuer(target); isNil {
234233
arrayScanner = reflect.New(reflect.TypeOf(target).Elem()).Interface().(ArraySetter)
235234
}
236235

pgtype/doc.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,16 @@ Compatibility with database/sql
139139
pgtype also includes support for custom types implementing the database/sql.Scanner and database/sql/driver.Valuer
140140
interfaces.
141141
142+
Encoding Typed Nils
143+
144+
pgtype encodes untyped and typed nils (e.g. nil and []byte(nil)) to the SQL NULL value without going through the Codec
145+
system. This means that Codecs and other encoding logic do not have to handle nil or *T(nil).
146+
147+
However, database/sql compatibility requires Value to be called on T(nil) when T implements driver.Valuer. Therefore,
148+
driver.Valuer values are only considered NULL when *T(nil) where driver.Valuer is implemented on T not on *T. See
149+
https://github.com/golang/go/issues/8415 and
150+
https://github.com/golang/go/commit/0ce1d79a6a771f7449ec493b993ed2a720917870.
151+
142152
Child Records
143153
144154
pgtype's support for arrays and composite records can be used to load records and their children in a single query. See

pgtype/pgtype.go

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,8 +1912,17 @@ func newEncodeError(value any, m *Map, oid uint32, formatCode int16, err error)
19121912
// (nil, nil). The caller of Encode is responsible for writing the correct NULL value or the length of the data
19131913
// written.
19141914
func (m *Map) Encode(oid uint32, formatCode int16, value any, buf []byte) (newBuf []byte, err error) {
1915-
if value == nil {
1916-
return nil, nil
1915+
if isNil, callNilDriverValuer := isNilDriverValuer(value); isNil {
1916+
if callNilDriverValuer {
1917+
newBuf, err = (&encodePlanDriverValuer{m: m, oid: oid, formatCode: formatCode}).Encode(value, buf)
1918+
if err != nil {
1919+
return nil, newEncodeError(value, m, oid, formatCode, err)
1920+
}
1921+
1922+
return newBuf, nil
1923+
} else {
1924+
return nil, nil
1925+
}
19171926
}
19181927

19191928
plan := m.PlanEncode(oid, formatCode, value)
@@ -1968,3 +1977,55 @@ func (w *sqlScannerWrapper) Scan(src any) error {
19681977

19691978
return w.m.Scan(t.OID, TextFormatCode, bufSrc, w.v)
19701979
}
1980+
1981+
// canBeNil returns true if value can be nil.
1982+
func canBeNil(value any) bool {
1983+
refVal := reflect.ValueOf(value)
1984+
kind := refVal.Kind()
1985+
switch kind {
1986+
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
1987+
return true
1988+
default:
1989+
return false
1990+
}
1991+
}
1992+
1993+
// valuerReflectType is a reflect.Type for driver.Valuer. It has confusing syntax because reflect.TypeOf returns nil
1994+
// when it's argument is a nil interface value. So we use a pointer to the interface and call Elem to get the actual
1995+
// type. Yuck.
1996+
//
1997+
// This can be simplified in Go 1.22 with reflect.TypeFor.
1998+
//
1999+
// var valuerReflectType = reflect.TypeFor[driver.Valuer]()
2000+
var valuerReflectType = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
2001+
2002+
// isNilDriverValuer returns true if value is any type of nil unless it implements driver.Valuer. *T is not considered to implement
2003+
// driver.Valuer if it is only implemented by T.
2004+
func isNilDriverValuer(value any) (isNil bool, callNilDriverValuer bool) {
2005+
if value == nil {
2006+
return true, false
2007+
}
2008+
2009+
refVal := reflect.ValueOf(value)
2010+
kind := refVal.Kind()
2011+
switch kind {
2012+
case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
2013+
if !refVal.IsNil() {
2014+
return false, false
2015+
}
2016+
2017+
if _, ok := value.(driver.Valuer); ok {
2018+
if kind == reflect.Ptr {
2019+
// The type assertion will succeed if driver.Valuer is implemented on T or *T. Check if it is implemented on *T
2020+
// by checking if it is not implemented on *T.
2021+
return true, !refVal.Type().Elem().Implements(valuerReflectType)
2022+
} else {
2023+
return true, true
2024+
}
2025+
}
2026+
2027+
return true, false
2028+
default:
2029+
return false, false
2030+
}
2031+
}

0 commit comments

Comments
 (0)