Repository: jackc/pgx
Pull request #2142 (2024-10-05 15:17:11)

Add xid8 type
https://github.com/jackc/pgx/discussions/2137


Patch:
diff --git a/pgtype/pgtype.go b/pgtype/pgtype.go
index bdd9f05c..5ceb1c27 100644
--- a/pgtype/pgtype.go
+++ b/pgtype/pgtype.go
@@ -29,6 +29,7 @@ const (
 	XMLOID                 = 142
 	XMLArrayOID            = 143
 	JSONArrayOID           = 199
+	XID8ArrayOID           = 271
 	PointOID               = 600
 	LsegOID                = 601
 	PathOID                = 602
@@ -117,6 +118,7 @@ const (
 	TstzmultirangeOID      = 4534
 	DatemultirangeOID      = 4535
 	Int8multirangeOID      = 4536
+	XID8OID                = 5069
 	Int4multirangeArrayOID = 6150
 	NummultirangeArrayOID  = 6151
 	TsmultirangeArrayOID   = 6152
diff --git a/pgtype/pgtype_default.go b/pgtype/pgtype_default.go
index c8125731..9496cb97 100644
--- a/pgtype/pgtype_default.go
+++ b/pgtype/pgtype_default.go
@@ -90,6 +90,7 @@ func initDefaultMap() {
 	defaultMap.RegisterType(&Type{Name: "varbit", OID: VarbitOID, Codec: BitsCodec{}})
 	defaultMap.RegisterType(&Type{Name: "varchar", OID: VarcharOID, Codec: TextCodec{}})
 	defaultMap.RegisterType(&Type{Name: "xid", OID: XIDOID, Codec: Uint32Codec{}})
+	defaultMap.RegisterType(&Type{Name: "xid8", OID: XID8OID, Codec: Uint64Codec{}})
 	defaultMap.RegisterType(&Type{Name: "xml", OID: XMLOID, Codec: &XMLCodec{Marshal: xml.Marshal, Unmarshal: xml.Unmarshal}})
 
 	// Range types
@@ -155,6 +156,7 @@ func initDefaultMap() {
 	defaultMap.RegisterType(&Type{Name: "_varbit", OID: VarbitArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[VarbitOID]}})
 	defaultMap.RegisterType(&Type{Name: "_varchar", OID: VarcharArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[VarcharOID]}})
 	defaultMap.RegisterType(&Type{Name: "_xid", OID: XIDArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[XIDOID]}})
+	defaultMap.RegisterType(&Type{Name: "_xid8", OID: XID8ArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[XID8OID]}})
 	defaultMap.RegisterType(&Type{Name: "_xml", OID: XMLArrayOID, Codec: &ArrayCodec{ElementType: defaultMap.oidToType[XMLOID]}})
 
 	// Integer types that directly map to a PostgreSQL type
diff --git a/pgtype/uint64.go b/pgtype/uint64.go
new file mode 100644
index 00000000..dd2130eb
--- /dev/null
+++ b/pgtype/uint64.go
@@ -0,0 +1,322 @@
+package pgtype
+
+import (
+	"database/sql/driver"
+	"encoding/binary"
+	"fmt"
+	"math"
+	"strconv"
+
+	"github.com/jackc/pgx/v5/internal/pgio"
+)
+
+type Uint64Scanner interface {
+	ScanUint64(v Uint64) error
+}
+
+type Uint64Valuer interface {
+	Uint64Value() (Uint64, error)
+}
+
+// Uint64 is the core type that is used to represent PostgreSQL types such as XID8.
+type Uint64 struct {
+	Uint64 uint64
+	Valid  bool
+}
+
+func (n *Uint64) ScanUint64(v Uint64) error {
+	*n = v
+	return nil
+}
+
+func (n Uint64) Uint64Value() (Uint64, error) {
+	return n, nil
+}
+
+// Scan implements the database/sql Scanner interface.
+func (dst *Uint64) Scan(src any) error {
+	if src == nil {
+		*dst = Uint64{}
+		return nil
+	}
+
+	var n uint64
+
+	switch src := src.(type) {
+	case int64:
+		if src < 0 {
+			return fmt.Errorf("%d is less than the minimum value for Uint64", src)
+		}
+		n = uint64(src)
+	case string:
+		un, err := strconv.ParseUint(src, 10, 64)
+		if err != nil {
+			return err
+		}
+		n = un
+	default:
+		return fmt.Errorf("cannot scan %T", src)
+	}
+
+	*dst = Uint64{Uint64: n, Valid: true}
+
+	return nil
+}
+
+// Value implements the database/sql/driver Valuer interface.
+func (src Uint64) Value() (driver.Value, error) {
+	if !src.Valid {
+		return nil, nil
+	}
+
+	// If the value is greater than the maximum value for int64, return it as a string instead of losing data or returning
+	// an error.
+	if src.Uint64 > math.MaxInt64 {
+		return strconv.FormatUint(src.Uint64, 10), nil
+	}
+
+	return int64(src.Uint64), nil
+}
+
+type Uint64Codec struct{}
+
+func (Uint64Codec) FormatSupported(format int16) bool {
+	return format == TextFormatCode || format == BinaryFormatCode
+}
+
+func (Uint64Codec) PreferredFormat() int16 {
+	return BinaryFormatCode
+}
+
+func (Uint64Codec) PlanEncode(m *Map, oid uint32, format int16, value any) EncodePlan {
+	switch format {
+	case BinaryFormatCode:
+		switch value.(type) {
+		case uint64:
+			return encodePlanUint64CodecBinaryUint64{}
+		case Uint64Valuer:
+			return encodePlanUint64CodecBinaryUint64Valuer{}
+		case Int64Valuer:
+			return encodePlanUint64CodecBinaryInt64Valuer{}
+		}
+	case TextFormatCode:
+		switch value.(type) {
+		case uint64:
+			return encodePlanUint64CodecTextUint64{}
+		case Int64Valuer:
+			return encodePlanUint64CodecTextInt64Valuer{}
+		}
+	}
+
+	return nil
+}
+
+type encodePlanUint64CodecBinaryUint64 struct{}
+
+func (encodePlanUint64CodecBinaryUint64) Encode(value any, buf []byte) (newBuf []byte, err error) {
+	v := value.(uint64)
+	return pgio.AppendUint64(buf, v), nil
+}
+
+type encodePlanUint64CodecBinaryUint64Valuer struct{}
+
+func (encodePlanUint64CodecBinaryUint64Valuer) Encode(value any, buf []byte) (newBuf []byte, err error) {
+	v, err := value.(Uint64Valuer).Uint64Value()
+	if err != nil {
+		return nil, err
+	}
+
+	if !v.Valid {
+		return nil, nil
+	}
+
+	return pgio.AppendUint64(buf, v.Uint64), nil
+}
+
+type encodePlanUint64CodecBinaryInt64Valuer struct{}
+
+func (encodePlanUint64CodecBinaryInt64Valuer) Encode(value any, buf []byte) (newBuf []byte, err error) {
+	v, err := value.(Int64Valuer).Int64Value()
+	if err != nil {
+		return nil, err
+	}
+
+	if !v.Valid {
+		return nil, nil
+	}
+
+	if v.Int64 < 0 {
+		return nil, fmt.Errorf("%d is less than minimum value for uint64", v.Int64)
+	}
+
+	return pgio.AppendUint64(buf, uint64(v.Int64)), nil
+}
+
+type encodePlanUint64CodecTextUint64 struct{}
+
+func (encodePlanUint64CodecTextUint64) Encode(value any, buf []byte) (newBuf []byte, err error) {
+	v := value.(uint64)
+	return append(buf, strconv.FormatUint(uint64(v), 10)...), nil
+}
+
+type encodePlanUint64CodecTextUint64Valuer struct{}
+
+func (encodePlanUint64CodecTextUint64Valuer) Encode(value any, buf []byte) (newBuf []byte, err error) {
+	v, err := value.(Uint64Valuer).Uint64Value()
+	if err != nil {
+		return nil, err
+	}
+
+	if !v.Valid {
+		return nil, nil
+	}
+
+	return append(buf, strconv.FormatUint(v.Uint64, 10)...), nil
+}
+
+type encodePlanUint64CodecTextInt64Valuer struct{}
+
+func (encodePlanUint64CodecTextInt64Valuer) Encode(value any, buf []byte) (newBuf []byte, err error) {
+	v, err := value.(Int64Valuer).Int64Value()
+	if err != nil {
+		return nil, err
+	}
+
+	if !v.Valid {
+		return nil, nil
+	}
+
+	if v.Int64 < 0 {
+		return nil, fmt.Errorf("%d is less than minimum value for uint64", v.Int64)
+	}
+
+	return append(buf, strconv.FormatInt(v.Int64, 10)...), nil
+}
+
+func (Uint64Codec) PlanScan(m *Map, oid uint32, format int16, target any) ScanPlan {
+
+	switch format {
+	case BinaryFormatCode:
+		switch target.(type) {
+		case *uint64:
+			return scanPlanBinaryUint64ToUint64{}
+		case Uint64Scanner:
+			return scanPlanBinaryUint64ToUint64Scanner{}
+		case TextScanner:
+			return scanPlanBinaryUint64ToTextScanner{}
+		}
+	case TextFormatCode:
+		switch target.(type) {
+		case *uint64:
+			return scanPlanTextAnyToUint64{}
+		case Uint64Scanner:
+			return scanPlanTextAnyToUint64Scanner{}
+		}
+	}
+
+	return nil
+}
+
+func (c Uint64Codec) DecodeDatabaseSQLValue(m *Map, oid uint32, format int16, src []byte) (driver.Value, error) {
+	if src == nil {
+		return nil, nil
+	}
+
+	var n uint64
+	err := codecScan(c, m, oid, format, src, &n)
+	if err != nil {
+		return nil, err
+	}
+	return int64(n), nil
+}
+
+func (c Uint64Codec) DecodeValue(m *Map, oid uint32, format int16, src []byte) (any, error) {
+	if src == nil {
+		return nil, nil
+	}
+
+	var n uint64
+	err := codecScan(c, m, oid, format, src, &n)
+	if err != nil {
+		return nil, err
+	}
+	return n, nil
+}
+
+type scanPlanBinaryUint64ToUint64 struct{}
+
+func (scanPlanBinaryUint64ToUint64) Scan(src []byte, dst any) error {
+	if src == nil {
+		return fmt.Errorf("cannot scan NULL into %T", dst)
+	}
+
+	if len(src) != 8 {
+		return fmt.Errorf("invalid length for uint64: %v", len(src))
+	}
+
+	p := (dst).(*uint64)
+	*p = binary.BigEndian.Uint64(src)
+
+	return nil
+}
+
+type scanPlanBinaryUint64ToUint64Scanner struct{}
+
+func (scanPlanBinaryUint64ToUint64Scanner) Scan(src []byte, dst any) error {
+	s, ok := (dst).(Uint64Scanner)
+	if !ok {
+		return ErrScanTargetTypeChanged
+	}
+
+	if src == nil {
+		return s.ScanUint64(Uint64{})
+	}
+
+	if len(src) != 8 {
+		return fmt.Errorf("invalid length for uint64: %v", len(src))
+	}
+
+	n := binary.BigEndian.Uint64(src)
+
+	return s.ScanUint64(Uint64{Uint64: n, Valid: true})
+}
+
+type scanPlanBinaryUint64ToTextScanner struct{}
+
+func (scanPlanBinaryUint64ToTextScanner) Scan(src []byte, dst any) error {
+	s, ok := (dst).(TextScanner)
+	if !ok {
+		return ErrScanTargetTypeChanged
+	}
+
+	if src == nil {
+		return s.ScanText(Text{})
+	}
+
+	if len(src) != 8 {
+		return fmt.Errorf("invalid length for uint64: %v", len(src))
+	}
+
+	n := uint64(binary.BigEndian.Uint64(src))
+	return s.ScanText(Text{String: strconv.FormatUint(n, 10), Valid: true})
+}
+
+type scanPlanTextAnyToUint64Scanner struct{}
+
+func (scanPlanTextAnyToUint64Scanner) Scan(src []byte, dst any) error {
+	s, ok := (dst).(Uint64Scanner)
+	if !ok {
+		return ErrScanTargetTypeChanged
+	}
+
+	if src == nil {
+		return s.ScanUint64(Uint64{})
+	}
+
+	n, err := strconv.ParseUint(string(src), 10, 64)
+	if err != nil {
+		return err
+	}
+
+	return s.ScanUint64(Uint64{Uint64: n, Valid: true})
+}


Test patch:
diff --git a/pgtype/uint64_test.go b/pgtype/uint64_test.go
new file mode 100644
index 00000000..33c2622d
--- /dev/null
+++ b/pgtype/uint64_test.go
@@ -0,0 +1,30 @@
+package pgtype_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/jackc/pgx/v5/pgtype"
+	"github.com/jackc/pgx/v5/pgxtest"
+)
+
+func TestUint64Codec(t *testing.T) {
+	skipCockroachDB(t, "Server does not support xid8 (https://github.com/cockroachdb/cockroach/issues/36815)")
+	skipPostgreSQLVersionLessThan(t, 13)
+
+	pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, pgxtest.KnownOIDQueryExecModes, "xid8", []pgxtest.ValueRoundTripTest{
+		{
+			pgtype.Uint64{Uint64: 1 << 36, Valid: true},
+			new(pgtype.Uint64),
+			isExpectedEq(pgtype.Uint64{Uint64: 1 << 36, Valid: true}),
+		},
+		{pgtype.Uint64{}, new(pgtype.Uint64), isExpectedEq(pgtype.Uint64{})},
+		{nil, new(pgtype.Uint64), isExpectedEq(pgtype.Uint64{})},
+		{
+			uint64(1 << 36),
+			new(uint64),
+			isExpectedEq(uint64(1 << 36)),
+		},
+		{"1147", new(string), isExpectedEq("1147")},
+	})
+}