diff --git a/conn.go b/conn.go index 21bd8f1b..4085722c 100644 --- a/conn.go +++ b/conn.go @@ -292,6 +292,8 @@ func (c *Conn) connect(config ConnConfig, network, address string, tlsConfig *tl Int4Oid: &pgtype.Int4{}, Int8ArrayOid: &pgtype.Int8Array{}, Int8Oid: &pgtype.Int8{}, + JsonbOid: &pgtype.Jsonb{}, + JsonOid: &pgtype.Json{}, NameOid: &pgtype.Name{}, OidOid: &pgtype.Oid{}, TextArrayOid: &pgtype.TextArray{}, diff --git a/pgtype/json.go b/pgtype/json.go new file mode 100644 index 00000000..8a258ea4 --- /dev/null +++ b/pgtype/json.go @@ -0,0 +1,102 @@ +package pgtype + +import ( + "encoding/json" + "io" +) + +type Json struct { + Bytes []byte + Status Status +} + +func (dst *Json) ConvertFrom(src interface{}) error { + switch value := src.(type) { + case string: + *dst = Json{Bytes: []byte(value), Status: Present} + case *string: + if value == nil { + *dst = Json{Status: Null} + } else { + *dst = Json{Bytes: []byte(*value), Status: Present} + } + case []byte: + if value == nil { + *dst = Json{Status: Null} + } else { + *dst = Json{Bytes: value, Status: Present} + } + default: + buf, err := json.Marshal(value) + if err != nil { + return err + } + *dst = Json{Bytes: buf, Status: Present} + } + + return nil +} + +func (src *Json) AssignTo(dst interface{}) error { + switch v := dst.(type) { + case *string: + if src.Status != Present { + v = nil + } else { + *v = string(src.Bytes) + } + case **string: + *v = new(string) + return src.AssignTo(*v) + case *[]byte: + if src.Status != Present { + *v = nil + } else { + buf := make([]byte, len(src.Bytes)) + copy(buf, src.Bytes) + *v = buf + } + default: + data := src.Bytes + if data == nil || src.Status != Present { + data = []byte("null") + } + + return json.Unmarshal(data, dst) + } + + return nil +} + +func (dst *Json) DecodeText(src []byte) error { + if src == nil { + *dst = Json{Status: Null} + return nil + } + + buf := make([]byte, len(src)) + copy(buf, src) + + *dst = Json{Bytes: buf, Status: Present} + return nil +} + +func (dst *Json) DecodeBinary(src []byte) error { + return dst.DecodeText(src) +} + +func (src Json) EncodeText(w io.Writer) (bool, error) { + switch src.Status { + case Null: + return true, nil + case Undefined: + return false, errUndefined + } + + _, err := w.Write(src.Bytes) + return false, err +} + +func (src Json) EncodeBinary(w io.Writer) (bool, error) { + return src.EncodeText(w) +} diff --git a/pgtype/json_test.go b/pgtype/json_test.go new file mode 100644 index 00000000..87770f31 --- /dev/null +++ b/pgtype/json_test.go @@ -0,0 +1,135 @@ +package pgtype_test + +import ( + "bytes" + "reflect" + "testing" + + "github.com/jackc/pgx/pgtype" +) + +func TestJsonTranscode(t *testing.T) { + testSuccessfulTranscode(t, "json", []interface{}{ + pgtype.Json{Bytes: []byte("{}"), Status: pgtype.Present}, + pgtype.Json{Bytes: []byte("null"), Status: pgtype.Present}, + pgtype.Json{Bytes: []byte("42"), Status: pgtype.Present}, + pgtype.Json{Bytes: []byte(`"hello"`), Status: pgtype.Present}, + pgtype.Json{Status: pgtype.Null}, + }) +} + +func TestJsonConvertFrom(t *testing.T) { + successfulTests := []struct { + source interface{} + result pgtype.Json + }{ + {source: "{}", result: pgtype.Json{Bytes: []byte("{}"), Status: pgtype.Present}}, + {source: []byte("{}"), result: pgtype.Json{Bytes: []byte("{}"), Status: pgtype.Present}}, + {source: ([]byte)(nil), result: pgtype.Json{Status: pgtype.Null}}, + {source: (*string)(nil), result: pgtype.Json{Status: pgtype.Null}}, + {source: []int{1, 2, 3}, result: pgtype.Json{Bytes: []byte("[1,2,3]"), Status: pgtype.Present}}, + {source: map[string]interface{}{"foo": "bar"}, result: pgtype.Json{Bytes: []byte(`{"foo":"bar"}`), Status: pgtype.Present}}, + } + + for i, tt := range successfulTests { + var d pgtype.Json + err := d.ConvertFrom(tt.source) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if !reflect.DeepEqual(d, tt.result) { + t.Errorf("%d: expected %v to convert to %v, but it was %v", i, tt.source, tt.result, d) + } + } +} + +func TestJsonAssignTo(t *testing.T) { + var s string + var ps *string + var b []byte + + rawStringTests := []struct { + src pgtype.Json + dst *string + expected string + }{ + {src: pgtype.Json{Bytes: []byte("{}"), Status: pgtype.Present}, dst: &s, expected: "{}"}, + } + + for i, tt := range rawStringTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if *tt.dst != tt.expected { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, *tt.dst) + } + } + + rawBytesTests := []struct { + src pgtype.Json + dst *[]byte + expected []byte + }{ + {src: pgtype.Json{Bytes: []byte("{}"), Status: pgtype.Present}, dst: &b, expected: []byte("{}")}, + {src: pgtype.Json{Status: pgtype.Null}, dst: &b, expected: (([]byte)(nil))}, + } + + for i, tt := range rawBytesTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if bytes.Compare(tt.expected, *tt.dst) != 0 { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, *tt.dst) + } + } + + var mapDst map[string]interface{} + type structDst struct { + Name string `json:"name"` + Age int `json:"age"` + } + var strDst structDst + + unmarshalTests := []struct { + src pgtype.Json + dst interface{} + expected interface{} + }{ + {src: pgtype.Json{Bytes: []byte(`{"foo":"bar"}`), Status: pgtype.Present}, dst: &mapDst, expected: map[string]interface{}{"foo": "bar"}}, + {src: pgtype.Json{Bytes: []byte(`{"name":"John","age":42}`), Status: pgtype.Present}, dst: &strDst, expected: structDst{Name: "John", Age: 42}}, + } + for i, tt := range unmarshalTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if dst := reflect.ValueOf(tt.dst).Elem().Interface(); !reflect.DeepEqual(dst, tt.expected) { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, dst) + } + } + + pointerAllocTests := []struct { + src pgtype.Json + dst **string + expected *string + }{ + {src: pgtype.Json{Status: pgtype.Null}, dst: &ps, expected: ((*string)(nil))}, + } + + for i, tt := range pointerAllocTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if *tt.dst == tt.expected { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, *tt.dst) + } + } +} diff --git a/pgtype/jsonb.go b/pgtype/jsonb.go new file mode 100644 index 00000000..0739a468 --- /dev/null +++ b/pgtype/jsonb.go @@ -0,0 +1,64 @@ +package pgtype + +import ( + "fmt" + "io" +) + +type Jsonb Json + +func (dst *Jsonb) ConvertFrom(src interface{}) error { + return (*Json)(dst).ConvertFrom(src) +} + +func (src *Jsonb) AssignTo(dst interface{}) error { + return (*Json)(src).AssignTo(dst) +} + +func (dst *Jsonb) DecodeText(src []byte) error { + return (*Json)(dst).DecodeText(src) +} + +func (dst *Jsonb) DecodeBinary(src []byte) error { + if src == nil { + *dst = Jsonb{Status: Null} + return nil + } + + if len(src) == 0 { + return fmt.Errorf("jsonb too short") + } + + if src[0] != 1 { + return fmt.Errorf("unknown jsonb version number %d", src[0]) + } + src = src[1:] + + buf := make([]byte, len(src)) + copy(buf, src) + + *dst = Jsonb{Bytes: buf, Status: Present} + return nil + +} + +func (src Jsonb) EncodeText(w io.Writer) (bool, error) { + return (Json)(src).EncodeText(w) +} + +func (src Jsonb) EncodeBinary(w io.Writer) (bool, error) { + switch src.Status { + case Null: + return true, nil + case Undefined: + return false, errUndefined + } + + _, err := w.Write([]byte{1}) + if err != nil { + return false, err + } + + _, err = w.Write(src.Bytes) + return false, err +} diff --git a/pgtype/jsonb_test.go b/pgtype/jsonb_test.go new file mode 100644 index 00000000..e42931d5 --- /dev/null +++ b/pgtype/jsonb_test.go @@ -0,0 +1,135 @@ +package pgtype_test + +import ( + "bytes" + "reflect" + "testing" + + "github.com/jackc/pgx/pgtype" +) + +func TestJsonbTranscode(t *testing.T) { + testSuccessfulTranscode(t, "jsonb", []interface{}{ + pgtype.Jsonb{Bytes: []byte("{}"), Status: pgtype.Present}, + pgtype.Jsonb{Bytes: []byte("null"), Status: pgtype.Present}, + pgtype.Jsonb{Bytes: []byte("42"), Status: pgtype.Present}, + pgtype.Jsonb{Bytes: []byte(`"hello"`), Status: pgtype.Present}, + pgtype.Jsonb{Status: pgtype.Null}, + }) +} + +func TestJsonbConvertFrom(t *testing.T) { + successfulTests := []struct { + source interface{} + result pgtype.Jsonb + }{ + {source: "{}", result: pgtype.Jsonb{Bytes: []byte("{}"), Status: pgtype.Present}}, + {source: []byte("{}"), result: pgtype.Jsonb{Bytes: []byte("{}"), Status: pgtype.Present}}, + {source: ([]byte)(nil), result: pgtype.Jsonb{Status: pgtype.Null}}, + {source: (*string)(nil), result: pgtype.Jsonb{Status: pgtype.Null}}, + {source: []int{1, 2, 3}, result: pgtype.Jsonb{Bytes: []byte("[1,2,3]"), Status: pgtype.Present}}, + {source: map[string]interface{}{"foo": "bar"}, result: pgtype.Jsonb{Bytes: []byte(`{"foo":"bar"}`), Status: pgtype.Present}}, + } + + for i, tt := range successfulTests { + var d pgtype.Jsonb + err := d.ConvertFrom(tt.source) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if !reflect.DeepEqual(d, tt.result) { + t.Errorf("%d: expected %v to convert to %v, but it was %v", i, tt.source, tt.result, d) + } + } +} + +func TestJsonbAssignTo(t *testing.T) { + var s string + var ps *string + var b []byte + + rawStringTests := []struct { + src pgtype.Jsonb + dst *string + expected string + }{ + {src: pgtype.Jsonb{Bytes: []byte("{}"), Status: pgtype.Present}, dst: &s, expected: "{}"}, + } + + for i, tt := range rawStringTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if *tt.dst != tt.expected { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, *tt.dst) + } + } + + rawBytesTests := []struct { + src pgtype.Jsonb + dst *[]byte + expected []byte + }{ + {src: pgtype.Jsonb{Bytes: []byte("{}"), Status: pgtype.Present}, dst: &b, expected: []byte("{}")}, + {src: pgtype.Jsonb{Status: pgtype.Null}, dst: &b, expected: (([]byte)(nil))}, + } + + for i, tt := range rawBytesTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if bytes.Compare(tt.expected, *tt.dst) != 0 { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, *tt.dst) + } + } + + var mapDst map[string]interface{} + type structDst struct { + Name string `json:"name"` + Age int `json:"age"` + } + var strDst structDst + + unmarshalTests := []struct { + src pgtype.Jsonb + dst interface{} + expected interface{} + }{ + {src: pgtype.Jsonb{Bytes: []byte(`{"foo":"bar"}`), Status: pgtype.Present}, dst: &mapDst, expected: map[string]interface{}{"foo": "bar"}}, + {src: pgtype.Jsonb{Bytes: []byte(`{"name":"John","age":42}`), Status: pgtype.Present}, dst: &strDst, expected: structDst{Name: "John", Age: 42}}, + } + for i, tt := range unmarshalTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if dst := reflect.ValueOf(tt.dst).Elem().Interface(); !reflect.DeepEqual(dst, tt.expected) { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, dst) + } + } + + pointerAllocTests := []struct { + src pgtype.Jsonb + dst **string + expected *string + }{ + {src: pgtype.Jsonb{Status: pgtype.Null}, dst: &ps, expected: ((*string)(nil))}, + } + + for i, tt := range pointerAllocTests { + err := tt.src.AssignTo(tt.dst) + if err != nil { + t.Errorf("%d: %v", i, err) + } + + if *tt.dst == tt.expected { + t.Errorf("%d: expected %v to assign %v, but result was %v", i, tt.src, tt.expected, *tt.dst) + } + } +} diff --git a/query.go b/query.go index 2a5d88fc..bc7aeda4 100644 --- a/query.go +++ b/query.go @@ -263,17 +263,6 @@ func (rows *Rows) Scan(dest ...interface{}) (err error) { if err != nil { rows.Fatal(scanArgError{col: i, err: err}) } - } else if vr.Type().DataType == JsonOid { - // Because the argument passed to decodeJSON will escape the heap. - // This allows d to be stack allocated and only copied to the heap when - // we actually are decoding JSON. This saves one memory allocation per - // row. - d2 := d - decodeJSON(vr, &d2) - } else if vr.Type().DataType == JsonbOid { - // Same trick as above for getting stack allocation - d2 := d - decodeJSONB(vr, &d2) } else { if pgVal, present := rows.conn.oidPgtypeValues[vr.Type().DataType]; present { switch vr.Type().FormatCode { diff --git a/values.go b/values.go index 778284a4..bc9e5c64 100644 --- a/values.go +++ b/values.go @@ -772,13 +772,6 @@ func Encode(wbuf *WriteBuf, oid Oid, arg interface{}) error { return Encode(wbuf, oid, arg) } - if oid == JsonOid { - return encodeJSON(wbuf, oid, arg) - } - if oid == JsonbOid { - return encodeJSONB(wbuf, oid, arg) - } - if value, ok := wbuf.conn.oidPgtypeValues[oid]; ok { if converterFrom, ok := value.(pgtype.ConverterFrom); ok { err := converterFrom.ConvertFrom(arg)