-
Notifications
You must be signed in to change notification settings - Fork 108
Register a custom type with sqlx #209
Description
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?