driver.Valuer existence takes precedence over codec for nil values
- Dominant language
- Go
- Stars
- 14.3k
- Forks
- 1.1k
- Avg merge
- 6d 9h
- Merged PRs (30d)
- 11
Description
**Describe the bug**
If a type implements the `driver.Valuer` interface and a nil value is encountered in an array the type will always be encoded using the driver valuer interface. If the type has a Codec it seems like calling driver.Valuer is logically wrong. This becomes relevant if the type's valuer is incorrect for nil: see https://github.com/pborman/uuid/pull/59.
This was introduced in https://github.com/jackc/pgx/pull/2567
**To Reproduce**
```go
package main
import (
"context"
"database/sql/driver"
"errors"
"fmt"
"log"
"os"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/pborman/uuid"
)
type Result struct {
ID int64
Area pgtype.Box
}
func main() {
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer conn.Close(context.Background())
m := conn.TypeMap()
m.RegisterType(&pgtype.Type{
Name: "uuid",
OID: pgtype.UUIDOID,
Codec: UUIDCodec{},
})
_, err = conn.Exec(context.Background(),
`CREATE OR REPLACE FUNCTION do_something( in_value UUID[] )
RETURNS TABLE (out_value UUID)
AS
$$
BEGIN
RETURN QUERY SELECT * FROM unnest(in_value);
END;
$$
LANGUAGE plpgsql SECURITY DEFINER IMMUTABLE;`)
if err != nil {
log.Fatal(err)
}
input := []uuid.UUID{uuid.NewRandom(), nil, uuid.NewRandom()}
_, err = conn.Exec(context.Background(), "SELECT * FROM do_something( $1 )", input)
if err != nil {
log.Fatal(err)
}
}
type UUIDCodec struct {
pgtype.UUIDCodec
}
func (c UUIDCodec) DecodeDatabaseSQLValue(tm *pgtype.Map, oid uint32, format int16, src []byte) (driver.Value, error) {
if src == nil {
return nil, nil
}
var target uuid.UUID
scanPlan := tm.PlanScan(oid, format, &target)
if scanPlan == nil {
return nil, errors.New("PlanScan did not find a plan")
}
err := scanPlan.Scan(src, &target)
if err != nil {
return nil, err
}
return target.Value()
}
func (c UUIDCodec) PlanEncode(m *pgtype.Map, oid uint32, format int16, value any) pgtype.EncodePlan {
switch value.(type) {
default:
// If this is not the pborman uuid, then fallback to the pgtype implementation
return c.UUIDCodec.PlanEncode(m, oid, format, value)
case uuid.UUID, *uuid.UUID:
switch format {
case pgtype.BinaryFormatCode:
return binaryEncodePlan{}
case pgtype.TextFormatCode:
return textEncodePlan{}
}
}
return nil
}
type binaryEncodePlan struct{}
func (plan binaryEncodePlan) Encode(value any, buf []byte) (newBuf []byte, err error) {
switch src := value.(type) {
case *uuid.UUID:
if src == nil {
return nil, nil
}
return plan.encodeUUID(*src, buf)
case uuid.UUID:
return plan.encodeUUID(src, buf)
default:
return nil, fmt.Errorf("cannot encode %T as uuid", src)
}
}
func (binaryEncodePlan) encodeUUID(value uuid.UUID, buf []byte) (newBuf []byte, err error) {
if value == nil {
return nil, nil
}
return append(buf, value[:]...), nil
}
type textEncodePlan struct{}
func (plan textEncodePlan) Encode(value any, buf []byte) (newBuf []byte, err error) {
switch src := value.(type) {
case *uuid.UUID:
if src == nil {
return nil, nil
}
return plan.encodeUUID(*src, buf)
case uuid.UUID:
return plan.encodeUUID(src, buf)
default:
return nil, fmt.Errorf("cannot encode %T as uuid", src)
}
}
func (textEncodePlan) encodeUUID(value uuid.UUID, buf []byte) (newBuf []byte, err error) {
if value == nil {
return nil, nil
}
return append(buf, value.String()...), nil
}
```
Please run your example with the race detector enabled. For example, `go run -race main.go` or `go test -race`.
**Expected behavior**
It's expected that the Exec call succeeds and sends the uuid array to the database.
**Actual behavior**
`2026/07/22 16:56:59 ERROR: invalid input syntax for type uuid: "" (SQLSTATE 22P02)`
As mentioned earlier this is because the uuid returns an empty string from driver.Valuer when nil.
**Version**
- Go: $ go version -> go version go1.26.2 linux/amd64
- PostgreSQL: $ psql --no-psqlrc --tuples-only -c 'select version()' -> PostgreSQL 17.10 (Debian 17.10-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
- pgx: $ grep 'github.com/jackc/pgx/v[0-9]' go.mod -> require github.com/jackc/pgx/v5 v5.10.0
**Additional context**
When looking, I saw that composites also have driver.Valuer take precedence over codecs too. My thought is that we should only use the driver.Valuer if there is no codec. I'm not entire sure the best way to go about deciding that though or if there's a reason that valuer should always be used first.
Contributor guide
Research direction
Start with the pgtype Map.PlanEncode path used by array and composite encoding, and trace how driver.Valuer is selected relative to a registered Codec. Run the provided main.go example with go run -race or go test -race; done means nil array elements use the codec when one is registered, the UUID array is accepted by PostgreSQL, and composite behavior is understood.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, postgresql
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100