Skip to content

Commit 2997d16

Browse files
committed
Implement driver option binary_parameters
If set, all parameters of type []byte will be sent over as binary data, which allows the driver to avoid the second round-trip to the server when not using a prepared statement proper. This also allows pgbouncer to be used with parameterized queries, which is impossible if binary_parameters has not been enabled. This patch is still missing documentation, but the code has been sitting on my hard drive for long enough. I'll commit the docs in a followup commit. Marko Tiikkaja, with some help from Chris Gilling
1 parent e93ec67 commit 2997d16

File tree

5 files changed

+300
-27
lines changed

5 files changed

+300
-27
lines changed

.travis.yml

+12-6
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,18 @@ env:
4646
- PQSSLCERTTEST_PATH=$PWD/certs
4747
- PGHOST=127.0.0.1
4848
matrix:
49-
- PGVERSION=9.4
50-
- PGVERSION=9.3
51-
- PGVERSION=9.2
52-
- PGVERSION=9.1
53-
- PGVERSION=9.0
54-
- PGVERSION=8.4
49+
- PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=yes
50+
- PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=yes
51+
- PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=yes
52+
- PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=yes
53+
- PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=yes
54+
- PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=yes
55+
- PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=no
56+
- PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=no
57+
- PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=no
58+
- PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=no
59+
- PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=no
60+
- PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=no
5561

5662
script:
5763
- go test -v ./...

conn.go

+127-19
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ type conn struct {
111111
// receiving query results from prepared statements. Only provided for
112112
// debugging.
113113
disablePreparedBinaryResult bool
114+
115+
// Whether to always send []byte parameters over as binary. Enables single
116+
// round-trip mode for non-prepared Query calls.
117+
binaryParameters bool
114118
}
115119

116120
// Handle driver-side settings in parsed connection string.
@@ -122,7 +126,7 @@ func (c *conn) handleDriverSettings(o values) (err error) {
122126
} else if value == "no" {
123127
*val = false
124128
} else {
125-
return fmt.Errorf("unrecognized value %q for disable_prepared_binary_result", value)
129+
return fmt.Errorf("unrecognized value %q for %s", value, key)
126130
}
127131
}
128132
return nil
@@ -132,6 +136,10 @@ func (c *conn) handleDriverSettings(o values) (err error) {
132136
if err != nil {
133137
return err
134138
}
139+
err = boolSetting("binary_parameters", &c.binaryParameters)
140+
if err != nil {
141+
return err
142+
}
135143
return nil
136144
}
137145

@@ -234,6 +242,7 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
234242
cn.ssl(o)
235243
cn.buf = bufio.NewReader(cn.c)
236244
cn.startup(o)
245+
237246
// reset the deadline, in case one was set (see dial)
238247
if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" {
239248
err = cn.c.SetDeadline(time.Time{})
@@ -696,18 +705,29 @@ func (cn *conn) Query(query string, args []driver.Value) (_ driver.Rows, err err
696705
return cn.simpleQuery(query)
697706
}
698707

699-
st := cn.prepareTo(query, "")
700-
st.exec(args)
701-
return &rows{
702-
cn: cn,
703-
colNames: st.colNames,
704-
colTyps: st.colTyps,
705-
colFmts: st.colFmts,
706-
}, nil
708+
if cn.binaryParameters {
709+
cn.sendBinaryModeQuery(query, args)
710+
711+
cn.readParseResponse()
712+
cn.readBindResponse()
713+
rows := &rows{cn: cn}
714+
rows.colNames, rows.colFmts, rows.colTyps = cn.readPortalDescribeResponse()
715+
cn.postExecuteWorkaround()
716+
return rows, nil
717+
} else {
718+
st := cn.prepareTo(query, "")
719+
st.exec(args)
720+
return &rows{
721+
cn: cn,
722+
colNames: st.colNames,
723+
colTyps: st.colTyps,
724+
colFmts: st.colFmts,
725+
}, nil
726+
}
707727
}
708728

709729
// Implement the optional "Execer" interface for one-shot queries
710-
func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err error) {
730+
func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) {
711731
if cn.bad {
712732
return nil, driver.ErrBadConn
713733
}
@@ -721,17 +741,26 @@ func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err er
721741
return r, err
722742
}
723743

724-
// Use the unnamed statement to defer planning until bind
725-
// time, or else value-based selectivity estimates cannot be
726-
// used.
727-
st := cn.prepareTo(query, "")
744+
if cn.binaryParameters {
745+
cn.sendBinaryModeQuery(query, args)
728746

729-
r, err := st.Exec(args)
730-
if err != nil {
731-
panic(err)
747+
cn.readParseResponse()
748+
cn.readBindResponse()
749+
cn.readPortalDescribeResponse()
750+
cn.postExecuteWorkaround()
751+
res, _, err = cn.readExecuteResponse("Execute")
752+
return res, err
753+
} else {
754+
// Use the unnamed statement to defer planning until bind
755+
// time, or else value-based selectivity estimates cannot be
756+
// used.
757+
st := cn.prepareTo(query, "")
758+
r, err := st.Exec(args)
759+
if err != nil {
760+
panic(err)
761+
}
762+
return r, err
732763
}
733-
734-
return r, err
735764
}
736765

737766
func (cn *conn) send(m *writeBuf) {
@@ -1026,6 +1055,8 @@ func isDriverSetting(key string) bool {
10261055
return true
10271056
case "disable_prepared_binary_result":
10281057
return true
1058+
case "binary_parameters":
1059+
return true
10291060

10301061
default:
10311062
return false
@@ -1375,6 +1406,65 @@ func md5s(s string) string {
13751406
return fmt.Sprintf("%x", h.Sum(nil))
13761407
}
13771408

1409+
func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) {
1410+
if len(args) >= 65536 {
1411+
errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args))
1412+
}
1413+
1414+
b := cn.writeBuf('P')
1415+
b.byte(0) // unnamed statement
1416+
b.string(query)
1417+
b.int16(0)
1418+
1419+
b.next('B')
1420+
b.int16(0) // unnamed portal and statement
1421+
1422+
// Do one pass over the parameters to see if we're going to send any of
1423+
// them over in binary. If we are, create a paramFormats array at the
1424+
// same time.
1425+
var paramFormats []int
1426+
for i, x := range args {
1427+
_, ok := x.([]byte)
1428+
if ok {
1429+
if paramFormats == nil {
1430+
paramFormats = make([]int, len(args))
1431+
}
1432+
paramFormats[i] = 1
1433+
}
1434+
}
1435+
if paramFormats == nil {
1436+
b.int16(0)
1437+
} else {
1438+
b.int16(len(paramFormats))
1439+
for _, x := range paramFormats {
1440+
b.int16(x)
1441+
}
1442+
}
1443+
1444+
b.int16(len(args))
1445+
for _, x := range args {
1446+
if x == nil {
1447+
b.int32(-1)
1448+
} else {
1449+
datum := binaryEncode(&cn.parameterStatus, x)
1450+
b.int32(len(datum))
1451+
b.bytes(datum)
1452+
}
1453+
}
1454+
b.int16(0)
1455+
1456+
b.next('D')
1457+
b.byte('P')
1458+
b.byte(0) // unnamed portal
1459+
1460+
b.next('E')
1461+
b.byte(0)
1462+
b.int32(0)
1463+
1464+
b.next('S')
1465+
cn.send(b)
1466+
}
1467+
13781468
func (c *conn) processParameterStatus(r *readBuf) {
13791469
var err error
13801470

@@ -1457,6 +1547,24 @@ func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames [
14571547
}
14581548
}
14591549

1550+
func (cn *conn) readPortalDescribeResponse() (colNames []string, colFmts []format, colTyps []oid.Oid) {
1551+
t, r := cn.recv1()
1552+
switch t {
1553+
case 'T':
1554+
return parsePortalRowDescribe(r)
1555+
case 'n':
1556+
return nil, nil, nil
1557+
case 'E':
1558+
err := parseError(r)
1559+
cn.readReadyForQuery()
1560+
panic(err)
1561+
default:
1562+
cn.bad = true
1563+
errorf("unexpected Describe response %q", t)
1564+
}
1565+
panic("not reached")
1566+
}
1567+
14601568
func (cn *conn) readBindResponse() {
14611569
t, r := cn.recv1()
14621570
switch t {

conn_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"os"
99
"reflect"
10+
"strings"
1011
"testing"
1112
"time"
1213
)
@@ -15,6 +16,17 @@ type Fatalistic interface {
1516
Fatal(args ...interface{})
1617
}
1718

19+
func forceBinaryParameters() bool {
20+
bp := os.Getenv("PQTEST_BINARY_PARAMETERS")
21+
if bp == "yes" {
22+
return true
23+
} else if bp == "" || bp == "no" {
24+
return false
25+
} else {
26+
panic("unexpected value for PQTEST_BINARY_PARAMETERS")
27+
}
28+
}
29+
1830
func openTestConnConninfo(conninfo string) (*sql.DB, error) {
1931
defaultTo := func(envvar string, value string) {
2032
if os.Getenv(envvar) == "" {
@@ -24,6 +36,13 @@ func openTestConnConninfo(conninfo string) (*sql.DB, error) {
2436
defaultTo("PGDATABASE", "pqgotest")
2537
defaultTo("PGSSLMODE", "disable")
2638
defaultTo("PGCONNECT_TIMEOUT", "20")
39+
40+
if forceBinaryParameters() &&
41+
!strings.HasPrefix(conninfo, "postgres://") &&
42+
!strings.HasPrefix(conninfo, "postgresql://") {
43+
conninfo = conninfo + " binary_parameters=yes"
44+
}
45+
2746
return sql.Open("postgres", conninfo)
2847
}
2948

encode.go

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ import (
1515
"github.com/lib/pq/oid"
1616
)
1717

18+
func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte {
19+
switch v := x.(type) {
20+
case []byte:
21+
return v
22+
default:
23+
return encode(parameterStatus, x, oid.T_unknown)
24+
}
25+
panic("not reached")
26+
}
27+
1828
func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte {
1929
switch v := x.(type) {
2030
case int64:

0 commit comments

Comments
 (0)