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

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