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 pborman/uuid#59.
This was introduced in #2567
To Reproduce
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.
Describe the bug
If a type implements the
driver.Valuerinterface 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 pborman/uuid#59.This was introduced in #2567
To Reproduce
Please run your example with the race detector enabled. For example,
go run -race main.goorgo 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
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.