pgtype
pgtype copied to clipboard
Constructor functions
Right now, many types have a "Set" method that allows conversion from a plain Go type to a pgtype. However, in practice it could be awkward to use. It would be nice to have constructor functions for each type, so that:
ts := pgtype.Timestamptz{}
ts.Set(t)
return myStruct{
ts: ts,
}
could be replaced by just
return myStruct{
ts: pgtype.NewTimestamptz(t),
}
This is not without precedent. Protobuf, for instance, has timestamppb.New and friends that allows this exact use case.
That would be more convenient but there is one problem. The Set method accepts interface{} and returns an error. This is so it can convert between compatible types such as different sizes of integers.
A constructor function would presumably need to do the same -- which would mean it also has to return an error -- which would mean it couldn't be used as a field value.
A constructor function would presumably need to do the same -- which would mean it also has to return an error -- which would mean it couldn't be used as a field value.
It doesn't have to, a constructor could expect the most common Go type for that SQL type.
pgtype.NewInt8Array([]int64{1, 2, 3})
With lib/pq for example, it is very simple to do the following:
db.Exec(
"...",
pq.Int64Array([]int64{1, 2, 3}),
pq.StringArray([]string{"foo", "bar"}),
)
This doesn't even use reflection because pq.Int64Array is defined like this:
type Int64Array []int64
pgtype could provide something similar by defining this kind of constructor:
func NewInt64Array(v []int64) *Int8Array {
...
}
I think the fact that these constructors don't support all types supported by Set shouldn't be a blocker, because their usefulness outweighs their lack of flexibility. And if someone wants to use another type, they can still use Set
What if we make the constructors stricter than Set? In the case of NewTimestamptz, we can choose to only accept an interface{} with concrete types nil, time.Time, *time.Time, or InfinityModifier; if anything else is passed, panic. This would take care of most cases the constructors would have been useful in the first place.
In the case of Int8Array, I think we could get away with being even stricter by only accepting []int64 or []*int64. True, Int8Array.Set allows a lot more than []int64 with various things like []*uint64, []int8, etc. But Go is pretty unforgiving when it comes to convenience of array operations (no type-safe slice.map()), so the programmer would already have to get used to it by using the language anyway. It also avoids tricky questions like "what happens to 232 when we are putting it into an Int4Array," and leave the users to deal with them instead in whatever way they find useful.
(I left open the possibility of allowing []*int64, but I could easily be swayed to disallow even that, or to have a separate function that converts []*int64 to Int8Array.)
I try to follow the rule that libraries shouldn't panic unless it is an obvious exception like a MustDoAThing variant of DoAThing. I guess the constructor could be prefixed with Must or Require.
And maybe strict, strongly typed constructor functions that can't fail might be reasonable. e.g. NewTimestamptzFromTime.
Though I do worry about adding too much new stuff to this library. It already adds a maybe a MB to every compiled binary using pgx. Over the long term I'd like to make the library smaller and simpler.
Hi! Saw this issue while searching for a convenient way to declare a composite type. In this regard, it would be very nice to be able to do something like this:
assume we have a native Go type
type MyType struct {
myInt int `db:"my_int"`
myString string `db:"my_string"`
myTimestampWithTZ time.Time `db:"my_timestamp_with_tz"`
myBool bool `db:"my_bool"`
}
and a respective type created in db:
CREATE TYPE my_type AS
(
"my_int" INTEGER,
"my_string" TEXT,
"my_timestamp_with_tz" TIMESTAMPTZ,
"my_bool" BOOLEAN
);
Right now, if I want to create a new CompositeType struct filled with values of a MyType struct (e.g. to perform an INSERT), I have to:
- create a
CompositeTypeFieldstruct for each field of theMyTypestruct using the predefined OIDs - create a
ValueTranscoderfor each field of theMyTypestruct - use
NewCompositeTypeValuesto create theCompositeTypestruct
And if I want to perform a SELECT, i have to Scan into a newly created CompositeType struct and then manually perform Get() getting the map[string]interface{} of its fields, then search for each field and construct a MyType struct
It would be nice if there were functions like
-
func NewValueTranscoder(value interface{}) (ValueTranscoder, error)- which would use reflect to determine the type ofvalueand create a respectiveValueTranscoder(a generalized version of a constructor suggested above), maybe only for basic types -
func NewCompositeTypeFromStruct(value interface{}) (*CompositeType, error)- which would inspect the fields ofvalue, recurse on structs and apply the constructor above to primitive type fields -
func NewEmptyCompositeTypeLikeStruct(value interface{}) (*CompositeType, error)- which would do the same as the previous functions except taht it would not fill theValueTranscoder's with thevalue's field values, but leave them empty -
func (*CompositeType) Deserialize(value interface{}) error- which would inspect theCompositeType, try and find respective fields invalueand write to them (valuemust be a pointer), or return an error if some fields ofvalueare not present in theCompositeType
Also, the same applies to ArrayType.
I have such functions right now in my own small library, so I would be happy to contribute if needed.
Composite types will be significantly simpler in pgx v5. See https://github.com/jackc/pgx/blob/v5-dev/pgtype/composite_test.go for some examples of the new style.