Update pg dependency to latest

This commit is contained in:
Dan Sosedoff 2015-07-14 22:11:02 -05:00
parent d9ada2e58a
commit d35f58a2b5
19 changed files with 1216 additions and 321 deletions

4
Godeps/Godeps.json generated
View File

@ -28,8 +28,8 @@
}, },
{ {
"ImportPath": "github.com/lib/pq", "ImportPath": "github.com/lib/pq",
"Comment": "go1.0-cutoff-13-g19eeca3", "Comment": "go1.0-cutoff-56-gdc50b6a",
"Rev": "19eeca3e30d2577b1761db471ec130810e67f532" "Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
}, },
{ {
"ImportPath": "github.com/mitchellh/go-homedir", "ImportPath": "github.com/mitchellh/go-homedir",

View File

@ -44,13 +44,20 @@ env:
- PGUSER=postgres - PGUSER=postgres
- PQGOSSLTESTS=1 - PQGOSSLTESTS=1
- PQSSLCERTTEST_PATH=$PWD/certs - PQSSLCERTTEST_PATH=$PWD/certs
- PGHOST=127.0.0.1
matrix: matrix:
- PGVERSION=9.4 - PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.3 - PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.2 - PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.1 - PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.0 - PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=8.4 - 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: script:
- go test -v ./... - go test -v ./...

View File

@ -57,10 +57,13 @@ code still exists in here.
* Brad Fitzpatrick (bradfitz) * Brad Fitzpatrick (bradfitz)
* Charlie Melbye (cmelbye) * Charlie Melbye (cmelbye)
* Chris Bandy (cbandy) * Chris Bandy (cbandy)
* Chris Gilling (cgilling)
* Chris Walsh (cwds) * Chris Walsh (cwds)
* Dan Sosedoff (sosedoff) * Dan Sosedoff (sosedoff)
* Daniel Farina (fdr) * Daniel Farina (fdr)
* Eric Chlebek (echlebek) * Eric Chlebek (echlebek)
* Eric Garrido (minusnine)
* Eric Urban (hydrogen18)
* Everyone at The Go Team * Everyone at The Go Team
* Evan Shaw (edsrzf) * Evan Shaw (edsrzf)
* Ewan Chou (coocood) * Ewan Chou (coocood)
@ -94,5 +97,7 @@ code still exists in here.
* Ryan Smith (ryandotsmith) * Ryan Smith (ryandotsmith)
* Samuel Stauffer (samuel) * Samuel Stauffer (samuel)
* Timothée Peignier (cyberdelia) * Timothée Peignier (cyberdelia)
* Travis Cline (tmc)
* TruongSinh Tran-Nguyen (truongsinh) * TruongSinh Tran-Nguyen (truongsinh)
* Yaismel Miranda (ympons)
* notedit (notedit) * notedit (notedit)

View File

@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"database/sql" "database/sql"
"database/sql/driver" "database/sql/driver"
"github.com/lib/pq/oid"
"io" "io"
"math/rand" "math/rand"
"net" "net"
@ -17,6 +16,8 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/lib/pq/oid"
) )
var ( var (
@ -324,7 +325,7 @@ var testIntBytes = []byte("1234")
func BenchmarkDecodeInt64(b *testing.B) { func BenchmarkDecodeInt64(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testIntBytes, oid.T_int8) decode(&parameterStatus{}, testIntBytes, oid.T_int8, formatText)
} }
} }
@ -332,7 +333,7 @@ var testFloatBytes = []byte("3.14159")
func BenchmarkDecodeFloat64(b *testing.B) { func BenchmarkDecodeFloat64(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testFloatBytes, oid.T_float8) decode(&parameterStatus{}, testFloatBytes, oid.T_float8, formatText)
} }
} }
@ -340,7 +341,7 @@ var testBoolBytes = []byte{'t'}
func BenchmarkDecodeBool(b *testing.B) { func BenchmarkDecodeBool(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testBoolBytes, oid.T_bool) decode(&parameterStatus{}, 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) { func BenchmarkDecodeTimestamptz(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz) decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText)
} }
} }
@ -370,7 +371,7 @@ func BenchmarkDecodeTimestamptzMultiThread(b *testing.B) {
f := func(wg *sync.WaitGroup, loops int) { f := func(wg *sync.WaitGroup, loops int) {
defer wg.Done() defer wg.Done()
for i := 0; i < loops; i++ { for i := 0; i < loops; i++ {
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz) decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText)
} }
} }

View File

@ -3,6 +3,7 @@ package pq
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"github.com/lib/pq/oid" "github.com/lib/pq/oid"
) )
@ -46,28 +47,44 @@ func (b *readBuf) byte() byte {
return b.next(1)[0] return b.next(1)[0]
} }
type writeBuf []byte type writeBuf struct {
buf []byte
pos int
}
func (b *writeBuf) int32(n int) { func (b *writeBuf) int32(n int) {
x := make([]byte, 4) x := make([]byte, 4)
binary.BigEndian.PutUint32(x, uint32(n)) binary.BigEndian.PutUint32(x, uint32(n))
*b = append(*b, x...) b.buf = append(b.buf, x...)
} }
func (b *writeBuf) int16(n int) { func (b *writeBuf) int16(n int) {
x := make([]byte, 2) x := make([]byte, 2)
binary.BigEndian.PutUint16(x, uint16(n)) binary.BigEndian.PutUint16(x, uint16(n))
*b = append(*b, x...) b.buf = append(b.buf, x...)
} }
func (b *writeBuf) string(s string) { func (b *writeBuf) string(s string) {
*b = append(*b, (s + "\000")...) b.buf = append(b.buf, (s + "\000")...)
} }
func (b *writeBuf) byte(c byte) { func (b *writeBuf) byte(c byte) {
*b = append(*b, c) b.buf = append(b.buf, c)
} }
func (b *writeBuf) bytes(v []byte) { 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)
} }

View File

@ -10,7 +10,6 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/lib/pq/oid"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@ -22,6 +21,8 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
"github.com/lib/pq/oid"
) )
// Common error types // Common error types
@ -105,12 +106,49 @@ type conn struct {
// If true, this connection is bad and all public-facing functions should // If true, this connection is bad and all public-facing functions should
// return ErrBadConn. // return ErrBadConn.
bad bool 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 { func (c *conn) writeBuf(b byte) *writeBuf {
c.scratch[0] = b c.scratch[0] = b
w := writeBuf(c.scratch[:5]) return &writeBuf{
return &w buf: c.scratch[:5],
pos: 1,
}
} }
func Open(name string) (_ driver.Conn, err error) { 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) { func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
defer func() { // Handle any panics during connection initialization. Note that we
// Handle any panics during connection initialization. Note that we // specifically do *not* want to use errRecover(), as that would turn any
// specifically do *not* want to use errRecover(), as that would turn // connection errors into ErrBadConns, hiding the real error message from
// any connection errors into ErrBadConns, hiding the real error // the user.
// message from the user. defer errRecoverNoErrBadConn(&err)
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)
}
}()
o := make(values) o := make(values)
@ -151,7 +178,7 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
o.Set(k, v) o.Set(k, v)
} }
if strings.HasPrefix(name, "postgres://") { if strings.HasPrefix(name, "postgres://") || strings.HasPrefix(name, "postgresql://") {
name, err = ParseURL(name) name, err = ParseURL(name)
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
cn := &conn{c: c} cn.c, err = dial(d, o)
if err != nil {
return nil, err
}
cn.ssl(o) cn.ssl(o)
cn.buf = bufio.NewReader(cn.c) cn.buf = bufio.NewReader(cn.c)
cn.startup(o) cn.startup(o)
// reset the deadline, in case one was set (see dial) // 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 return cn, err
} }
func dial(d Dialer, o values) (net.Conn, error) { func dial(d Dialer, o values) (net.Conn, error) {
ntw, addr := network(o) ntw, addr := network(o)
// SSL is not necessary or supported over UNIX domain sockets
timeout := o.Get("connect_timeout") if ntw == "unix" {
o["sslmode"] = "disable"
}
// Zero or not specified means wait indefinitely. // 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) seconds, err := strconv.ParseInt(timeout, 10, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err) 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") _, commandTag, err := cn.simpleExec("COMMIT")
if err != nil { if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err return err
} }
if commandTag != "COMMIT" { if commandTag != "COMMIT" {
@ -455,6 +494,9 @@ func (cn *conn) Rollback() (err error) {
cn.checkIsInTransaction(true) cn.checkIsInTransaction(true)
_, commandTag, err := cn.simpleExec("ROLLBACK") _, commandTag, err := cn.simpleExec("ROLLBACK")
if err != nil { if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err return err
} }
if commandTag != "ROLLBACK" { 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) defer cn.errRecover(&err)
st := &stmt{cn: cn, name: ""} st := &stmt{cn: cn, name: ""}
@ -515,7 +557,13 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
cn.bad = true cn.bad = true
errorf("unexpected message %q in simple query execution", t) 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': case 'Z':
cn.processReadyForQuery(r) cn.processReadyForQuery(r)
// done // done
@ -534,8 +582,8 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
case 'T': case 'T':
// res might be non-nil here if we received a previous // res might be non-nil here if we received a previous
// CommandComplete, but that's fine; just overwrite it // CommandComplete, but that's fine; just overwrite it
res = &rows{st: st} res = &rows{cn: cn}
st.cols, st.rowTyps = parseMeta(r) res.colNames, res.colFmts, res.colTyps = parsePortalRowDescribe(r)
// To work around a bug in QueryRow in Go 1.2 and earlier, wait // To work around a bug in QueryRow in Go 1.2 and earlier, wait
// until the first DataRow has been received. // 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} st := &stmt{cn: cn, name: stmtName}
b := cn.writeBuf('P') b := cn.writeBuf('P')
b.string(st.name) b.string(st.name)
b.string(q) b.string(q)
b.int16(0) b.int16(0)
cn.send(b)
b = cn.writeBuf('D') b.next('D')
b.byte('S') b.byte('S')
b.string(st.name) b.string(st.name)
b.next('S')
cn.send(b) cn.send(b)
cn.send(cn.writeBuf('S')) cn.readParseResponse()
st.paramTyps, st.colNames, st.colTyps = cn.readStatementDescribeResponse()
for { st.colFmts, st.colFmtData = decideColumnFormats(st.colTyps, cn.disablePreparedBinaryResult)
t, r := cn.recv1() cn.readReadyForQuery()
switch t { return st
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)
}
}
} }
func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { 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") { if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") {
return cn.prepareCopyIn(q) return cn.prepareCopyIn(q)
} }
return cn.prepareTo(q, cn.gname()) return cn.prepareTo(q, cn.gname()), nil
} }
func (cn *conn) Close() (err error) { 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) return cn.simpleQuery(query)
} }
st, err := cn.prepareTo(query, "") if cn.binaryParameters {
if err != nil { cn.sendBinaryModeQuery(query, args)
panic(err)
}
st.exec(args) cn.readParseResponse()
return &rows{st: st}, nil 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 // 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 { if cn.bad {
return nil, driver.ErrBadConn return nil, driver.ErrBadConn
} }
@ -654,32 +741,42 @@ func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err er
return r, err return r, err
} }
// Use the unnamed statement to defer planning until bind if cn.binaryParameters {
// time, or else value-based selectivity estimates cannot be cn.sendBinaryModeQuery(query, args)
// used.
st, err := cn.prepareTo(query, "")
if err != nil {
panic(err)
}
r, err := st.Exec(args) cn.readParseResponse()
if err != nil { cn.readBindResponse()
panic(err) 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) { func (cn *conn) send(m *writeBuf) {
b := (*m)[1:] _, err := cn.c.Write(m.wrap())
binary.BigEndian.PutUint32(b, uint32(len(b))) if err != nil {
panic(err)
}
}
if (*m)[0] == 0 { func (cn *conn) sendStartupPacket(m *writeBuf) {
*m = b // sanity check
if m.buf[0] != 0 {
panic("oops")
} }
_, err := cn.c.Write(*m) _, err := cn.c.Write((m.wrap())[1:])
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -819,7 +916,7 @@ func (cn *conn) ssl(o values) {
w := cn.writeBuf(0) w := cn.writeBuf(0)
w.int32(80877103) w.int32(80877103)
cn.send(w) cn.sendStartupPacket(w)
b := cn.scratch[:1] b := cn.scratch[:1]
_, err := io.ReadFull(cn.c, b) _, err := io.ReadFull(cn.c, b)
@ -956,6 +1053,10 @@ func isDriverSetting(key string) bool {
return true return true
case "connect_timeout": case "connect_timeout":
return true return true
case "disable_prepared_binary_result":
return true
case "binary_parameters":
return true
default: default:
return false return false
@ -983,7 +1084,7 @@ func (cn *conn) startup(o values) {
w.string(v) w.string(v)
} }
w.string("") w.string("")
cn.send(w) cn.sendStartupPacket(w)
for { for {
t, r := cn.recv() 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 { type stmt struct {
cn *conn cn *conn
name string name string
cols []string colNames []string
rowTyps []oid.Oid colFmts []format
paramTyps []oid.Oid colFmtData []byte
closed bool colTyps []oid.Oid
paramTyps []oid.Oid
closed bool
} }
func (st *stmt) Close() (err error) { 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) defer st.cn.errRecover(&err)
st.exec(v) 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) { 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) defer st.cn.errRecover(&err)
st.exec(v) st.exec(v)
res, _, err = st.cn.readExecuteResponse("simple query")
for { return res, err
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)
}
}
} }
func (st *stmt) exec(v []driver.Value) { 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)) errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps))
} }
w := st.cn.writeBuf('B') cn := st.cn
w.string("") w := cn.writeBuf('B')
w.byte(0) // unnamed portal
w.string(st.name) w.string(st.name)
w.int16(0)
w.int16(len(v)) if cn.binaryParameters {
for i, x := range v { cn.sendBinaryParameters(w, v)
if x == nil { } else {
w.int32(-1) w.int16(0)
} else { w.int16(len(v))
b := encode(&st.cn.parameterStatus, x, st.paramTyps[i]) for i, x := range v {
w.int32(len(b)) if x == nil {
w.bytes(b) w.int32(-1)
} else {
b := encode(&cn.parameterStatus, x, st.paramTyps[i])
w.int32(len(b))
w.bytes(b)
}
} }
} }
w.int16(0) w.bytes(st.colFmtData)
st.cn.send(w)
w = st.cn.writeBuf('E') w.next('E')
w.string("") w.byte(0)
w.int32(0) w.int32(0)
st.cn.send(w)
st.cn.send(st.cn.writeBuf('S')) w.next('S')
cn.send(w)
var err error cn.readBindResponse()
for { cn.postExecuteWorkaround()
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)
}
}
// 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 { func (st *stmt) NumInput() int {
@ -1260,9 +1316,12 @@ func (cn *conn) parseComplete(commandTag string) (driver.Result, string) {
} }
type rows struct { type rows struct {
st *stmt cn *conn
done bool colNames []string
rb readBuf colTyps []oid.Oid
colFmts []format
done bool
rb readBuf
} }
func (rs *rows) Close() error { func (rs *rows) Close() error {
@ -1280,7 +1339,7 @@ func (rs *rows) Close() error {
} }
func (rs *rows) Columns() []string { func (rs *rows) Columns() []string {
return rs.st.cols return rs.colNames
} }
func (rs *rows) Next(dest []driver.Value) (err error) { 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 return io.EOF
} }
conn := rs.st.cn conn := rs.cn
if conn.bad { if conn.bad {
return driver.ErrBadConn return driver.ErrBadConn
} }
@ -1319,7 +1378,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) {
dest[i] = nil dest[i] = nil
continue 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 return
default: default:
@ -1352,6 +1411,68 @@ func md5s(s string) string {
return fmt.Sprintf("%x", h.Sum(nil)) 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) { func (c *conn) processParameterStatus(r *readBuf) {
var err error var err error
@ -1381,15 +1502,167 @@ func (c *conn) processReadyForQuery(r *readBuf) {
c.txnStatus = transactionStatus(r.byte()) 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() n := r.int16()
cols = make([]string, n) colNames = make([]string, n)
rowTyps = make([]oid.Oid, n) colTyps = make([]oid.Oid, n)
for i := range cols { for i := range colNames {
cols[i] = r.string() colNames[i] = r.string()
r.next(6) r.next(6)
rowTyps[i] = r.oid() colTyps[i] = r.oid()
r.next(8) 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 return
} }

View File

@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"reflect" "reflect"
"strings"
"testing" "testing"
"time" "time"
) )
@ -15,21 +16,31 @@ type Fatalistic interface {
Fatal(args ...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) { func openTestConnConninfo(conninfo string) (*sql.DB, error) {
datname := os.Getenv("PGDATABASE") defaultTo := func(envvar string, value string) {
sslmode := os.Getenv("PGSSLMODE") if os.Getenv(envvar) == "" {
timeout := os.Getenv("PGCONNECT_TIMEOUT") os.Setenv(envvar, value)
}
if datname == "" {
os.Setenv("PGDATABASE", "pqgotest")
} }
defaultTo("PGDATABASE", "pqgotest")
defaultTo("PGSSLMODE", "disable")
defaultTo("PGCONNECT_TIMEOUT", "20")
if sslmode == "" { if forceBinaryParameters() &&
os.Setenv("PGSSLMODE", "disable") !strings.HasPrefix(conninfo, "postgres://") &&
} !strings.HasPrefix(conninfo, "postgresql://") {
conninfo = conninfo + " binary_parameters=yes"
if timeout == "" {
os.Setenv("PGCONNECT_TIMEOUT", "20")
} }
return sql.Open("postgres", conninfo) return sql.Open("postgres", conninfo)
@ -106,18 +117,22 @@ func TestCommitInFailedTransaction(t *testing.T) {
} }
func TestOpenURL(t *testing.T) { func TestOpenURL(t *testing.T) {
db, err := openTestConnConninfo("postgres://") testURL := func(url string) {
if err != nil { db, err := openTestConnConninfo(url)
t.Fatal(err) 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() testURL("postgres://")
// database/sql might not call our Open at all unless we do something with testURL("postgresql://")
// the connection
txn, err := db.Begin()
if err != nil {
t.Fatal(err)
}
txn.Rollback()
} }
func TestExec(t *testing.T) { func TestExec(t *testing.T) {
@ -342,6 +357,7 @@ func TestEncodeDecode(t *testing.T) {
'2000-1-1 01:02:03.04-7'::timestamptz, '2000-1-1 01:02:03.04-7'::timestamptz,
0::boolean, 0::boolean,
123, 123,
-321,
3.14::float8 3.14::float8
WHERE WHERE
E'\\000\\001\\002'::bytea = $1 E'\\000\\001\\002'::bytea = $1
@ -370,9 +386,9 @@ func TestEncodeDecode(t *testing.T) {
var got2 string var got2 string
var got3 = sql.NullInt64{Valid: true} var got3 = sql.NullInt64{Valid: true}
var got4 time.Time 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -401,8 +417,12 @@ func TestEncodeDecode(t *testing.T) {
t.Fatalf("expected 123, got %d", got6) t.Fatalf("expected 123, got %d", got6)
} }
if got7 != float64(3.14) { if got7 != int64(-321) {
t.Fatalf("expected 3.14, got %f", got7) t.Fatalf("expected -321, got %d", got7)
}
if got8 != float64(3.14) {
t.Fatalf("expected 3.14, got %f", got8)
} }
} }

View File

@ -150,6 +150,8 @@ func (ci *copyin) resploop() {
switch t { switch t {
case 'C': case 'C':
// complete // complete
case 'N':
// NoticeResponse
case 'Z': case 'Z':
ci.cn.processReadyForQuery(&r) ci.cn.processReadyForQuery(&r)
ci.done <- true ci.done <- true

View File

@ -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) { func TestCopyInTypes(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()
@ -307,12 +387,14 @@ func TestCopyRespLoopConnectionError(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// We have to try and send something over, since postgres won't process if getServerVersion(t, db) < 90500 {
// SIGTERMs while it's waiting for CopyData/CopyEnd messages; see // We have to try and send something over, since postgres before
// tcop/postgres.c. // version 9.5 won't process SIGTERMs while it's waiting for
_, err = stmt.Exec(1) // CopyData/CopyEnd messages; see tcop/postgres.c.
if err != nil { _, err = stmt.Exec(1)
t.Fatal(err) if err != nil {
t.Fatal(err)
}
} }
_, err = stmt.Exec() _, err = stmt.Exec()
if err == nil { if err == nil {

View File

@ -5,8 +5,9 @@ In most cases clients will use the database/sql package instead of
using this package directly. For example: using this package directly. For example:
import ( import (
_ "github.com/lib/pq"
"database/sql" "database/sql"
_ "github.com/lib/pq"
) )
func main() { func main() {

View File

@ -3,24 +3,34 @@ package pq
import ( import (
"bytes" "bytes"
"database/sql/driver" "database/sql/driver"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/lib/pq/oid"
"math" "math"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "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 { func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte {
switch v := x.(type) { switch v := x.(type) {
case int64: case int64:
return []byte(fmt.Sprintf("%d", v)) return strconv.AppendInt(nil, v, 10)
case float32:
return []byte(fmt.Sprintf("%.9f", v))
case float64: case float64:
return []byte(fmt.Sprintf("%.17f", v)) return strconv.AppendFloat(nil, v, 'f', -1, 64)
case []byte: case []byte:
if pgtypOid == oid.T_bytea { if pgtypOid == oid.T_bytea {
return encodeBytea(parameterStatus.serverVersion, v) return encodeBytea(parameterStatus.serverVersion, v)
@ -34,7 +44,7 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [
return []byte(v) return []byte(v)
case bool: case bool:
return []byte(fmt.Sprintf("%t", v)) return strconv.AppendBool(nil, v)
case time.Time: case time.Time:
return formatTs(v) return formatTs(v)
@ -45,7 +55,33 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [
panic("not reached") 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 { switch typ {
case oid.T_bytea: case oid.T_bytea:
return parseBytea(s) 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) return mustParse("15:04:05-07", typ, s)
case oid.T_bool: case oid.T_bool:
return s[0] == 't' 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) i, err := strconv.ParseInt(string(s), 10, 64)
if err != nil { if err != nil {
errorf("%s", err) errorf("%s", err)
@ -86,8 +122,6 @@ func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface
switch v := x.(type) { switch v := x.(type) {
case int64: case int64:
return strconv.AppendInt(buf, v, 10) return strconv.AppendInt(buf, v, 10)
case float32:
return strconv.AppendFloat(buf, float64(v), 'f', -1, 32)
case float64: case float64:
return strconv.AppendFloat(buf, v, 'f', -1, 64) return strconv.AppendFloat(buf, v, 'f', -1, 64)
case []byte: case []byte:
@ -149,12 +183,6 @@ func appendEscapedText(buf []byte, text string) []byte {
func mustParse(f string, typ oid.Oid, s []byte) time.Time { func mustParse(f string, typ oid.Oid, s []byte) time.Time {
str := string(s) 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 // check for a 30-minute-offset timezone
if (typ == oid.T_timestamptz || typ == oid.T_timetz) && if (typ == oid.T_timestamptz || typ == oid.T_timetz) &&
str[len(str)-3] == ':' { str[len(str)-3] == ':' {
@ -212,11 +240,72 @@ func (c *locationCache) getLocation(offset int) *time.Location {
return 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 // This is a time function specific to the Postgres default DateStyle
// setting ("ISO, MDY"), the only one we currently support. This // setting ("ISO, MDY"), the only one we currently support. This
// accounts for the discrepancies between the parsing available with // accounts for the discrepancies between the parsing available with
// time.Parse and the Postgres date formatting quirks. // 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, '-') monSep := strings.IndexRune(str, '-')
// this is Gregorian year, not ISO Year // this is Gregorian year, not ISO Year
// In Gregorian system, the year 1 BC is followed by AD 1 // 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 return t
} }
// formatTs formats t as time.RFC3339Nano and appends time zone seconds if // formatTs formats t into a format postgres understands.
// needed.
func formatTs(t time.Time) (b []byte) { 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 // Need to send dates before 0001 A.D. with " BC" suffix, instead of the
// minus sign preferred by Go. // minus sign preferred by Go.
// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on // 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 bc = true
} }
b = []byte(t.Format(time.RFC3339Nano)) b = []byte(t.Format(time.RFC3339Nano))
if bc {
b = append(b, " BC"...)
}
_, offset := t.Zone() _, offset := t.Zone()
offset = offset % 60 offset = offset % 60
if offset == 0 { if offset != 0 {
return b // 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 { if bc {
offset = -offset b = append(b, " BC"...)
} }
return b
b = append(b, ':')
if offset < 10 {
b = append(b, '0')
}
return strconv.AppendInt(b, int64(offset), 10)
} }
// Parse a bytea value received from the server. Both "hex" and the legacy // 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) { func encodeBytea(serverVersion int, v []byte) (result []byte) {
if serverVersion >= 90000 { if serverVersion >= 90000 {
// Use the hex format if we know that the server supports it // 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 { } else {
// .. or resort to "escape" // .. or resort to "escape"
for _, b := range v { for _, b := range v {

View File

@ -1,12 +1,13 @@
package pq package pq
import ( import (
"github.com/lib/pq/oid"
"bytes" "bytes"
"database/sql"
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/lib/pq/oid"
) )
func TestScanTimestamp(t *testing.T) { func TestScanTimestamp(t *testing.T) {
@ -78,7 +79,11 @@ func tryParse(str string) (t time.Time, err error) {
return 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 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("", 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("", 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(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(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) { 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") 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) { func TestStringWithNul(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() 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) db := openTestConn(t)
defer db.Close() 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) db := openTestConn(t)
defer db.Close() 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) { func TestByteaOutputFormatEncoding(t *testing.T) {
input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123") input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123")
want := []byte("\\x5c78000102fffe6162636465666730313233") want := []byte("\\x5c78000102fffe6162636465666730313233")
@ -321,7 +591,7 @@ func TestByteaOutputFormats(t *testing.T) {
return return
} }
testByteaOutputFormat := func(f string) { testByteaOutputFormat := func(f string, usePrepared bool) {
expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08") expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08")
sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')" sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')"
@ -338,8 +608,18 @@ func TestByteaOutputFormats(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// use Query; QueryRow would hide the actual error var rows *sql.Rows
rows, err := txn.Query(sqlQuery) 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -357,13 +637,21 @@ func TestByteaOutputFormats(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if stmt != nil {
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
}
if !bytes.Equal(data, expectedData) { if !bytes.Equal(data, expectedData) {
t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData) t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData)
} }
} }
testByteaOutputFormat("hex") testByteaOutputFormat("hex", false)
testByteaOutputFormat("escape") testByteaOutputFormat("escape", false)
testByteaOutputFormat("hex", true)
testByteaOutputFormat("escape", true)
} }
func TestAppendEncodedText(t *testing.T) { func TestAppendEncodedText(t *testing.T) {
@ -371,15 +659,13 @@ func TestAppendEncodedText(t *testing.T) {
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, int64(10)) buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, int64(10))
buf = append(buf, '\t') buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, float32(42.0000000001))
buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, 42.0000000001) buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, 42.0000000001)
buf = append(buf, '\t') buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, "hello\tworld") buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, "hello\tworld")
buf = append(buf, '\t') buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255}) buf = appendEncodedText(&parameterStatus{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)) t.Fatal(string(buf))
} }
} }

View File

@ -459,6 +459,19 @@ func errorf(s string, args ...interface{}) {
panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) 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) { func (c *conn) errRecover(err *error) {
e := recover() e := recover()
switch v := e.(type) { switch v := e.(type) {

View File

@ -2,9 +2,10 @@ package hstore
import ( import (
"database/sql" "database/sql"
_ "github.com/lib/pq"
"os" "os"
"testing" "testing"
_ "github.com/lib/pq"
) )
type Fatalistic interface { type Fatalistic interface {

View File

@ -18,11 +18,11 @@ mechanism to avoid polling the database while waiting for more work to arrive.
package main package main
import ( import (
"github.com/lib/pq"
"database/sql" "database/sql"
"fmt" "fmt"
"time" "time"
"github.com/lib/pq"
) )
func doWork(db *sql.DB, work int64) { func doWork(db *sql.DB, work int64) {

View File

@ -6,7 +6,6 @@ package pq
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "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 // Returns an error if an unrecoverable error has occurred and the ListenerConn
// should be abandoned. // should be abandoned.
func (l *ListenerConn) acquireSenderLock() error { func (l *ListenerConn) acquireSenderLock() error {
l.connectionLock.Lock() // we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery
defer l.connectionLock.Unlock()
if l.err != nil {
return l.err
}
l.senderLock.Lock() l.senderLock.Lock()
l.connectionLock.Lock()
err := l.err
l.connectionLock.Unlock()
if err != nil {
l.senderLock.Unlock()
return err
}
return nil 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 // away or should be discarded because we couldn't agree on the state with the
// server backend. // server backend.
func (l *ListenerConn) listenerConnLoop() (err error) { func (l *ListenerConn) listenerConnLoop() (err error) {
defer l.cn.errRecover(&err) defer errRecoverNoErrBadConn(&err)
r := &readBuf{} r := &readBuf{}
for { for {
@ -140,6 +143,9 @@ func (l *ListenerConn) listenerConnLoop() (err error) {
// about the scratch buffer being overwritten. // about the scratch buffer being overwritten.
l.notificationChan <- recvNotification(r) l.notificationChan <- recvNotification(r)
case 'T', 'D':
// only used by tests; ignore
case 'E': case 'E':
// We might receive an ErrorResponse even when not in a query; it // We might receive an ErrorResponse even when not in a query; it
// is expected that the server will close the connection after // 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 // The caller must be holding senderLock (see acquireSenderLock and
// releaseSenderLock). // releaseSenderLock).
func (l *ListenerConn) sendSimpleQuery(q string) (err error) { func (l *ListenerConn) sendSimpleQuery(q string) (err error) {
defer l.cn.errRecover(&err) defer errRecoverNoErrBadConn(&err)
// must set connection state before sending the query // must set connection state before sending the query
if !l.setState(connStateExpectResponse) { 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 // Can't use l.cn.writeBuf here because it uses the scratch buffer which
// might get overwritten by listenerConnLoop. // might get overwritten by listenerConnLoop.
data := writeBuf([]byte("Q\x00\x00\x00\x00")) b := &writeBuf{
b := &data buf: []byte("Q\x00\x00\x00\x00"),
pos: 1,
}
b.string(q) b.string(q)
l.cn.send(b) 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 // We can't know what state the protocol is in, so we need to abandon
// this connection. // this connection.
l.connectionLock.Lock() l.connectionLock.Lock()
defer l.connectionLock.Unlock()
// Set the error pointer if it hasn't been set already; see // Set the error pointer if it hasn't been set already; see
// listenerConnMain. // listenerConnMain.
if l.err == nil { if l.err == nil {
l.err = err l.err = err
} }
l.cn.Close() l.connectionLock.Unlock()
l.cn.c.Close()
return false, err return false, err
} }
@ -292,8 +300,11 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
m, ok := <-l.replyChan m, ok := <-l.replyChan
if !ok { if !ok {
// We lost the connection to server, don't bother waiting for a // We lost the connection to server, don't bother waiting for a
// a response. // a response. err should have been set already.
return false, io.EOF l.connectionLock.Lock()
err := l.err
l.connectionLock.Unlock()
return false, err
} }
switch m.typ { switch m.typ {
case 'Z': case 'Z':
@ -320,12 +331,15 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
func (l *ListenerConn) Close() error { func (l *ListenerConn) Close() error {
l.connectionLock.Lock() l.connectionLock.Lock()
defer l.connectionLock.Unlock()
if l.err != nil { if l.err != nil {
l.connectionLock.Unlock()
return errListenerConnClosed return errListenerConnClosed
} }
l.err = 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 // Err() returns the reason the connection was closed. It is not safe to call

View File

@ -5,6 +5,9 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"runtime"
"sync"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@ -43,7 +46,7 @@ func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEven
} }
return nil return nil
case <-time.After(1500 * time.Millisecond): 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) { func TestNotifyExtra(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()

View File

@ -5,12 +5,12 @@
package main package main
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"database/sql"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )

View File

@ -34,7 +34,7 @@ func ParseURL(url string) (string, error) {
return "", err return "", err
} }
if u.Scheme != "postgres" { if u.Scheme != "postgres" && u.Scheme != "postgresql" {
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
} }