diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c7d9df..f22d8d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# 3.5.0 (June 29, 2019) + +## Features + +* Protocol support for PortalSuspended message (avivklas) +* Read OIDs for composite types on connection init (Nick Jones) + +## Fixes + +* Hstore can have empty keys (Josh Leverette) +* Fix -0 value for numeric type (David Hudson) +* Log error message on rows-close error (Euan Kemp) + +## Changes + +* Explicitly cast binary string to bytea in simple protocol (jinhua luo) +* Skip parse and sanitize simple query when no arguments (jinhua luo) + # 3.4.0 (May 3, 2019) ## Features diff --git a/README.md b/README.md index d6499ba4..b7051f65 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ if err != nil { } ``` +## v4 Coming Soon + +This is the current stable v3 version. v4 is currently is in prelease status. Consider using [v4](https://github.com/jackc/pgx/tree/v4) for new development or test upgrading existing applications. + ## Features pgx supports many additional features beyond what is available through database/sql. diff --git a/conn.go b/conn.go index cb24748c..0cf6c167 100644 --- a/conn.go +++ b/conn.go @@ -404,9 +404,11 @@ func initPostgresql(c *Conn) (*pgtype.ConnInfo, error) { from pg_type t left join pg_type base_type on t.typelem=base_type.oid left join pg_namespace nsp on t.typnamespace=nsp.oid +left join pg_class cls on t.typrelid=cls.oid where ( - t.typtype in('b', 'p', 'r', 'e') + t.typtype in('b', 'p', 'r', 'e', 'c') and (base_type.oid is null or base_type.typtype in('b', 'p', 'r')) + and (cls.oid is null or cls.relkind='c') )` ) diff --git a/internal/sanitize/sanitize.go b/internal/sanitize/sanitize.go index 53543b89..8939d797 100644 --- a/internal/sanitize/sanitize.go +++ b/internal/sanitize/sanitize.go @@ -87,7 +87,7 @@ func QuoteString(str string) string { } func QuoteBytes(buf []byte) string { - return `'\x` + hex.EncodeToString(buf) + "'" + return `'\x` + hex.EncodeToString(buf) + "'::bytea" } type sqlLexer struct { diff --git a/internal/sanitize/sanitize_test.go b/internal/sanitize/sanitize_test.go index 9597840e..f4337253 100644 --- a/internal/sanitize/sanitize_test.go +++ b/internal/sanitize/sanitize_test.go @@ -108,7 +108,7 @@ func TestQuerySanitize(t *testing.T) { { query: sanitize.Query{Parts: []sanitize.Part{"select ", 1}}, args: []interface{}{[]byte{0, 1, 2, 3, 255}}, - expected: `select '\x00010203ff'`, + expected: `select '\x00010203ff'::bytea`, }, { query: sanitize.Query{Parts: []sanitize.Part{"select ", 1}}, diff --git a/log/zapadapter/adapter.go b/log/zapadapter/adapter.go index 82263b6e..a5a377e6 100644 --- a/log/zapadapter/adapter.go +++ b/log/zapadapter/adapter.go @@ -19,7 +19,7 @@ func (pl *Logger) Log(level pgx.LogLevel, msg string, data map[string]interface{ fields := make([]zapcore.Field, len(data)) i := 0 for k, v := range data { - fields[i] = zap.Reflect(k, v) + fields[i] = zap.Any(k, v) i++ } diff --git a/pgmock/pgmock.go b/pgmock/pgmock.go index 4d15f7b8..d4ab0d13 100644 --- a/pgmock/pgmock.go +++ b/pgmock/pgmock.go @@ -211,9 +211,11 @@ func PgxInitSteps() []Step { from pg_type t left join pg_type base_type on t.typelem=base_type.oid left join pg_namespace nsp on t.typnamespace=nsp.oid +left join pg_class cls on t.typrelid=cls.oid where ( - t.typtype in('b', 'p', 'r', 'e') + t.typtype in('b', 'p', 'r', 'e', 'c') and (base_type.oid is null or base_type.typtype in('b', 'p', 'r')) + and (cls.oid is null or cls.relkind='c') )`, }), ExpectMessage(&pgproto3.Describe{ diff --git a/pgproto3/frontend.go b/pgproto3/frontend.go index 4c05fcc3..be2c01cd 100644 --- a/pgproto3/frontend.go +++ b/pgproto3/frontend.go @@ -23,7 +23,7 @@ type Frontend struct { copyInResponse CopyInResponse copyOutResponse CopyOutResponse copyDone CopyDone - copyFail CopyFail + copyFail CopyFail dataRow DataRow emptyQueryResponse EmptyQueryResponse errorResponse ErrorResponse @@ -36,6 +36,7 @@ type Frontend struct { parseComplete ParseComplete readyForQuery ReadyForQuery rowDescription RowDescription + portalSuspended PortalSuspended bodyLen int msgType byte @@ -76,8 +77,8 @@ func (b *Frontend) Receive() (BackendMessage, error) { msg = &b.notificationResponse case 'c': msg = &b.copyDone - case 'f': - msg = &b.copyFail + case 'f': + msg = &b.copyFail case 'C': msg = &b.commandComplete case 'd': @@ -112,6 +113,8 @@ func (b *Frontend) Receive() (BackendMessage, error) { msg = &b.copyBothResponse case 'Z': msg = &b.readyForQuery + case 's': + msg = &b.portalSuspended default: return nil, errors.Errorf("unknown message type: %c", b.msgType) } diff --git a/pgproto3/portal_suspended.go b/pgproto3/portal_suspended.go new file mode 100644 index 00000000..dc81b027 --- /dev/null +++ b/pgproto3/portal_suspended.go @@ -0,0 +1,29 @@ +package pgproto3 + +import ( + "encoding/json" +) + +type PortalSuspended struct{} + +func (*PortalSuspended) Backend() {} + +func (dst *PortalSuspended) Decode(src []byte) error { + if len(src) != 0 { + return &invalidMessageLenErr{messageType: "PortalSuspended", expectedLen: 0, actualLen: len(src)} + } + + return nil +} + +func (src *PortalSuspended) Encode(dst []byte) []byte { + return append(dst, 's', 0, 0, 0, 4) +} + +func (src *PortalSuspended) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Type string + }{ + Type: "PortalSuspended", + }) +} diff --git a/pgtype/numeric.go b/pgtype/numeric.go index fb63df75..e14d02e4 100644 --- a/pgtype/numeric.go +++ b/pgtype/numeric.go @@ -321,7 +321,7 @@ func parseNumericString(str string) (n *big.Int, exp int32, err error) { if len(parts) > 1 { exp = int32(-len(parts[1])) } else { - for len(digits) > 1 && digits[len(digits)-1] == '0' { + for len(digits) > 1 && digits[len(digits)-1] == '0' && digits[len(digits)-2] != '-' { digits = digits[:len(digits)-1] exp++ } diff --git a/pgtype/numeric_array_test.go b/pgtype/numeric_array_test.go index 22ee1bc4..28aa67d9 100644 --- a/pgtype/numeric_array_test.go +++ b/pgtype/numeric_array_test.go @@ -1,6 +1,7 @@ package pgtype_test import ( + "math" "math/big" "reflect" "testing" @@ -65,6 +66,13 @@ func TestNumericArraySet(t *testing.T) { Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 1}}, Status: pgtype.Present}, }, + { + source: []float32{float32(math.Copysign(0, -1))}, + result: pgtype.NumericArray{ + Elements: []pgtype.Numeric{{Int: big.NewInt(0), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 1}}, + Status: pgtype.Present}, + }, { source: []float64{1}, result: pgtype.NumericArray{ @@ -72,6 +80,13 @@ func TestNumericArraySet(t *testing.T) { Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 1}}, Status: pgtype.Present}, }, + { + source: []float64{math.Copysign(0, -1)}, + result: pgtype.NumericArray{ + Elements: []pgtype.Numeric{{Int: big.NewInt(0), Status: pgtype.Present}}, + Dimensions: []pgtype.ArrayDimension{{LowerBound: 1, Length: 1}}, + Status: pgtype.Present}, + }, { source: (([]float32)(nil)), result: pgtype.NumericArray{Status: pgtype.Null}, diff --git a/pgtype/numeric_test.go b/pgtype/numeric_test.go index 9d7d83d6..a5f70c9e 100644 --- a/pgtype/numeric_test.go +++ b/pgtype/numeric_test.go @@ -1,6 +1,7 @@ package pgtype_test import ( + "math" "math/big" "math/rand" "reflect" @@ -188,7 +189,9 @@ func TestNumericSet(t *testing.T) { result *pgtype.Numeric }{ {source: float32(1), result: &pgtype.Numeric{Int: big.NewInt(1), Status: pgtype.Present}}, + {source: float32(math.Copysign(0, -1)), result: &pgtype.Numeric{Int: big.NewInt(0), Status: pgtype.Present}}, {source: float64(1), result: &pgtype.Numeric{Int: big.NewInt(1), Status: pgtype.Present}}, + {source: float64(math.Copysign(0, -1)), result: &pgtype.Numeric{Int: big.NewInt(0), Status: pgtype.Present}}, {source: int8(1), result: &pgtype.Numeric{Int: big.NewInt(1), Status: pgtype.Present}}, {source: int16(1), result: &pgtype.Numeric{Int: big.NewInt(1), Status: pgtype.Present}}, {source: int32(1), result: &pgtype.Numeric{Int: big.NewInt(1), Status: pgtype.Present}}, diff --git a/query.go b/query.go index 27969be9..5c6cbf7f 100644 --- a/query.go +++ b/query.go @@ -84,7 +84,7 @@ func (rows *Rows) Close() { rows.conn.log(LogLevelInfo, "Query", map[string]interface{}{"sql": rows.sql, "args": logQueryArgs(rows.args), "time": endTime.Sub(rows.startTime), "rowCount": rows.rowCount}) } } else if rows.conn.shouldLog(LogLevelError) { - rows.conn.log(LogLevelError, "Query", map[string]interface{}{"sql": rows.sql, "args": logQueryArgs(rows.args)}) + rows.conn.log(LogLevelError, "Query", map[string]interface{}{"sql": rows.sql, "args": logQueryArgs(rows.args), "err": rows.err}) } if rows.batch != nil && rows.err != nil { @@ -522,6 +522,10 @@ func (c *Conn) readUntilRowDescription() ([]FieldDescription, error) { } func (c *Conn) sanitizeAndSendSimpleQuery(sql string, args ...interface{}) (err error) { + if len(args) == 0 { + return c.sendSimpleQuery(sql) + } + if c.RuntimeParams["standard_conforming_strings"] != "on" { return errors.New("simple protocol queries must be run with standard_conforming_strings=on") }