Skip to content
This repository was archived by the owner on Jul 12, 2025. It is now read-only.
This repository was archived by the owner on Jul 12, 2025. It is now read-only.

Register a custom type with sqlx #209

@cuprumtan

Description

@cuprumtan

I have a question about custom types registration. Can you please help me?

I use PostgreSQL with 64-bit XID patch. So this means that the regular pgtype.XID is no longer suitable because it is based on pguint32. I wrote custom pguint64 and XID64 types:

pguint64.go
package types

import (
	"database/sql/driver"
	"encoding/binary"
	"errors"
	"fmt"
	"strconv"

	"github.com/jackc/pgio"
	"github.com/jackc/pgtype"
)

// pguint64 is the core type that is used to implement PostgrePro type such as XID.
type pguint64 struct {
	Uint   uint64
	Status pgtype.Status
}

// Set converts from src to dst. Note that as pguint64 is not a general
// number type Set does not do automatic type conversion as other number
// types do.
func (dst *pguint64) Set(src interface{}) error {
	switch value := src.(type) {
	case int64:
		if value < 0 {
			return fmt.Errorf("%d is less than minimum value for pguint64", value)
		}
		*dst = pguint64{Uint: uint64(value), Status: pgtype.Present}
	case uint64:
		*dst = pguint64{Uint: value, Status: pgtype.Present}
	default:
		return fmt.Errorf("cannot convert %v to pguint64", value)
	}

	return nil
}

func (dst pguint64) Get() interface{} {
	switch dst.Status {
	case pgtype.Present:
		return dst.Uint
	case pgtype.Null:
		return nil
	default:
		return dst.Status
	}
}

// AssignTo assigns from src to dst. Note that as pguint64 is not a general number
// type AssignTo does not do automatic type conversion as other number types do.
func (src *pguint64) AssignTo(dst interface{}) error {
	switch v := dst.(type) {
	case *uint64:
		if src.Status == pgtype.Present {
			*v = src.Uint
		} else {
			return fmt.Errorf("cannot assign %v into %T", src, dst)
		}
	case **uint64:
		if src.Status == pgtype.Present {
			n := src.Uint
			*v = &n
		} else {
			*v = nil
		}
	}

	return nil
}

func (dst *pguint64) DecodeText(ci *pgtype.ConnInfo, src []byte) error {
	if src == nil {
		*dst = pguint64{Status: pgtype.Null}
		return nil
	}

	n, err := strconv.ParseUint(string(src), 10, 64)
	if err != nil {
		return err
	}

	*dst = pguint64{Uint: n, Status: pgtype.Present}
	return nil
}

func (dst *pguint64) DecodeBinary(ci *pgtype.ConnInfo, src []byte) error {
	if src == nil {
		*dst = pguint64{Status: pgtype.Null}
		return nil
	}

	if len(src) != 8 {
		return fmt.Errorf("invalid length: %v", len(src))
	}

	n := binary.BigEndian.Uint64(src)
	*dst = pguint64{Uint: n, Status: pgtype.Present}
	return nil
}

func (src pguint64) EncodeText(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
	switch src.Status {
	case pgtype.Null:
		return nil, nil
	case pgtype.Undefined:
		return nil, errors.New("cannot encode status undefined")
	}

	return append(buf, strconv.FormatUint(src.Uint, 10)...), nil
}

func (src pguint64) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
	switch src.Status {
	case pgtype.Null:
		return nil, nil
	case pgtype.Undefined:
		return nil, errors.New("cannot encode status undefined")
	}

	return pgio.AppendUint64(buf, src.Uint), nil
}

// Scan implements the database/sql Scanner interface.
func (dst *pguint64) Scan(src interface{}) error {
	if src == nil {
		*dst = pguint64{Status: pgtype.Null}
		return nil
	}

	switch src := src.(type) {
	case uint64:
		*dst = pguint64{Uint: src, Status: pgtype.Present}
		return nil
	case int64:
		*dst = pguint64{Uint: uint64(src), Status: pgtype.Present}
		return nil
	case string:
		return dst.DecodeText(nil, []byte(src))
	case []byte:
		srcCopy := make([]byte, len(src))
		copy(srcCopy, src)
		return dst.DecodeText(nil, srcCopy)
	}

	return fmt.Errorf("cannot scan %T", src)
}

// Value implements the database/sql/driver Valuer interface.
func (src pguint64) Value() (driver.Value, error) {
	switch src.Status {
	case pgtype.Present:
		return src.Uint, nil
	case pgtype.Null:
		return nil, nil
	default:
		return nil, errors.New("cannot encode status undefined")
	}
}
xid64.go
package types

import (
	"database/sql/driver"
	"github.com/jackc/pgtype"
)

// XID is PostgresPro's Transaction ID type.
//
// In later versions of PostgresPro, it is the type used for the backend_xid
// and backend_xmin columns of the pg_stat_activity system view.
//
// Also, when one does
//
//	select xmin, xmax, * from some_table;
//
// it is the data type of the xmin and xmax hidden system columns.
//
// It is currently implemented as an unsigned eight byte integer.
type XID64 pguint64

// Set converts from src to dst. Note that as XID64 is not a general
// number type Set does not do automatic type conversion as other number
// types do.
func (dst *XID64) Set(src interface{}) error {
	return (*pguint64)(dst).Set(src)
}

func (dst XID64) Get() interface{} {
	return (pguint64)(dst).Get()
}

// AssignTo assigns from src to dst. Note that as XID64 is not a general number
// type AssignTo does not do automatic type conversion as other number types do.
func (src *XID64) AssignTo(dst interface{}) error {
	return (*pguint64)(src).AssignTo(dst)
}

func (dst *XID64) DecodeText(ci *pgtype.ConnInfo, src []byte) error {
	return (*pguint64)(dst).DecodeText(ci, src)
}

func (dst *XID64) DecodeBinary(ci *pgtype.ConnInfo, src []byte) error {
	return (*pguint64)(dst).DecodeBinary(ci, src)
}

func (src XID64) EncodeText(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
	return (pguint64)(src).EncodeText(ci, buf)
}

func (src XID64) EncodeBinary(ci *pgtype.ConnInfo, buf []byte) ([]byte, error) {
	return (pguint64)(src).EncodeBinary(ci, buf)
}

// Scan implements the database/sql Scanner interface.
func (dst *XID64) Scan(src interface{}) error {
	return (*pguint64)(dst).Scan(src)
}

// Value implements the database/sql/driver Valuer interface.
func (src XID64) Value() (driver.Value, error) {
	return (pguint64)(src).Value()
}

I can easily register this new XID64 type with pgx and replace the regular one:

db, err := pgx.Connect(context.Background(), str)
if err != nil {
	return nil, fmt.Errorf("connect to database failed: %w", err)
}

db.ConnInfo().RegisterDataType(pgtype.DataType{Value: &types.XID64{}, Name: "xid", OID: pgtype.XIDOID})

But for me it is crucial to use sqlx instead of pgx. Is there any possibility to register type with sqlx?

To avoid misunderstandings, I will indicate the key point of importance of sqlx for me: I need to scan an unspecified number of fields into struct in different cases, so I need sqlx methods which contain destination as interface{}. pgx does not have such functionality.
pgx v5 can be used with scany which has methods for scanning into struct. But with pgtype v5 I cannot scan nullable fields with complex types like inet, xid, etc. and cannot differentiate that a field is NULL or just undefined (like Status in pgtype v4). Or am I wrong about this?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions