From d35f58a2b543debca192ce5924d62a0756eed232 Mon Sep 17 00:00:00 2001 From: Dan Sosedoff Date: Tue, 14 Jul 2015 22:11:02 -0500 Subject: [PATCH] Update pg dependency to latest --- Godeps/Godeps.json | 4 +- .../src/github.com/lib/pq/.travis.yml | 19 +- .../src/github.com/lib/pq/README.md | 5 + .../src/github.com/lib/pq/bench_test.go | 13 +- .../_workspace/src/github.com/lib/pq/buf.go | 29 +- .../_workspace/src/github.com/lib/pq/conn.go | 667 ++++++++++++------ .../src/github.com/lib/pq/conn_test.go | 74 +- .../_workspace/src/github.com/lib/pq/copy.go | 2 + .../src/github.com/lib/pq/copy_test.go | 94 ++- .../_workspace/src/github.com/lib/pq/doc.go | 3 +- .../src/github.com/lib/pq/encode.go | 169 ++++- .../src/github.com/lib/pq/encode_test.go | 314 ++++++++- .../_workspace/src/github.com/lib/pq/error.go | 13 + .../github.com/lib/pq/hstore/hstore_test.go | 3 +- .../github.com/lib/pq/listen_example/doc.go | 4 +- .../src/github.com/lib/pq/notify.go | 46 +- .../src/github.com/lib/pq/notify_test.go | 74 +- .../src/github.com/lib/pq/oid/gen.go | 2 +- .../_workspace/src/github.com/lib/pq/url.go | 2 +- 19 files changed, 1216 insertions(+), 321 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 116330d..2ef1c8d 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -28,8 +28,8 @@ }, { "ImportPath": "github.com/lib/pq", - "Comment": "go1.0-cutoff-13-g19eeca3", - "Rev": "19eeca3e30d2577b1761db471ec130810e67f532" + "Comment": "go1.0-cutoff-56-gdc50b6a", + "Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43" }, { "ImportPath": "github.com/mitchellh/go-homedir", diff --git a/Godeps/_workspace/src/github.com/lib/pq/.travis.yml b/Godeps/_workspace/src/github.com/lib/pq/.travis.yml index 82df2a0..9bf6837 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/.travis.yml +++ b/Godeps/_workspace/src/github.com/lib/pq/.travis.yml @@ -44,13 +44,20 @@ env: - PGUSER=postgres - PQGOSSLTESTS=1 - PQSSLCERTTEST_PATH=$PWD/certs + - PGHOST=127.0.0.1 matrix: - - PGVERSION=9.4 - - PGVERSION=9.3 - - PGVERSION=9.2 - - PGVERSION=9.1 - - PGVERSION=9.0 - - PGVERSION=8.4 + - PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=yes + - PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=no + - PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=no script: - go test -v ./... diff --git a/Godeps/_workspace/src/github.com/lib/pq/README.md b/Godeps/_workspace/src/github.com/lib/pq/README.md index cb15d6f..358d644 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/README.md +++ b/Godeps/_workspace/src/github.com/lib/pq/README.md @@ -57,10 +57,13 @@ code still exists in here. * Brad Fitzpatrick (bradfitz) * Charlie Melbye (cmelbye) * Chris Bandy (cbandy) +* Chris Gilling (cgilling) * Chris Walsh (cwds) * Dan Sosedoff (sosedoff) * Daniel Farina (fdr) * Eric Chlebek (echlebek) +* Eric Garrido (minusnine) +* Eric Urban (hydrogen18) * Everyone at The Go Team * Evan Shaw (edsrzf) * Ewan Chou (coocood) @@ -94,5 +97,7 @@ code still exists in here. * Ryan Smith (ryandotsmith) * Samuel Stauffer (samuel) * Timothée Peignier (cyberdelia) +* Travis Cline (tmc) * TruongSinh Tran-Nguyen (truongsinh) +* Yaismel Miranda (ympons) * notedit (notedit) diff --git a/Godeps/_workspace/src/github.com/lib/pq/bench_test.go b/Godeps/_workspace/src/github.com/lib/pq/bench_test.go index f2eb1da..e71f41d 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/bench_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/bench_test.go @@ -7,7 +7,6 @@ import ( "bytes" "database/sql" "database/sql/driver" - "github.com/lib/pq/oid" "io" "math/rand" "net" @@ -17,6 +16,8 @@ import ( "sync" "testing" "time" + + "github.com/lib/pq/oid" ) var ( @@ -324,7 +325,7 @@ var testIntBytes = []byte("1234") func BenchmarkDecodeInt64(b *testing.B) { for i := 0; i < b.N; i++ { - decode(¶meterStatus{}, testIntBytes, oid.T_int8) + decode(¶meterStatus{}, testIntBytes, oid.T_int8, formatText) } } @@ -332,7 +333,7 @@ var testFloatBytes = []byte("3.14159") func BenchmarkDecodeFloat64(b *testing.B) { for i := 0; i < b.N; i++ { - decode(¶meterStatus{}, testFloatBytes, oid.T_float8) + decode(¶meterStatus{}, testFloatBytes, oid.T_float8, formatText) } } @@ -340,7 +341,7 @@ var testBoolBytes = []byte{'t'} func BenchmarkDecodeBool(b *testing.B) { for i := 0; i < b.N; i++ { - decode(¶meterStatus{}, testBoolBytes, oid.T_bool) + decode(¶meterStatus{}, testBoolBytes, oid.T_bool, formatText) } } @@ -357,7 +358,7 @@ var testTimestamptzBytes = []byte("2013-09-17 22:15:32.360754-07") func BenchmarkDecodeTimestamptz(b *testing.B) { for i := 0; i < b.N; i++ { - decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz) + decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText) } } @@ -370,7 +371,7 @@ func BenchmarkDecodeTimestamptzMultiThread(b *testing.B) { f := func(wg *sync.WaitGroup, loops int) { defer wg.Done() for i := 0; i < loops; i++ { - decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz) + decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText) } } diff --git a/Godeps/_workspace/src/github.com/lib/pq/buf.go b/Godeps/_workspace/src/github.com/lib/pq/buf.go index 9f417a1..e7ff577 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/buf.go +++ b/Godeps/_workspace/src/github.com/lib/pq/buf.go @@ -3,6 +3,7 @@ package pq import ( "bytes" "encoding/binary" + "github.com/lib/pq/oid" ) @@ -46,28 +47,44 @@ func (b *readBuf) byte() byte { return b.next(1)[0] } -type writeBuf []byte +type writeBuf struct { + buf []byte + pos int +} func (b *writeBuf) int32(n int) { x := make([]byte, 4) binary.BigEndian.PutUint32(x, uint32(n)) - *b = append(*b, x...) + b.buf = append(b.buf, x...) } func (b *writeBuf) int16(n int) { x := make([]byte, 2) binary.BigEndian.PutUint16(x, uint16(n)) - *b = append(*b, x...) + b.buf = append(b.buf, x...) } func (b *writeBuf) string(s string) { - *b = append(*b, (s + "\000")...) + b.buf = append(b.buf, (s + "\000")...) } func (b *writeBuf) byte(c byte) { - *b = append(*b, c) + b.buf = append(b.buf, c) } func (b *writeBuf) bytes(v []byte) { - *b = append(*b, v...) + b.buf = append(b.buf, v...) +} + +func (b *writeBuf) wrap() []byte { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + return b.buf +} + +func (b *writeBuf) next(c byte) { + p := b.buf[b.pos:] + binary.BigEndian.PutUint32(p, uint32(len(p))) + b.pos = len(b.buf) + 1 + b.buf = append(b.buf, c, 0, 0, 0, 0) } diff --git a/Godeps/_workspace/src/github.com/lib/pq/conn.go b/Godeps/_workspace/src/github.com/lib/pq/conn.go index 44e4833..40a630d 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/conn.go +++ b/Godeps/_workspace/src/github.com/lib/pq/conn.go @@ -10,7 +10,6 @@ import ( "encoding/binary" "errors" "fmt" - "github.com/lib/pq/oid" "io" "io/ioutil" "net" @@ -22,6 +21,8 @@ import ( "strings" "time" "unicode" + + "github.com/lib/pq/oid" ) // Common error types @@ -105,12 +106,49 @@ type conn struct { // If true, this connection is bad and all public-facing functions should // return ErrBadConn. bad bool + + // If set, this connection should never use the binary format when + // receiving query results from prepared statements. Only provided for + // debugging. + disablePreparedBinaryResult bool + + // Whether to always send []byte parameters over as binary. Enables single + // round-trip mode for non-prepared Query calls. + binaryParameters bool +} + +// Handle driver-side settings in parsed connection string. +func (c *conn) handleDriverSettings(o values) (err error) { + boolSetting := func(key string, val *bool) error { + if value := o.Get(key); value != "" { + if value == "yes" { + *val = true + } else if value == "no" { + *val = false + } else { + return fmt.Errorf("unrecognized value %q for %s", value, key) + } + } + return nil + } + + err = boolSetting("disable_prepared_binary_result", &c.disablePreparedBinaryResult) + if err != nil { + return err + } + err = boolSetting("binary_parameters", &c.binaryParameters) + if err != nil { + return err + } + return nil } func (c *conn) writeBuf(b byte) *writeBuf { c.scratch[0] = b - w := writeBuf(c.scratch[:5]) - return &w + return &writeBuf{ + buf: c.scratch[:5], + pos: 1, + } } func Open(name string) (_ driver.Conn, err error) { @@ -118,22 +156,11 @@ func Open(name string) (_ driver.Conn, err error) { } func DialOpen(d Dialer, name string) (_ driver.Conn, err error) { - defer func() { - // Handle any panics during connection initialization. Note that we - // specifically do *not* want to use errRecover(), as that would turn - // any connection errors into ErrBadConns, hiding the real error - // message from the user. - e := recover() - if e == nil { - // Do nothing - return - } - var ok bool - err, ok = e.(error) - if !ok { - err = fmt.Errorf("pq: unexpected error: %#v", e) - } - }() + // Handle any panics during connection initialization. Note that we + // specifically do *not* want to use errRecover(), as that would turn any + // connection errors into ErrBadConns, hiding the real error message from + // the user. + defer errRecoverNoErrBadConn(&err) o := make(values) @@ -151,7 +178,7 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) { o.Set(k, v) } - if strings.HasPrefix(name, "postgres://") { + if strings.HasPrefix(name, "postgres://") || strings.HasPrefix(name, "postgresql://") { name, err = ParseURL(name) if err != nil { return nil, err @@ -202,27 +229,36 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) { } } - c, err := dial(d, o) + cn := &conn{} + err = cn.handleDriverSettings(o) if err != nil { return nil, err } - cn := &conn{c: c} + cn.c, err = dial(d, o) + if err != nil { + return nil, err + } cn.ssl(o) cn.buf = bufio.NewReader(cn.c) cn.startup(o) + // reset the deadline, in case one was set (see dial) - err = cn.c.SetDeadline(time.Time{}) + if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" { + err = cn.c.SetDeadline(time.Time{}) + } return cn, err } func dial(d Dialer, o values) (net.Conn, error) { ntw, addr := network(o) - - timeout := o.Get("connect_timeout") + // SSL is not necessary or supported over UNIX domain sockets + if ntw == "unix" { + o["sslmode"] = "disable" + } // Zero or not specified means wait indefinitely. - if timeout != "" && timeout != "0" { + if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" { seconds, err := strconv.ParseInt(timeout, 10, 0) if err != nil { return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err) @@ -436,6 +472,9 @@ func (cn *conn) Commit() (err error) { _, commandTag, err := cn.simpleExec("COMMIT") if err != nil { + if cn.isInTransaction() { + cn.bad = true + } return err } if commandTag != "COMMIT" { @@ -455,6 +494,9 @@ func (cn *conn) Rollback() (err error) { cn.checkIsInTransaction(true) _, commandTag, err := cn.simpleExec("ROLLBACK") if err != nil { + if cn.isInTransaction() { + cn.bad = true + } return err } if commandTag != "ROLLBACK" { @@ -494,7 +536,7 @@ func (cn *conn) simpleExec(q string) (res driver.Result, commandTag string, err } } -func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) { +func (cn *conn) simpleQuery(q string) (res *rows, err error) { defer cn.errRecover(&err) st := &stmt{cn: cn, name: ""} @@ -515,7 +557,13 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) { cn.bad = true errorf("unexpected message %q in simple query execution", t) } - res = &rows{st: st, done: true} + res = &rows{ + cn: cn, + colNames: st.colNames, + colTyps: st.colTyps, + colFmts: st.colFmts, + done: true, + } case 'Z': cn.processReadyForQuery(r) // done @@ -534,8 +582,8 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) { case 'T': // res might be non-nil here if we received a previous // CommandComplete, but that's fine; just overwrite it - res = &rows{st: st} - st.cols, st.rowTyps = parseMeta(r) + res = &rows{cn: cn} + res.colNames, res.colFmts, res.colTyps = parsePortalRowDescribe(r) // To work around a bug in QueryRow in Go 1.2 and earlier, wait // until the first DataRow has been received. @@ -546,47 +594,74 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) { } } -func (cn *conn) prepareTo(q, stmtName string) (_ *stmt, err error) { +// Decides which column formats to use for a prepared statement. The input is +// an array of type oids, one element per result column. +func decideColumnFormats(colTyps []oid.Oid, forceText bool) (colFmts []format, colFmtData []byte) { + if len(colTyps) == 0 { + return nil, colFmtDataAllText + } + + colFmts = make([]format, len(colTyps)) + if forceText { + return colFmts, colFmtDataAllText + } + + allBinary := true + allText := true + for i, o := range colTyps { + switch o { + // This is the list of types to use binary mode for when receiving them + // through a prepared statement. If a type appears in this list, it + // must also be implemented in binaryDecode in encode.go. + case oid.T_bytea: + fallthrough + case oid.T_int8: + fallthrough + case oid.T_int4: + fallthrough + case oid.T_int2: + colFmts[i] = formatBinary + allText = false + + default: + allBinary = false + } + } + + if allBinary { + return colFmts, colFmtDataAllBinary + } else if allText { + return colFmts, colFmtDataAllText + } else { + colFmtData = make([]byte, 2+len(colFmts)*2) + binary.BigEndian.PutUint16(colFmtData, uint16(len(colFmts))) + for i, v := range colFmts { + binary.BigEndian.PutUint16(colFmtData[2+i*2:], uint16(v)) + } + return colFmts, colFmtData + } +} + +func (cn *conn) prepareTo(q, stmtName string) *stmt { st := &stmt{cn: cn, name: stmtName} b := cn.writeBuf('P') b.string(st.name) b.string(q) b.int16(0) - cn.send(b) - b = cn.writeBuf('D') + b.next('D') b.byte('S') b.string(st.name) + + b.next('S') cn.send(b) - cn.send(cn.writeBuf('S')) - - for { - t, r := cn.recv1() - switch t { - case '1': - case 't': - nparams := r.int16() - st.paramTyps = make([]oid.Oid, nparams) - - for i := range st.paramTyps { - st.paramTyps[i] = r.oid() - } - case 'T': - st.cols, st.rowTyps = parseMeta(r) - case 'n': - // no data - case 'Z': - cn.processReadyForQuery(r) - return st, err - case 'E': - err = parseError(r) - default: - cn.bad = true - errorf("unexpected describe rows response: %q", t) - } - } + cn.readParseResponse() + st.paramTyps, st.colNames, st.colTyps = cn.readStatementDescribeResponse() + st.colFmts, st.colFmtData = decideColumnFormats(st.colTyps, cn.disablePreparedBinaryResult) + cn.readReadyForQuery() + return st } func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { @@ -598,7 +673,7 @@ func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") { return cn.prepareCopyIn(q) } - return cn.prepareTo(q, cn.gname()) + return cn.prepareTo(q, cn.gname()), nil } func (cn *conn) Close() (err error) { @@ -630,17 +705,29 @@ func (cn *conn) Query(query string, args []driver.Value) (_ driver.Rows, err err return cn.simpleQuery(query) } - st, err := cn.prepareTo(query, "") - if err != nil { - panic(err) - } + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) - st.exec(args) - return &rows{st: st}, nil + cn.readParseResponse() + cn.readBindResponse() + rows := &rows{cn: cn} + rows.colNames, rows.colFmts, rows.colTyps = cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + return rows, nil + } else { + st := cn.prepareTo(query, "") + st.exec(args) + return &rows{ + cn: cn, + colNames: st.colNames, + colTyps: st.colTyps, + colFmts: st.colFmts, + }, nil + } } // Implement the optional "Execer" interface for one-shot queries -func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err error) { +func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) { if cn.bad { return nil, driver.ErrBadConn } @@ -654,32 +741,42 @@ func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err er return r, err } - // Use the unnamed statement to defer planning until bind - // time, or else value-based selectivity estimates cannot be - // used. - st, err := cn.prepareTo(query, "") - if err != nil { - panic(err) - } + if cn.binaryParameters { + cn.sendBinaryModeQuery(query, args) - r, err := st.Exec(args) - if err != nil { - panic(err) + cn.readParseResponse() + cn.readBindResponse() + cn.readPortalDescribeResponse() + cn.postExecuteWorkaround() + res, _, err = cn.readExecuteResponse("Execute") + return res, err + } else { + // Use the unnamed statement to defer planning until bind + // time, or else value-based selectivity estimates cannot be + // used. + st := cn.prepareTo(query, "") + r, err := st.Exec(args) + if err != nil { + panic(err) + } + return r, err } - - return r, err } -// Assumes len(*m) is > 5 func (cn *conn) send(m *writeBuf) { - b := (*m)[1:] - binary.BigEndian.PutUint32(b, uint32(len(b))) + _, err := cn.c.Write(m.wrap()) + if err != nil { + panic(err) + } +} - if (*m)[0] == 0 { - *m = b +func (cn *conn) sendStartupPacket(m *writeBuf) { + // sanity check + if m.buf[0] != 0 { + panic("oops") } - _, err := cn.c.Write(*m) + _, err := cn.c.Write((m.wrap())[1:]) if err != nil { panic(err) } @@ -819,7 +916,7 @@ func (cn *conn) ssl(o values) { w := cn.writeBuf(0) w.int32(80877103) - cn.send(w) + cn.sendStartupPacket(w) b := cn.scratch[:1] _, err := io.ReadFull(cn.c, b) @@ -956,6 +1053,10 @@ func isDriverSetting(key string) bool { return true case "connect_timeout": return true + case "disable_prepared_binary_result": + return true + case "binary_parameters": + return true default: return false @@ -983,7 +1084,7 @@ func (cn *conn) startup(o values) { w.string(v) } w.string("") - cn.send(w) + cn.sendStartupPacket(w) for { t, r := cn.recv() @@ -1038,13 +1139,26 @@ func (cn *conn) auth(r *readBuf, o values) { } } +type format int + +const formatText format = 0 +const formatBinary format = 1 + +// One result-column format code with the value 1 (i.e. all binary). +var colFmtDataAllBinary []byte = []byte{0, 1, 0, 1} + +// No result-column format codes (i.e. all text). +var colFmtDataAllText []byte = []byte{0, 0} + type stmt struct { - cn *conn - name string - cols []string - rowTyps []oid.Oid - paramTyps []oid.Oid - closed bool + cn *conn + name string + colNames []string + colFmts []format + colFmtData []byte + colTyps []oid.Oid + paramTyps []oid.Oid + closed bool } func (st *stmt) Close() (err error) { @@ -1087,7 +1201,12 @@ func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) { defer st.cn.errRecover(&err) st.exec(v) - return &rows{st: st}, nil + return &rows{ + cn: st.cn, + colNames: st.colNames, + colTyps: st.colTyps, + colFmts: st.colFmts, + }, nil } func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { @@ -1097,25 +1216,8 @@ func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { defer st.cn.errRecover(&err) st.exec(v) - - for { - t, r := st.cn.recv1() - switch t { - case 'E': - err = parseError(r) - case 'C': - res, _ = st.cn.parseComplete(r.string()) - case 'Z': - st.cn.processReadyForQuery(r) - // done - return - case 'T', 'D', 'I': - // ignore any results - default: - st.cn.bad = true - errorf("unknown exec response: %q", t) - } - } + res, _, err = st.cn.readExecuteResponse("simple query") + return res, err } func (st *stmt) exec(v []driver.Value) { @@ -1126,84 +1228,38 @@ func (st *stmt) exec(v []driver.Value) { errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps)) } - w := st.cn.writeBuf('B') - w.string("") + cn := st.cn + w := cn.writeBuf('B') + w.byte(0) // unnamed portal w.string(st.name) - w.int16(0) - w.int16(len(v)) - for i, x := range v { - if x == nil { - w.int32(-1) - } else { - b := encode(&st.cn.parameterStatus, x, st.paramTyps[i]) - w.int32(len(b)) - w.bytes(b) + + if cn.binaryParameters { + cn.sendBinaryParameters(w, v) + } else { + w.int16(0) + w.int16(len(v)) + for i, x := range v { + if x == nil { + w.int32(-1) + } else { + b := encode(&cn.parameterStatus, x, st.paramTyps[i]) + w.int32(len(b)) + w.bytes(b) + } } } - w.int16(0) - st.cn.send(w) + w.bytes(st.colFmtData) - w = st.cn.writeBuf('E') - w.string("") + w.next('E') + w.byte(0) w.int32(0) - st.cn.send(w) - st.cn.send(st.cn.writeBuf('S')) + w.next('S') + cn.send(w) - var err error - for { - t, r := st.cn.recv1() - switch t { - case 'E': - err = parseError(r) - case '2': - if err != nil { - panic(err) - } - goto workaround - case 'Z': - st.cn.processReadyForQuery(r) - if err != nil { - panic(err) - } - return - default: - st.cn.bad = true - errorf("unexpected bind response: %q", t) - } - } + cn.readBindResponse() + cn.postExecuteWorkaround() - // Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores - // any errors from rows.Next, which masks errors that happened during the - // execution of the query. To avoid the problem in common cases, we wait - // here for one more message from the database. If it's not an error the - // query will likely succeed (or perhaps has already, if it's a - // CommandComplete), so we push the message into the conn struct; recv1 - // will return it as the next message for rows.Next or rows.Close. - // However, if it's an error, we wait until ReadyForQuery and then return - // the error to our caller. -workaround: - for { - t, r := st.cn.recv1() - switch t { - case 'E': - err = parseError(r) - case 'C', 'D', 'I': - // the query didn't fail, but we can't process this message - st.cn.saveMessage(t, r) - return - case 'Z': - if err == nil { - st.cn.bad = true - errorf("unexpected ReadyForQuery during extended query execution") - } - st.cn.processReadyForQuery(r) - panic(err) - default: - st.cn.bad = true - errorf("unexpected message during query execution: %q", t) - } - } } func (st *stmt) NumInput() int { @@ -1260,9 +1316,12 @@ func (cn *conn) parseComplete(commandTag string) (driver.Result, string) { } type rows struct { - st *stmt - done bool - rb readBuf + cn *conn + colNames []string + colTyps []oid.Oid + colFmts []format + done bool + rb readBuf } func (rs *rows) Close() error { @@ -1280,7 +1339,7 @@ func (rs *rows) Close() error { } func (rs *rows) Columns() []string { - return rs.st.cols + return rs.colNames } func (rs *rows) Next(dest []driver.Value) (err error) { @@ -1288,7 +1347,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) { return io.EOF } - conn := rs.st.cn + conn := rs.cn if conn.bad { return driver.ErrBadConn } @@ -1319,7 +1378,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) { dest[i] = nil continue } - dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.st.rowTyps[i]) + dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.colTyps[i], rs.colFmts[i]) } return default: @@ -1352,6 +1411,68 @@ func md5s(s string) string { return fmt.Sprintf("%x", h.Sum(nil)) } +func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) { + // Do one pass over the parameters to see if we're going to send any of + // them over in binary. If we are, create a paramFormats array at the + // same time. + var paramFormats []int + for i, x := range args { + _, ok := x.([]byte) + if ok { + if paramFormats == nil { + paramFormats = make([]int, len(args)) + } + paramFormats[i] = 1 + } + } + if paramFormats == nil { + b.int16(0) + } else { + b.int16(len(paramFormats)) + for _, x := range paramFormats { + b.int16(x) + } + } + + b.int16(len(args)) + for _, x := range args { + if x == nil { + b.int32(-1) + } else { + datum := binaryEncode(&cn.parameterStatus, x) + b.int32(len(datum)) + b.bytes(datum) + } + } +} + +func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) { + if len(args) >= 65536 { + errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args)) + } + + b := cn.writeBuf('P') + b.byte(0) // unnamed statement + b.string(query) + b.int16(0) + + b.next('B') + b.int16(0) // unnamed portal and statement + cn.sendBinaryParameters(b, args) + b.bytes(colFmtDataAllText) + + b.next('D') + b.byte('P') + b.byte(0) // unnamed portal + + b.next('E') + b.byte(0) + b.int32(0) + + b.next('S') + cn.send(b) +} + func (c *conn) processParameterStatus(r *readBuf) { var err error @@ -1381,15 +1502,167 @@ func (c *conn) processReadyForQuery(r *readBuf) { c.txnStatus = transactionStatus(r.byte()) } -func parseMeta(r *readBuf) (cols []string, rowTyps []oid.Oid) { +func (cn *conn) readReadyForQuery() { + t, r := cn.recv1() + switch t { + case 'Z': + cn.processReadyForQuery(r) + return + default: + cn.bad = true + errorf("unexpected message %q; expected ReadyForQuery", t) + } +} + +func (cn *conn) readParseResponse() { + t, r := cn.recv1() + switch t { + case '1': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Parse response %q", t) + } +} + +func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames []string, colTyps []oid.Oid) { + for { + t, r := cn.recv1() + switch t { + case 't': + nparams := r.int16() + paramTyps = make([]oid.Oid, nparams) + for i := range paramTyps { + paramTyps[i] = r.oid() + } + case 'n': + return paramTyps, nil, nil + case 'T': + colNames, colTyps = parseStatementRowDescribe(r) + return paramTyps, colNames, colTyps + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Describe statement response %q", t) + } + } +} + +func (cn *conn) readPortalDescribeResponse() (colNames []string, colFmts []format, colTyps []oid.Oid) { + t, r := cn.recv1() + switch t { + case 'T': + return parsePortalRowDescribe(r) + case 'n': + return nil, nil, nil + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Describe response %q", t) + } + panic("not reached") +} + +func (cn *conn) readBindResponse() { + t, r := cn.recv1() + switch t { + case '2': + return + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + default: + cn.bad = true + errorf("unexpected Bind response %q", t) + } +} + +func (cn *conn) postExecuteWorkaround() { + // Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores + // any errors from rows.Next, which masks errors that happened during the + // execution of the query. To avoid the problem in common cases, we wait + // here for one more message from the database. If it's not an error the + // query will likely succeed (or perhaps has already, if it's a + // CommandComplete), so we push the message into the conn struct; recv1 + // will return it as the next message for rows.Next or rows.Close. + // However, if it's an error, we wait until ReadyForQuery and then return + // the error to our caller. + for { + t, r := cn.recv1() + switch t { + case 'E': + err := parseError(r) + cn.readReadyForQuery() + panic(err) + case 'C', 'D', 'I': + // the query didn't fail, but we can't process this message + cn.saveMessage(t, r) + return + default: + cn.bad = true + errorf("unexpected message during extended query execution: %q", t) + } + } +} + +// Only for Exec(), since we ignore the returned data +func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, commandTag string, err error) { + for { + t, r := cn.recv1() + switch t { + case 'C': + res, commandTag = cn.parseComplete(r.string()) + case 'Z': + cn.processReadyForQuery(r) + return res, commandTag, err + case 'E': + err = parseError(r) + case 'T', 'D', 'I': + // ignore any results + default: + cn.bad = true + errorf("unknown %s response: %q", protocolState, t) + } + } +} + +func parseStatementRowDescribe(r *readBuf) (colNames []string, colTyps []oid.Oid) { n := r.int16() - cols = make([]string, n) - rowTyps = make([]oid.Oid, n) - for i := range cols { - cols[i] = r.string() + colNames = make([]string, n) + colTyps = make([]oid.Oid, n) + for i := range colNames { + colNames[i] = r.string() r.next(6) - rowTyps[i] = r.oid() - r.next(8) + colTyps[i] = r.oid() + r.next(6) + // format code not known when describing a statement; always 0 + r.next(2) + } + return +} + +func parsePortalRowDescribe(r *readBuf) (colNames []string, colFmts []format, colTyps []oid.Oid) { + n := r.int16() + colNames = make([]string, n) + colFmts = make([]format, n) + colTyps = make([]oid.Oid, n) + for i := range colNames { + colNames[i] = r.string() + r.next(6) + colTyps[i] = r.oid() + r.next(6) + colFmts[i] = format(r.int16()) } return } diff --git a/Godeps/_workspace/src/github.com/lib/pq/conn_test.go b/Godeps/_workspace/src/github.com/lib/pq/conn_test.go index 6c3c6b5..af07e55 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/conn_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/conn_test.go @@ -7,6 +7,7 @@ import ( "io" "os" "reflect" + "strings" "testing" "time" ) @@ -15,21 +16,31 @@ type Fatalistic interface { Fatal(args ...interface{}) } +func forceBinaryParameters() bool { + bp := os.Getenv("PQTEST_BINARY_PARAMETERS") + if bp == "yes" { + return true + } else if bp == "" || bp == "no" { + return false + } else { + panic("unexpected value for PQTEST_BINARY_PARAMETERS") + } +} + func openTestConnConninfo(conninfo string) (*sql.DB, error) { - datname := os.Getenv("PGDATABASE") - sslmode := os.Getenv("PGSSLMODE") - timeout := os.Getenv("PGCONNECT_TIMEOUT") - - if datname == "" { - os.Setenv("PGDATABASE", "pqgotest") + defaultTo := func(envvar string, value string) { + if os.Getenv(envvar) == "" { + os.Setenv(envvar, value) + } } + defaultTo("PGDATABASE", "pqgotest") + defaultTo("PGSSLMODE", "disable") + defaultTo("PGCONNECT_TIMEOUT", "20") - if sslmode == "" { - os.Setenv("PGSSLMODE", "disable") - } - - if timeout == "" { - os.Setenv("PGCONNECT_TIMEOUT", "20") + if forceBinaryParameters() && + !strings.HasPrefix(conninfo, "postgres://") && + !strings.HasPrefix(conninfo, "postgresql://") { + conninfo = conninfo + " binary_parameters=yes" } return sql.Open("postgres", conninfo) @@ -106,18 +117,22 @@ func TestCommitInFailedTransaction(t *testing.T) { } func TestOpenURL(t *testing.T) { - db, err := openTestConnConninfo("postgres://") - if err != nil { - t.Fatal(err) + testURL := func(url string) { + db, err := openTestConnConninfo(url) + if err != nil { + t.Fatal(err) + } + defer db.Close() + // database/sql might not call our Open at all unless we do something with + // the connection + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + txn.Rollback() } - defer db.Close() - // database/sql might not call our Open at all unless we do something with - // the connection - txn, err := db.Begin() - if err != nil { - t.Fatal(err) - } - txn.Rollback() + testURL("postgres://") + testURL("postgresql://") } func TestExec(t *testing.T) { @@ -342,6 +357,7 @@ func TestEncodeDecode(t *testing.T) { '2000-1-1 01:02:03.04-7'::timestamptz, 0::boolean, 123, + -321, 3.14::float8 WHERE E'\\000\\001\\002'::bytea = $1 @@ -370,9 +386,9 @@ func TestEncodeDecode(t *testing.T) { var got2 string var got3 = sql.NullInt64{Valid: true} var got4 time.Time - var got5, got6, got7 interface{} + var got5, got6, got7, got8 interface{} - err = r.Scan(&got1, &got2, &got3, &got4, &got5, &got6, &got7) + err = r.Scan(&got1, &got2, &got3, &got4, &got5, &got6, &got7, &got8) if err != nil { t.Fatal(err) } @@ -401,8 +417,12 @@ func TestEncodeDecode(t *testing.T) { t.Fatalf("expected 123, got %d", got6) } - if got7 != float64(3.14) { - t.Fatalf("expected 3.14, got %f", got7) + if got7 != int64(-321) { + t.Fatalf("expected -321, got %d", got7) + } + + if got8 != float64(3.14) { + t.Fatalf("expected 3.14, got %f", got8) } } diff --git a/Godeps/_workspace/src/github.com/lib/pq/copy.go b/Godeps/_workspace/src/github.com/lib/pq/copy.go index 18c04e7..e44fa48 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/copy.go +++ b/Godeps/_workspace/src/github.com/lib/pq/copy.go @@ -150,6 +150,8 @@ func (ci *copyin) resploop() { switch t { case 'C': // complete + case 'N': + // NoticeResponse case 'Z': ci.cn.processReadyForQuery(&r) ci.done <- true diff --git a/Godeps/_workspace/src/github.com/lib/pq/copy_test.go b/Godeps/_workspace/src/github.com/lib/pq/copy_test.go index e489f22..6af4c9c 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/copy_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/copy_test.go @@ -94,6 +94,86 @@ func TestCopyInMultipleValues(t *testing.T) { } } +func TestCopyInRaiseStmtTrigger(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + if getServerVersion(t, db) < 90000 { + var exists int + err := db.QueryRow("SELECT 1 FROM pg_language WHERE lanname = 'plpgsql'").Scan(&exists) + if err == sql.ErrNoRows { + t.Skip("language PL/PgSQL does not exist; skipping TestCopyInRaiseStmtTrigger") + } else if err != nil { + t.Fatal(err) + } + } + + txn, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer txn.Rollback() + + _, err = txn.Exec("CREATE TEMP TABLE temp (a int, b varchar)") + if err != nil { + t.Fatal(err) + } + + _, err = txn.Exec(` + CREATE OR REPLACE FUNCTION pg_temp.temptest() + RETURNS trigger AS + $BODY$ begin + raise notice 'Hello world'; + return new; + end $BODY$ + LANGUAGE plpgsql`) + if err != nil { + t.Fatal(err) + } + + _, err = txn.Exec(` + CREATE TRIGGER temptest_trigger + BEFORE INSERT + ON temp + FOR EACH ROW + EXECUTE PROCEDURE pg_temp.temptest()`) + if err != nil { + t.Fatal(err) + } + + stmt, err := txn.Prepare(CopyIn("temp", "a", "b")) + if err != nil { + t.Fatal(err) + } + + longString := strings.Repeat("#", 500) + + _, err = stmt.Exec(int64(1), longString) + if err != nil { + t.Fatal(err) + } + + _, err = stmt.Exec() + if err != nil { + t.Fatal(err) + } + + err = stmt.Close() + if err != nil { + t.Fatal(err) + } + + var num int + err = txn.QueryRow("SELECT COUNT(*) FROM temp").Scan(&num) + if err != nil { + t.Fatal(err) + } + + if num != 1 { + t.Fatalf("expected 1 items, not %d", num) + } +} + func TestCopyInTypes(t *testing.T) { db := openTestConn(t) defer db.Close() @@ -307,12 +387,14 @@ func TestCopyRespLoopConnectionError(t *testing.T) { t.Fatal(err) } - // We have to try and send something over, since postgres won't process - // SIGTERMs while it's waiting for CopyData/CopyEnd messages; see - // tcop/postgres.c. - _, err = stmt.Exec(1) - if err != nil { - t.Fatal(err) + if getServerVersion(t, db) < 90500 { + // We have to try and send something over, since postgres before + // version 9.5 won't process SIGTERMs while it's waiting for + // CopyData/CopyEnd messages; see tcop/postgres.c. + _, err = stmt.Exec(1) + if err != nil { + t.Fatal(err) + } } _, err = stmt.Exec() if err == nil { diff --git a/Godeps/_workspace/src/github.com/lib/pq/doc.go b/Godeps/_workspace/src/github.com/lib/pq/doc.go index 4d7a0e3..f772117 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/doc.go +++ b/Godeps/_workspace/src/github.com/lib/pq/doc.go @@ -5,8 +5,9 @@ In most cases clients will use the database/sql package instead of using this package directly. For example: import ( - _ "github.com/lib/pq" "database/sql" + + _ "github.com/lib/pq" ) func main() { diff --git a/Godeps/_workspace/src/github.com/lib/pq/encode.go b/Godeps/_workspace/src/github.com/lib/pq/encode.go index 3887f72..88422eb 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/encode.go +++ b/Godeps/_workspace/src/github.com/lib/pq/encode.go @@ -3,24 +3,34 @@ package pq import ( "bytes" "database/sql/driver" + "encoding/binary" "encoding/hex" "fmt" - "github.com/lib/pq/oid" "math" "strconv" "strings" "sync" "time" + + "github.com/lib/pq/oid" ) +func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte { + switch v := x.(type) { + case []byte: + return v + default: + return encode(parameterStatus, x, oid.T_unknown) + } + panic("not reached") +} + func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte { switch v := x.(type) { case int64: - return []byte(fmt.Sprintf("%d", v)) - case float32: - return []byte(fmt.Sprintf("%.9f", v)) + return strconv.AppendInt(nil, v, 10) case float64: - return []byte(fmt.Sprintf("%.17f", v)) + return strconv.AppendFloat(nil, v, 'f', -1, 64) case []byte: if pgtypOid == oid.T_bytea { return encodeBytea(parameterStatus.serverVersion, v) @@ -34,7 +44,7 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [ return []byte(v) case bool: - return []byte(fmt.Sprintf("%t", v)) + return strconv.AppendBool(nil, v) case time.Time: return formatTs(v) @@ -45,7 +55,33 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [ panic("not reached") } -func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { +func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid, f format) interface{} { + if f == formatBinary { + return binaryDecode(parameterStatus, s, typ) + } else { + return textDecode(parameterStatus, s, typ) + } +} + +func binaryDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { + switch typ { + case oid.T_bytea: + return s + case oid.T_int8: + return int64(binary.BigEndian.Uint64(s)) + case oid.T_int4: + return int64(int32(binary.BigEndian.Uint32(s))) + case oid.T_int2: + return int64(int16(binary.BigEndian.Uint16(s))) + + default: + errorf("don't know how to decode binary parameter of type %u", uint32(typ)) + } + + panic("not reached") +} + +func textDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { switch typ { case oid.T_bytea: return parseBytea(s) @@ -59,7 +95,7 @@ func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} return mustParse("15:04:05-07", typ, s) case oid.T_bool: return s[0] == 't' - case oid.T_int8, oid.T_int2, oid.T_int4: + case oid.T_int8, oid.T_int4, oid.T_int2: i, err := strconv.ParseInt(string(s), 10, 64) if err != nil { errorf("%s", err) @@ -86,8 +122,6 @@ func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface switch v := x.(type) { case int64: return strconv.AppendInt(buf, v, 10) - case float32: - return strconv.AppendFloat(buf, float64(v), 'f', -1, 32) case float64: return strconv.AppendFloat(buf, v, 'f', -1, 64) case []byte: @@ -149,12 +183,6 @@ func appendEscapedText(buf []byte, text string) []byte { func mustParse(f string, typ oid.Oid, s []byte) time.Time { str := string(s) - // Special case until time.Parse bug is fixed: - // http://code.google.com/p/go/issues/detail?id=3487 - if str[len(str)-2] == '.' { - str += "0" - } - // check for a 30-minute-offset timezone if (typ == oid.T_timestamptz || typ == oid.T_timetz) && str[len(str)-3] == ':' { @@ -212,11 +240,72 @@ func (c *locationCache) getLocation(offset int) *time.Location { return location } +var infinityTsEnabled = false +var infinityTsNegative time.Time +var infinityTsPositive time.Time + +const ( + infinityTsEnabledAlready = "pq: infinity timestamp enabled already" + infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive" +) + +/* + * If EnableInfinityTs is not called, "-infinity" and "infinity" will return + * []byte("-infinity") and []byte("infinity") respectively, and potentially + * cause error "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time", + * when scanning into a time.Time value. + * + * Once EnableInfinityTs has been called, all connections created using this + * driver will decode Postgres' "-infinity" and "infinity" for "timestamp", + * "timestamp with time zone" and "date" types to the predefined minimum and + * maximum times, respectively. When encoding time.Time values, any time which + * equals or preceeds the predefined minimum time will be encoded to + * "-infinity". Any values at or past the maximum time will similarly be + * encoded to "infinity". + * + * + * If EnableInfinityTs is called with negative >= positive, it will panic. + * Calling EnableInfinityTs after a connection has been established results in + * undefined behavior. If EnableInfinityTs is called more than once, it will + * panic. + */ +func EnableInfinityTs(negative time.Time, positive time.Time) { + if infinityTsEnabled { + panic(infinityTsEnabledAlready) + } + if !negative.Before(positive) { + panic(infinityTsNegativeMustBeSmaller) + } + infinityTsEnabled = true + infinityTsNegative = negative + infinityTsPositive = positive +} + +/* + * Testing might want to toggle infinityTsEnabled + */ +func disableInfinityTs() { + infinityTsEnabled = false +} + // This is a time function specific to the Postgres default DateStyle // setting ("ISO, MDY"), the only one we currently support. This // accounts for the discrepancies between the parsing available with // time.Parse and the Postgres date formatting quirks. -func parseTs(currentLocation *time.Location, str string) (result time.Time) { +func parseTs(currentLocation *time.Location, str string) interface{} { + switch str { + case "-infinity": + if infinityTsEnabled { + return infinityTsNegative + } + return []byte(str) + case "infinity": + if infinityTsEnabled { + return infinityTsPositive + } + return []byte(str) + } + monSep := strings.IndexRune(str, '-') // this is Gregorian year, not ISO Year // In Gregorian system, the year 1 BC is followed by AD 1 @@ -310,10 +399,18 @@ func parseTs(currentLocation *time.Location, str string) (result time.Time) { return t } -// formatTs formats t as time.RFC3339Nano and appends time zone seconds if -// needed. +// formatTs formats t into a format postgres understands. func formatTs(t time.Time) (b []byte) { - b = []byte(t.Format(time.RFC3339Nano)) + if infinityTsEnabled { + // t <= -infinity : ! (t > -infinity) + if !t.After(infinityTsNegative) { + return []byte("-infinity") + } + // t >= infinity : ! (!t < infinity) + if !t.Before(infinityTsPositive) { + return []byte("infinity") + } + } // Need to send dates before 0001 A.D. with " BC" suffix, instead of the // minus sign preferred by Go. // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on @@ -324,25 +421,26 @@ func formatTs(t time.Time) (b []byte) { bc = true } b = []byte(t.Format(time.RFC3339Nano)) - if bc { - b = append(b, " BC"...) - } _, offset := t.Zone() offset = offset % 60 - if offset == 0 { - return b + if offset != 0 { + // RFC3339Nano already printed the minus sign + if offset < 0 { + offset = -offset + } + + b = append(b, ':') + if offset < 10 { + b = append(b, '0') + } + b = strconv.AppendInt(b, int64(offset), 10) } - if offset < 0 { - offset = -offset + if bc { + b = append(b, " BC"...) } - - b = append(b, ':') - if offset < 10 { - b = append(b, '0') - } - return strconv.AppendInt(b, int64(offset), 10) + return b } // Parse a bytea value received from the server. Both "hex" and the legacy @@ -397,7 +495,10 @@ func parseBytea(s []byte) (result []byte) { func encodeBytea(serverVersion int, v []byte) (result []byte) { if serverVersion >= 90000 { // Use the hex format if we know that the server supports it - result = []byte(fmt.Sprintf("\\x%x", v)) + result = make([]byte, 2+hex.EncodedLen(len(v))) + result[0] = '\\' + result[1] = 'x' + hex.Encode(result[2:], v) } else { // .. or resort to "escape" for _, b := range v { diff --git a/Godeps/_workspace/src/github.com/lib/pq/encode_test.go b/Godeps/_workspace/src/github.com/lib/pq/encode_test.go index e835f2b..97b6638 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/encode_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/encode_test.go @@ -1,12 +1,13 @@ package pq import ( - "github.com/lib/pq/oid" - "bytes" + "database/sql" "fmt" "testing" "time" + + "github.com/lib/pq/oid" ) func TestScanTimestamp(t *testing.T) { @@ -78,7 +79,11 @@ func tryParse(str string) (t time.Time, err error) { return } }() - t = parseTs(nil, str) + i := parseTs(nil, str) + t, ok := i.(time.Time) + if !ok { + err = fmt.Errorf("Not a time.Time type, got %#v", i) + } return } @@ -132,8 +137,18 @@ var formatTimeTests = []struct { {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"}, - {time.Date(1, time.January, 1, 0, 0, 0, 0, time.FixedZone("", 19*60+32)), "0001-01-01T00:00:00+00:19:32"}, {time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"}, + + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z"}, + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00"}, + {time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00"}, + + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z BC"}, + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00 BC"}, + {time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00 BC"}, + + {time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09"}, + {time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09 BC"}, } func TestFormatTs(t *testing.T) { @@ -249,6 +264,131 @@ func TestTimestampWithOutTimezone(t *testing.T) { test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033") } +func TestInfinityTimestamp(t *testing.T) { + db := openTestConn(t) + defer db.Close() + var err error + var resultT time.Time + + expectedError := fmt.Errorf(`sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time`) + type testCases []struct { + Query string + Param string + ExpectedErr error + ExpectedVal interface{} + } + tc := testCases{ + {"SELECT $1::timestamp", "-infinity", expectedError, "-infinity"}, + {"SELECT $1::timestamptz", "-infinity", expectedError, "-infinity"}, + {"SELECT $1::timestamp", "infinity", expectedError, "infinity"}, + {"SELECT $1::timestamptz", "infinity", expectedError, "infinity"}, + } + // try to assert []byte to time.Time + for _, q := range tc { + err = db.QueryRow(q.Query, q.Param).Scan(&resultT) + if err.Error() != q.ExpectedErr.Error() { + t.Errorf("Scanning -/+infinity, expected error, %q, got %q", q.ExpectedErr, err) + } + } + // yield []byte + for _, q := range tc { + var resultI interface{} + err = db.QueryRow(q.Query, q.Param).Scan(&resultI) + if err != nil { + t.Errorf("Scanning -/+infinity, expected no error, got %q", err) + } + result, ok := resultI.([]byte) + if !ok { + t.Errorf("Scanning -/+infinity, expected []byte, got %#v", resultI) + } + if string(result) != q.ExpectedVal { + t.Errorf("Scanning -/+infinity, expected %q, got %q", q.ExpectedVal, result) + } + } + + y1500 := time.Date(1500, time.January, 1, 0, 0, 0, 0, time.UTC) + y2500 := time.Date(2500, time.January, 1, 0, 0, 0, 0, time.UTC) + EnableInfinityTs(y1500, y2500) + + err = db.QueryRow("SELECT $1::timestamp", "infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning infinity, expected no error, got %q", err) + } + if !resultT.Equal(y2500) { + t.Errorf("Scanning infinity, expected %q, got %q", y2500, resultT) + } + + err = db.QueryRow("SELECT $1::timestamptz", "infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning infinity, expected no error, got %q", err) + } + if !resultT.Equal(y2500) { + t.Errorf("Scanning Infinity, expected time %q, got %q", y2500, resultT.String()) + } + + err = db.QueryRow("SELECT $1::timestamp", "-infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning -infinity, expected no error, got %q", err) + } + if !resultT.Equal(y1500) { + t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String()) + } + + err = db.QueryRow("SELECT $1::timestamptz", "-infinity").Scan(&resultT) + if err != nil { + t.Errorf("Scanning -infinity, expected no error, got %q", err) + } + if !resultT.Equal(y1500) { + t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String()) + } + + y_1500 := time.Date(-1500, time.January, 1, 0, 0, 0, 0, time.UTC) + y11500 := time.Date(11500, time.January, 1, 0, 0, 0, 0, time.UTC) + var s string + err = db.QueryRow("SELECT $1::timestamp::text", y_1500).Scan(&s) + if err != nil { + t.Errorf("Encoding -infinity, expected no error, got %q", err) + } + if s != "-infinity" { + t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s) + } + err = db.QueryRow("SELECT $1::timestamptz::text", y_1500).Scan(&s) + if err != nil { + t.Errorf("Encoding -infinity, expected no error, got %q", err) + } + if s != "-infinity" { + t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s) + } + + err = db.QueryRow("SELECT $1::timestamp::text", y11500).Scan(&s) + if err != nil { + t.Errorf("Encoding infinity, expected no error, got %q", err) + } + if s != "infinity" { + t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s) + } + err = db.QueryRow("SELECT $1::timestamptz::text", y11500).Scan(&s) + if err != nil { + t.Errorf("Encoding infinity, expected no error, got %q", err) + } + if s != "infinity" { + t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s) + } + + disableInfinityTs() + + var panicErrorString string + func() { + defer func() { + panicErrorString, _ = recover().(string) + }() + EnableInfinityTs(y2500, y1500) + }() + if panicErrorString != infinityTsNegativeMustBeSmaller { + t.Errorf("Expected error, %q, got %q", infinityTsNegativeMustBeSmaller, panicErrorString) + } +} + func TestStringWithNul(t *testing.T) { db := openTestConn(t) defer db.Close() @@ -261,7 +401,7 @@ func TestStringWithNul(t *testing.T) { } } -func TestByteaToText(t *testing.T) { +func TestByteSliceToText(t *testing.T) { db := openTestConn(t) defer db.Close() @@ -279,7 +419,7 @@ func TestByteaToText(t *testing.T) { } } -func TestTextToBytea(t *testing.T) { +func TestStringToBytea(t *testing.T) { db := openTestConn(t) defer db.Close() @@ -297,6 +437,136 @@ func TestTextToBytea(t *testing.T) { } } +func TestTextByteSliceToUUID(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + b := []byte("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + row := db.QueryRow("SELECT $1::uuid", b) + + var result string + err := row.Scan(&result) + if forceBinaryParameters() { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22P03" { + t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code) + } + } else { + if err != nil { + t.Fatal(err) + } + + if result != string(b) { + t.Fatalf("expected %v but got %v", b, result) + } + } +} + +func TestBinaryByteSlicetoUUID(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + b := []byte{'\xa0','\xee','\xbc','\x99', + '\x9c', '\x0b', + '\x4e', '\xf8', + '\xbb', '\x00', '\x6b', + '\xb9', '\xbd', '\x38', '\x0a', '\x11'} + row := db.QueryRow("SELECT $1::uuid", b) + + var result string + err := row.Scan(&result) + if forceBinaryParameters() { + if err != nil { + t.Fatal(err) + } + + if result != string("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11") { + t.Fatalf("expected %v but got %v", b, result) + } + } else { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22021" { + t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code) + } + } +} + +func TestStringToUUID(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + s := "a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11" + row := db.QueryRow("SELECT $1::uuid", s) + + var result string + err := row.Scan(&result) + if err != nil { + t.Fatal(err) + } + + if result != s { + t.Fatalf("expected %v but got %v", s, result) + } +} + +func TestTextByteSliceToInt(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + expected := 12345678 + b := []byte(fmt.Sprintf("%d", expected)) + row := db.QueryRow("SELECT $1::int", b) + + var result int + err := row.Scan(&result) + if forceBinaryParameters() { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22P03" { + t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code) + } + } else { + if err != nil { + t.Fatal(err) + } + if result != expected { + t.Fatalf("expected %v but got %v", expected, result) + } + } +} + +func TestBinaryByteSliceToInt(t *testing.T) { + db := openTestConn(t) + defer db.Close() + + expected := 12345678 + b := []byte{'\x00', '\xbc', '\x61', '\x4e'} + row := db.QueryRow("SELECT $1::int", b) + + var result int + err := row.Scan(&result) + if forceBinaryParameters() { + if err != nil { + t.Fatal(err) + } + if result != expected { + t.Fatalf("expected %v but got %v", expected, result) + } + } else { + pqErr := err.(*Error) + if pqErr == nil { + t.Errorf("Expected to get error") + } else if pqErr.Code != "22021" { + t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code) + } + } +} + func TestByteaOutputFormatEncoding(t *testing.T) { input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123") want := []byte("\\x5c78000102fffe6162636465666730313233") @@ -321,7 +591,7 @@ func TestByteaOutputFormats(t *testing.T) { return } - testByteaOutputFormat := func(f string) { + testByteaOutputFormat := func(f string, usePrepared bool) { expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08") sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')" @@ -338,8 +608,18 @@ func TestByteaOutputFormats(t *testing.T) { if err != nil { t.Fatal(err) } - // use Query; QueryRow would hide the actual error - rows, err := txn.Query(sqlQuery) + var rows *sql.Rows + var stmt *sql.Stmt + if usePrepared { + stmt, err = txn.Prepare(sqlQuery) + if err != nil { + t.Fatal(err) + } + rows, err = stmt.Query() + } else { + // use Query; QueryRow would hide the actual error + rows, err = txn.Query(sqlQuery) + } if err != nil { t.Fatal(err) } @@ -357,13 +637,21 @@ func TestByteaOutputFormats(t *testing.T) { if err != nil { t.Fatal(err) } + if stmt != nil { + err = stmt.Close() + if err != nil { + t.Fatal(err) + } + } if !bytes.Equal(data, expectedData) { t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData) } } - testByteaOutputFormat("hex") - testByteaOutputFormat("escape") + testByteaOutputFormat("hex", false) + testByteaOutputFormat("escape", false) + testByteaOutputFormat("hex", true) + testByteaOutputFormat("escape", true) } func TestAppendEncodedText(t *testing.T) { @@ -371,15 +659,13 @@ func TestAppendEncodedText(t *testing.T) { buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, int64(10)) buf = append(buf, '\t') - buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, float32(42.0000000001)) - buf = append(buf, '\t') buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, 42.0000000001) buf = append(buf, '\t') buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, "hello\tworld") buf = append(buf, '\t') buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255}) - if string(buf) != "10\t42\t42.0000000001\thello\\tworld\t\\\\x0080ff" { + if string(buf) != "10\t42.0000000001\thello\\tworld\t\\\\x0080ff" { t.Fatal(string(buf)) } } diff --git a/Godeps/_workspace/src/github.com/lib/pq/error.go b/Godeps/_workspace/src/github.com/lib/pq/error.go index 0a49364..b4bb44c 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/error.go +++ b/Godeps/_workspace/src/github.com/lib/pq/error.go @@ -459,6 +459,19 @@ func errorf(s string, args ...interface{}) { panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) } +func errRecoverNoErrBadConn(err *error) { + e := recover() + if e == nil { + // Do nothing + return + } + var ok bool + *err, ok = e.(error) + if !ok { + *err = fmt.Errorf("pq: unexpected error: %#v", e) + } +} + func (c *conn) errRecover(err *error) { e := recover() switch v := e.(type) { diff --git a/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go index 8e61e69..c9c108f 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/hstore/hstore_test.go @@ -2,9 +2,10 @@ package hstore import ( "database/sql" - _ "github.com/lib/pq" "os" "testing" + + _ "github.com/lib/pq" ) type Fatalistic interface { diff --git a/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go b/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go index 34496f4..5bc99f5 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go +++ b/Godeps/_workspace/src/github.com/lib/pq/listen_example/doc.go @@ -18,11 +18,11 @@ mechanism to avoid polling the database while waiting for more work to arrive. package main import ( - "github.com/lib/pq" - "database/sql" "fmt" "time" + + "github.com/lib/pq" ) func doWork(db *sql.DB, work int64) { diff --git a/Godeps/_workspace/src/github.com/lib/pq/notify.go b/Godeps/_workspace/src/github.com/lib/pq/notify.go index e3b08d5..8cad578 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/notify.go +++ b/Godeps/_workspace/src/github.com/lib/pq/notify.go @@ -6,7 +6,6 @@ package pq import ( "errors" "fmt" - "io" "sync" "sync/atomic" "time" @@ -87,12 +86,16 @@ func NewListenerConn(name string, notificationChan chan<- *Notification) (*Liste // Returns an error if an unrecoverable error has occurred and the ListenerConn // should be abandoned. func (l *ListenerConn) acquireSenderLock() error { - l.connectionLock.Lock() - defer l.connectionLock.Unlock() - if l.err != nil { - return l.err - } + // we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery l.senderLock.Lock() + + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + if err != nil { + l.senderLock.Unlock() + return err + } return nil } @@ -125,7 +128,7 @@ func (l *ListenerConn) setState(newState int32) bool { // away or should be discarded because we couldn't agree on the state with the // server backend. func (l *ListenerConn) listenerConnLoop() (err error) { - defer l.cn.errRecover(&err) + defer errRecoverNoErrBadConn(&err) r := &readBuf{} for { @@ -140,6 +143,9 @@ func (l *ListenerConn) listenerConnLoop() (err error) { // about the scratch buffer being overwritten. l.notificationChan <- recvNotification(r) + case 'T', 'D': + // only used by tests; ignore + case 'E': // We might receive an ErrorResponse even when not in a query; it // is expected that the server will close the connection after @@ -238,7 +244,7 @@ func (l *ListenerConn) Ping() error { // The caller must be holding senderLock (see acquireSenderLock and // releaseSenderLock). func (l *ListenerConn) sendSimpleQuery(q string) (err error) { - defer l.cn.errRecover(&err) + defer errRecoverNoErrBadConn(&err) // must set connection state before sending the query if !l.setState(connStateExpectResponse) { @@ -247,8 +253,10 @@ func (l *ListenerConn) sendSimpleQuery(q string) (err error) { // Can't use l.cn.writeBuf here because it uses the scratch buffer which // might get overwritten by listenerConnLoop. - data := writeBuf([]byte("Q\x00\x00\x00\x00")) - b := &data + b := &writeBuf{ + buf: []byte("Q\x00\x00\x00\x00"), + pos: 1, + } b.string(q) l.cn.send(b) @@ -277,13 +285,13 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) { // We can't know what state the protocol is in, so we need to abandon // this connection. l.connectionLock.Lock() - defer l.connectionLock.Unlock() // Set the error pointer if it hasn't been set already; see // listenerConnMain. if l.err == nil { l.err = err } - l.cn.Close() + l.connectionLock.Unlock() + l.cn.c.Close() return false, err } @@ -292,8 +300,11 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) { m, ok := <-l.replyChan if !ok { // We lost the connection to server, don't bother waiting for a - // a response. - return false, io.EOF + // a response. err should have been set already. + l.connectionLock.Lock() + err := l.err + l.connectionLock.Unlock() + return false, err } switch m.typ { case 'Z': @@ -320,12 +331,15 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) { func (l *ListenerConn) Close() error { l.connectionLock.Lock() - defer l.connectionLock.Unlock() if l.err != nil { + l.connectionLock.Unlock() return errListenerConnClosed } l.err = errListenerConnClosed - return l.cn.Close() + l.connectionLock.Unlock() + // We can't send anything on the connection without holding senderLock. + // Simply close the net.Conn to wake up everyone operating on it. + return l.cn.c.Close() } // Err() returns the reason the connection was closed. It is not safe to call diff --git a/Godeps/_workspace/src/github.com/lib/pq/notify_test.go b/Godeps/_workspace/src/github.com/lib/pq/notify_test.go index ae1208d..fe8941a 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/notify_test.go +++ b/Godeps/_workspace/src/github.com/lib/pq/notify_test.go @@ -5,6 +5,9 @@ import ( "fmt" "io" "os" + "runtime" + "sync" + "sync/atomic" "testing" "time" ) @@ -43,7 +46,7 @@ func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEven } return nil case <-time.After(1500 * time.Millisecond): - return fmt.Errorf("timeout") + panic("expectEvent timeout") } } @@ -210,6 +213,75 @@ func TestConnPing(t *testing.T) { } } +// Test for deadlock where a query fails while another one is queued +func TestConnExecDeadlock(t *testing.T) { + l, _ := newTestListenerConn(t) + defer l.Close() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + l.ExecSimpleQuery("SELECT pg_sleep(60)") + wg.Done() + }() + runtime.Gosched() + go func() { + l.ExecSimpleQuery("SELECT 1") + wg.Done() + }() + // give the two goroutines some time to get into position + runtime.Gosched() + // calls Close on the net.Conn; equivalent to a network failure + l.Close() + + var done int32 = 0 + go func() { + time.Sleep(10 * time.Second) + if atomic.LoadInt32(&done) != 1 { + panic("timed out") + } + }() + wg.Wait() + atomic.StoreInt32(&done, 1) +} + +// Test for ListenerConn being closed while a slow query is executing +func TestListenerConnCloseWhileQueryIsExecuting(t *testing.T) { + l, _ := newTestListenerConn(t) + defer l.Close() + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + sent, err := l.ExecSimpleQuery("SELECT pg_sleep(60)") + if sent { + panic("expected sent=false") + } + // could be any of a number of errors + if err == nil { + panic("expected error") + } + wg.Done() + }() + // give the above goroutine some time to get into position + runtime.Gosched() + err := l.Close() + if err != nil { + t.Fatal(err) + } + var done int32 = 0 + go func() { + time.Sleep(10 * time.Second) + if atomic.LoadInt32(&done) != 1 { + panic("timed out") + } + }() + wg.Wait() + atomic.StoreInt32(&done, 1) +} + func TestNotifyExtra(t *testing.T) { db := openTestConn(t) defer db.Close() diff --git a/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go b/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go index f16a51c..cd4aea8 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go +++ b/Godeps/_workspace/src/github.com/lib/pq/oid/gen.go @@ -5,12 +5,12 @@ package main import ( + "database/sql" "fmt" "log" "os" "os/exec" - "database/sql" _ "github.com/lib/pq" ) diff --git a/Godeps/_workspace/src/github.com/lib/pq/url.go b/Godeps/_workspace/src/github.com/lib/pq/url.go index b83e806..9bac95c 100644 --- a/Godeps/_workspace/src/github.com/lib/pq/url.go +++ b/Godeps/_workspace/src/github.com/lib/pq/url.go @@ -34,7 +34,7 @@ func ParseURL(url string) (string, error) { return "", err } - if u.Scheme != "postgres" { + if u.Scheme != "postgres" && u.Scheme != "postgresql" { return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) }