Skip to content

driver.Valuer existence takes precedence over codec for nil values #2611

Description

@jaltavilla

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.

Activity

  1. jackc commented on Jul 31, 2026

    @jackc
    Owner

    This is documented behavior that Codecs do not need to handle nils. This would change that. I think I would see this case as a bug in github.com/pborman/uuid.

    pgx/pgtype/doc.go

    Lines 145 to 153 in 39feb8f

    Encoding Typed Nils
    pgtype encodes untyped and typed nils (e.g. nil and []byte(nil)) to the SQL NULL value without going through the [Codec]
    system. This means that [Codec]s and other encoding logic do not have to handle nil or *T(nil).
    However, [database/sql] compatibility requires Value to be called on T(nil) when T implements [database/sql/driver.Valuer]. Therefore,
    [database/sql/driver.Valuer] values are only considered NULL when *T(nil) where [database/sql/driver.Valuer] is implemented on T not on *T. See
    https://github.com/golang/go/issues/8415 and
    https://github.com/golang/go/commit/0ce1d79a6a771f7449ec493b993ed2a720917870.

  2. jaltavilla commented on Aug 3, 2026

    @jaltavilla
    ContributorAuthor

    Thanks, I missed that in the documentation. This was surprising to me because the pgx interface seems to override the database/sql interfaces otherwise, including when scanning. It's a bit unfortunate because there is no way to make a type not implement an interface it satisfies.

    In the case of uuid, it might eventually become moot since go 1.27 introduces a uuid type to the standard library that doesn't implement driver.Valuer or sql.Scanner. 🤷‍♀️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions