pgtype icon indicating copy to clipboard operation
pgtype copied to clipboard

Constructor functions

Open TimothyGu opened this issue 4 years ago • 6 comments

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.

TimothyGu avatar Dec 06 '20 22:12 TimothyGu

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.

jackc avatar Dec 12 '20 14:12 jackc

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

asdine avatar Dec 18 '20 07:12 asdine

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.)

TimothyGu avatar Dec 18 '20 07:12 TimothyGu

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.

jackc avatar Dec 21 '20 14:12 jackc

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 CompositeTypeField struct for each field of the MyType struct using the predefined OIDs
  • create a ValueTranscoder for each field of the MyType struct
  • use NewCompositeTypeValues to create the CompositeType struct

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 of value and create a respective ValueTranscoder (a generalized version of a constructor suggested above), maybe only for basic types

  • func NewCompositeTypeFromStruct(value interface{}) (*CompositeType, error) - which would inspect the fields of value, 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 the ValueTranscoder's with the value's field values, but leave them empty

  • func (*CompositeType) Deserialize(value interface{}) error - which would inspect the CompositeType, try and find respective fields in value and write to them (value must be a pointer), or return an error if some fields of value are not present in the CompositeType

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.

tigrkoshka avatar Apr 30 '22 04:04 tigrkoshka

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.

jackc avatar Apr 30 '22 13:04 jackc