Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7766c3bb66 | ||
|
|
35aa91de27 | ||
|
|
3a793ed87d | ||
|
|
9cfdf4a194 | ||
|
|
aa0ffedc8c | ||
|
|
82c162d399 | ||
|
|
995d52db1b | ||
|
|
c7e58f0828 | ||
|
|
1f99576a37 | ||
|
|
91d8d3ee83 | ||
|
|
5a92c5508c | ||
|
|
73a97893e9 | ||
|
|
bc1f876fb5 | ||
|
|
eeb0fdf3a4 | ||
|
|
4fb24cb229 | ||
|
|
d35b3661a9 | ||
|
|
3e6e223ed0 | ||
|
|
8579edf1ca | ||
|
|
420d85e811 | ||
|
|
924a46894b | ||
|
|
71aec1f07a | ||
|
|
4688e62b90 | ||
|
|
232790b452 | ||
|
|
4c1bb758ce | ||
|
|
6749b2ace8 | ||
|
|
8f7fdcbdca | ||
|
|
1e50471a87 | ||
|
|
20a5728152 | ||
|
|
3170fb22cb | ||
|
|
c02d15ec34 | ||
|
|
2a4edaf08f | ||
|
|
639ca96caa | ||
|
|
fb13ce8b07 | ||
|
|
2aacab44ec | ||
|
|
279acc8af8 | ||
|
|
3765728342 | ||
|
|
68ceb212b5 | ||
|
|
e52114e969 | ||
|
|
e3b7dd013b | ||
|
|
cea3b6b43e | ||
|
|
06d39e0ab7 | ||
|
|
785108002e | ||
|
|
c12853d0b0 | ||
|
|
f1afa0ac33 | ||
|
|
5265016003 | ||
|
|
c6cb3db624 | ||
|
|
d35f58a2b5 | ||
|
|
d9ada2e58a | ||
|
|
4d6c688bf0 | ||
|
|
3eec0947d2 | ||
|
|
5979272115 | ||
|
|
5fb70ace3c | ||
|
|
5e4398ceae | ||
|
|
67a9820c98 | ||
|
|
665f79d26c | ||
|
|
6152819e45 | ||
|
|
427a08029a | ||
|
|
df7ef6d8f0 | ||
|
|
b9b646bb11 | ||
|
|
ae62f8ea3d | ||
|
|
0939632399 | ||
|
|
4b5a2b96a8 | ||
|
|
79bf430dee | ||
|
|
62a168d44f | ||
|
|
502c8dae91 | ||
|
|
afdfc54802 | ||
|
|
651b65a882 | ||
|
|
c2290acae3 | ||
|
|
e1276afd71 | ||
|
|
dea0fb8b78 | ||
|
|
faebfb3e62 | ||
|
|
6fd7b7d427 | ||
|
|
385b415676 | ||
|
|
50f0f5f6dc | ||
|
|
8889847c71 | ||
|
|
d26bb75ebc | ||
|
|
a65cca44e5 | ||
|
|
18b0260d1f |
+2
-1
@@ -1,11 +1,12 @@
|
|||||||
language: go
|
language: go
|
||||||
|
sudo: false
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.3"
|
postgresql: "9.3"
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.4.1
|
|
||||||
- 1.4.2
|
- 1.4.2
|
||||||
|
- 1.5.1
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- make setup
|
- make setup
|
||||||
|
|||||||
@@ -1,3 +1,36 @@
|
|||||||
|
## 0.6.3 - 2015-08-16
|
||||||
|
|
||||||
|
- Adds PostgreSQL password escaping in web ui, GH-96
|
||||||
|
- Adds base64 query encoding for CSV export, GH-95
|
||||||
|
- Adds automatic saving of last executed query to localStorage
|
||||||
|
- Adds request middleware to log incoming form params in debug mode
|
||||||
|
|
||||||
|
## 0.6.2 - 2015-07-15
|
||||||
|
|
||||||
|
- Adds ability to specify connection strings prefixed by `postgresql://`, [GH-92]
|
||||||
|
- Updates configuration for Heroku, [GH-89], [GH-90]
|
||||||
|
- Updates postgresql library dependency to latest, [GH-91]
|
||||||
|
- Fixes password field to not display plaintext passwords, [GH-87]
|
||||||
|
|
||||||
|
## 0.6.1 - 2015-06-18
|
||||||
|
|
||||||
|
- This release is repackage-release targeted to fix binary downloads
|
||||||
|
|
||||||
|
## 0.6.0 - 2015-05-31
|
||||||
|
|
||||||
|
- Adds ability to execute only selected SQL query in run command view, [GH-85]
|
||||||
|
- Adds ability to delete/truncate table via context meny on sidebar view
|
||||||
|
- Adds ability to export table contents to CSV via context menu on sidebar view
|
||||||
|
- Changes sidebar color scheme to a lighter and better looking one
|
||||||
|
|
||||||
|
## 0.5.3 - 2015-05-06
|
||||||
|
|
||||||
|
- Changes default server port from 8080 to 8081 to avoil conflict with RethinkDB
|
||||||
|
- Changes styles for table rows and connection settings window
|
||||||
|
- Adds highlighting styles for columns with sort order
|
||||||
|
- Adds git sha into program version output
|
||||||
|
- Add new endpoint /api/info to get build details
|
||||||
|
|
||||||
## 0.5.2 - 2015-04-13
|
## 0.5.2 - 2015-04-13
|
||||||
|
|
||||||
- Adds a new endpoint /activity that retuns active queries
|
- Adds a new endpoint /activity that retuns active queries
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
- Fork repository
|
||||||
|
- Create a new git branch
|
||||||
|
- Make changes
|
||||||
|
- Run tests: `make test`
|
||||||
|
- If you change frontend code (js/css) make sure to rebuild assets: `make assets`
|
||||||
|
- Open a PR
|
||||||
@@ -8,4 +8,5 @@ RUN go get github.com/tools/godep
|
|||||||
RUN godep restore
|
RUN godep restore
|
||||||
RUN godep go build && godep go install
|
RUN godep go build && godep go install
|
||||||
|
|
||||||
|
EXPOSE 8081
|
||||||
CMD ["pgweb", "--bind", "0.0.0.0"]
|
CMD ["pgweb", "--bind", "0.0.0.0"]
|
||||||
Generated
+2
-2
@@ -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",
|
||||||
|
|||||||
+13
-6
@@ -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 ./...
|
||||||
|
|||||||
+5
@@ -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)
|
||||||
|
|||||||
+7
-6
@@ -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(¶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) {
|
func BenchmarkDecodeFloat64(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
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) {
|
func BenchmarkDecodeBool(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
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) {
|
func BenchmarkDecodeTimestamptz(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
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) {
|
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(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz)
|
decode(¶meterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+23
-6
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+470
-197
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-27
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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
|
||||||
|
|||||||
+88
-6
@@ -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 {
|
||||||
|
|||||||
+2
-1
@@ -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() {
|
||||||
|
|||||||
+135
-34
@@ -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 {
|
||||||
|
|||||||
+300
-14
@@ -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(¶meterStatus{serverVersion: 90000}, buf, int64(10))
|
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, int64(10))
|
||||||
buf = append(buf, '\t')
|
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 = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, 42.0000000001)
|
||||||
buf = append(buf, '\t')
|
buf = append(buf, '\t')
|
||||||
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, "hello\tworld")
|
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, "hello\tworld")
|
||||||
buf = append(buf, '\t')
|
buf = append(buf, '\t')
|
||||||
buf = appendEncodedText(¶meterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255})
|
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))
|
t.Fatal(string(buf))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
@@ -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) {
|
||||||
|
|||||||
+2
-1
@@ -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 {
|
||||||
|
|||||||
+2
-2
@@ -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) {
|
||||||
|
|||||||
+30
-16
@@ -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
|
||||||
|
|||||||
+73
-1
@@ -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()
|
||||||
|
|||||||
+1
-1
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014-2015 Dan Sosedoff <dan.sosedoff@gmail.com>
|
Copyright (c) 2014-2016 Dan Sosedoff <dan.sosedoff@gmail.com>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
|||||||
@@ -40,11 +40,15 @@ build: assets
|
|||||||
@echo "You can now execute ./pgweb"
|
@echo "You can now execute ./pgweb"
|
||||||
|
|
||||||
release: assets
|
release: assets
|
||||||
gox \
|
@echo "Building binaries..."
|
||||||
|
@gox \
|
||||||
-osarch="$(TARGETS)" \
|
-osarch="$(TARGETS)" \
|
||||||
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
|
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
|
||||||
-output="./bin/pgweb_{{.OS}}_{{.Arch}}"
|
-output="./bin/pgweb_{{.OS}}_{{.Arch}}"
|
||||||
|
|
||||||
|
@echo "\nPackaging binaries...\n"
|
||||||
|
@./script/package.sh
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
gox -build-toolchain
|
gox -build-toolchain
|
||||||
|
|
||||||
@@ -57,7 +61,7 @@ setup:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f ./pgweb
|
rm -f ./pgweb
|
||||||
rm -f ./bin/*
|
rm -rf ./bin/*
|
||||||
rm -f bindata.go
|
rm -f bindata.go
|
||||||
make assets
|
make assets
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Web-based PostgreSQL database browser written in Go.
|
Web-based PostgreSQL database browser written in Go.
|
||||||
|
|
||||||
[](https://github.com/sosedoff/pgweb/releases)
|
[](https://github.com/sosedoff/pgweb/releases)
|
||||||
|
[](https://travis-ci.org/sosedoff/pgweb)
|
||||||
|
[](https://ci.appveyor.com/project/sosedoff/pgweb)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -12,8 +14,7 @@ is to utilize ability of the compiler to produce zero-dependency binaries for
|
|||||||
multiple platforms. Pgweb was created as an attempt to build very simple and portable
|
multiple platforms. Pgweb was created as an attempt to build very simple and portable
|
||||||
application to work with local or remote PostgreSQL databases.
|
application to work with local or remote PostgreSQL databases.
|
||||||
|
|
||||||
<img src="screenshots/browse.png" width="345px" />
|
[See application screenshots](SCREENS.md)
|
||||||
<img src="screenshots/query.png" width="345px" />
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -21,10 +22,10 @@ application to work with local or remote PostgreSQL databases.
|
|||||||
- Zero dependencies
|
- Zero dependencies
|
||||||
- Simple installation (distributes as a single binary)
|
- Simple installation (distributes as a single binary)
|
||||||
- Connect to local or remote servers
|
- Connect to local or remote servers
|
||||||
- Browse tables and table data
|
- Browse tables and table rows
|
||||||
- Get table details: structure, size, indeces, row count
|
- Get table details: structure, size, indeces, row count
|
||||||
- Run / analyze custom queries
|
- Run and analyze custom SQL queries
|
||||||
- Export query results to CSV
|
- Export table rows and query results to CSV/JSON/XML
|
||||||
- Query history
|
- Query history
|
||||||
- Server bookmarks
|
- Server bookmarks
|
||||||
|
|
||||||
@@ -63,23 +64,25 @@ pgweb --url postgres://user:password@host:port/database?sslmode=[mode]
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Run tests:
|
Before running tests, make sure you have PostgreSQL server running on `localhost:5432`
|
||||||
|
interface. Also, you must have `postgres` user that could create new databases
|
||||||
|
in your local environment. Pgweb server should not be running at the same time.
|
||||||
|
|
||||||
|
Execute test suite:
|
||||||
|
|
||||||
```
|
```
|
||||||
make test
|
make test
|
||||||
```
|
```
|
||||||
|
|
||||||
Continuous integration status:
|
|
||||||
|
|
||||||
- Travis (Linux) - [](https://travis-ci.org/sosedoff/pgweb)
|
|
||||||
- Appveyor (Windows) - [](https://ci.appveyor.com/project/sosedoff/pgweb)
|
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
- Fork repository
|
- Fork this repository
|
||||||
- Create a feature or bugfix branch
|
- Create a new feature branch for a new functionality or bugfix
|
||||||
- Open a new pull request
|
- Commit your changes
|
||||||
- Use [github issues](https://github.com/sosedoff/pgweb/issues) for any questions
|
- Execute test suite
|
||||||
|
- Push your code and open a new pull request
|
||||||
|
- Use [issues](https://github.com/sosedoff/pgweb/issues) for any questions
|
||||||
|
- Check [wiki](https://github.com/sosedoff/pgweb/wiki) for extra documentation
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
@@ -91,4 +94,4 @@ Continuous integration status:
|
|||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014-2015 Dan Sosedoff, <dan.sosedoff@gmail.com>
|
Copyright (c) 2014-2016 Dan Sosedoff, <dan.sosedoff@gmail.com>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Screenshots
|
||||||
|
|
||||||
|
### Browse table rows
|
||||||
|
<img src="screenshots/browse.png" />
|
||||||
|
|
||||||
|
### Write SQL queries
|
||||||
|
<img src="screenshots/query.png" />
|
||||||
@@ -8,20 +8,20 @@
|
|||||||
"keywords": [
|
"keywords": [
|
||||||
"postgres",
|
"postgres",
|
||||||
"database",
|
"database",
|
||||||
|
"golang",
|
||||||
"frontend"
|
"frontend"
|
||||||
],
|
],
|
||||||
"addons": [
|
"addons": [
|
||||||
"heroku-postgresql"
|
"heroku-postgresql"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"BUILDPACK_URL": "https://github.com/kr/heroku-buildpack-go",
|
|
||||||
"AUTH_USER": {
|
"AUTH_USER": {
|
||||||
"description": "A username to log into pgweb.",
|
"description": "A username to log into pgweb.",
|
||||||
"value": "admin"
|
"value": "admin"
|
||||||
},
|
},
|
||||||
"AUTH_PASS": {
|
"AUTH_PASS": {
|
||||||
"description": "The login password. Please change this.",
|
"description": "The login password. Please change this.",
|
||||||
"value": "pgweb"
|
"generator": "secret"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,13 +1,15 @@
|
|||||||
clone_folder: c:\go\src\github.com\sosedoff\pgweb
|
clone_folder: c:\go\src\github.com\sosedoff\pgweb
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
GOPATH: c:\go
|
GOPATH: c:\gopath
|
||||||
|
|
||||||
services:
|
services:
|
||||||
- postgresql
|
- postgresql
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- set PATH=%GOPATH%\bin;%PATH%
|
- set PATH=%GOPATH%\bin;%PATH%
|
||||||
|
- echo %PATH%
|
||||||
|
- echo %GOPATH%
|
||||||
- cd %APPVEYOR_BUILD_FOLDER%
|
- cd %APPVEYOR_BUILD_FOLDER%
|
||||||
- go env
|
- go env
|
||||||
- go version
|
- go version
|
||||||
@@ -20,6 +22,6 @@ build_script:
|
|||||||
- godep go build
|
- godep go build
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
- godep go test
|
- godep go test -cover ./...
|
||||||
|
|
||||||
deploy: off
|
deploy: off
|
||||||
+35
-9
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -163,6 +164,11 @@ func GetConnectionInfo(c *gin.Context) {
|
|||||||
c.JSON(200, res.Format()[0])
|
c.JSON(200, res.Format()[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetSequences(c *gin.Context) {
|
||||||
|
res, err := DbClient.Sequences()
|
||||||
|
serveResult(res, err, c)
|
||||||
|
}
|
||||||
|
|
||||||
func GetActivity(c *gin.Context) {
|
func GetActivity(c *gin.Context) {
|
||||||
res, err := DbClient.Activity()
|
res, err := DbClient.Activity()
|
||||||
serveResult(res, err, c)
|
serveResult(res, err, c)
|
||||||
@@ -173,24 +179,44 @@ func GetTableIndexes(c *gin.Context) {
|
|||||||
serveResult(res, err, c)
|
serveResult(res, err, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleQuery(query string, c *gin.Context) {
|
func GetTableConstraints(c *gin.Context) {
|
||||||
result, err := DbClient.Query(query)
|
res, err := DbClient.TableConstraints(c.Params.ByName("table"))
|
||||||
|
serveResult(res, err, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleQuery(query string, c *gin.Context) {
|
||||||
|
rawQuery, err := base64.StdEncoding.DecodeString(query)
|
||||||
|
if err == nil {
|
||||||
|
query = string(rawQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := DbClient.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, NewError(err))
|
c.JSON(400, NewError(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
q := c.Request.URL.Query()
|
format := getQueryParam(c, "format")
|
||||||
|
filename := getQueryParam(c, "filename")
|
||||||
|
|
||||||
if len(q["format"]) > 0 && q["format"][0] == "csv" {
|
if filename == "" {
|
||||||
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix())
|
filename = fmt.Sprintf("pgweb-%v.%v", time.Now().Unix(), format)
|
||||||
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
|
|
||||||
c.Data(200, "text/csv", result.CSV())
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, result)
|
if format != "" {
|
||||||
|
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "csv":
|
||||||
|
c.Data(200, "text/csv", result.CSV())
|
||||||
|
case "json":
|
||||||
|
c.Data(200, "applicaiton/json", result.JSON())
|
||||||
|
case "xml":
|
||||||
|
c.XML(200, result)
|
||||||
|
default:
|
||||||
|
c.JSON(200, result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetBookmarks(c *gin.Context) {
|
func GetBookmarks(c *gin.Context) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"mime"
|
"mime"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@@ -21,6 +22,17 @@ type Error struct {
|
|||||||
Message string `json:"error"`
|
Message string `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getQueryParam(c *gin.Context, name string) string {
|
||||||
|
result := ""
|
||||||
|
q := c.Request.URL.Query()
|
||||||
|
|
||||||
|
if len(q[name]) > 0 {
|
||||||
|
result = q[name][0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func assetContentType(name string) string {
|
func assetContentType(name string) string {
|
||||||
ext := filepath.Ext(name)
|
ext := filepath.Ext(name)
|
||||||
result := mime.TypeByExtension(ext)
|
result := mime.TypeByExtension(ext)
|
||||||
@@ -76,6 +88,15 @@ func dbCheckMiddleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Middleware function to print out request parameters and body for debugging
|
||||||
|
func requestInspectMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
err := c.Request.ParseForm()
|
||||||
|
|
||||||
|
log.Println("Request params:", err, c.Request.Form)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func serveStaticAsset(path string, c *gin.Context) {
|
func serveStaticAsset(path string, c *gin.Context) {
|
||||||
data, err := data.Asset("static" + path)
|
data, err := data.Asset("static" + path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+12
-1
@@ -2,20 +2,30 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sosedoff/pgweb/pkg/command"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func SetupMiddlewares(group *gin.RouterGroup) {
|
||||||
|
if command.Opts.Debug {
|
||||||
|
group.Use(requestInspectMiddleware())
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Use(dbCheckMiddleware())
|
||||||
|
}
|
||||||
|
|
||||||
func SetupRoutes(router *gin.Engine) {
|
func SetupRoutes(router *gin.Engine) {
|
||||||
router.GET("/", GetHome)
|
router.GET("/", GetHome)
|
||||||
router.GET("/static/*path", GetAsset)
|
router.GET("/static/*path", GetAsset)
|
||||||
|
|
||||||
api := router.Group("/api")
|
api := router.Group("/api")
|
||||||
{
|
{
|
||||||
api.Use(dbCheckMiddleware())
|
SetupMiddlewares(api)
|
||||||
|
|
||||||
api.GET("/info", GetInfo)
|
api.GET("/info", GetInfo)
|
||||||
api.POST("/connect", Connect)
|
api.POST("/connect", Connect)
|
||||||
api.GET("/databases", GetDatabases)
|
api.GET("/databases", GetDatabases)
|
||||||
api.GET("/connection", GetConnectionInfo)
|
api.GET("/connection", GetConnectionInfo)
|
||||||
|
api.GET("/sequences", GetSequences)
|
||||||
api.GET("/activity", GetActivity)
|
api.GET("/activity", GetActivity)
|
||||||
api.GET("/schemas", GetSchemas)
|
api.GET("/schemas", GetSchemas)
|
||||||
api.GET("/tables", GetTables)
|
api.GET("/tables", GetTables)
|
||||||
@@ -23,6 +33,7 @@ func SetupRoutes(router *gin.Engine) {
|
|||||||
api.GET("/tables/:table/rows", GetTableRows)
|
api.GET("/tables/:table/rows", GetTableRows)
|
||||||
api.GET("/tables/:table/info", GetTableInfo)
|
api.GET("/tables/:table/info", GetTableInfo)
|
||||||
api.GET("/tables/:table/indexes", GetTableIndexes)
|
api.GET("/tables/:table/indexes", GetTableIndexes)
|
||||||
|
api.GET("/tables/:table/constraints", GetTableConstraints)
|
||||||
api.GET("/query", RunQuery)
|
api.GET("/query", RunQuery)
|
||||||
api.POST("/query", RunQuery)
|
api.POST("/query", RunQuery)
|
||||||
api.GET("/explain", ExplainQuery)
|
api.GET("/explain", ExplainQuery)
|
||||||
|
|||||||
+14
-54
@@ -1,8 +1,6 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/csv"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
@@ -21,13 +19,6 @@ type Client struct {
|
|||||||
ConnectionString string
|
ConnectionString string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Row []interface{}
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
Columns []string `json:"columns"`
|
|
||||||
Rows []Row `json:"rows"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Struct to hold table rows browsing options
|
// Struct to hold table rows browsing options
|
||||||
type RowsOptions struct {
|
type RowsOptions struct {
|
||||||
Limit int // Number of rows to fetch
|
Limit int // Number of rows to fetch
|
||||||
@@ -136,6 +127,20 @@ func (client *Client) TableIndexes(table string) (*Result, error) {
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (client *Client) TableConstraints(table string) (*Result, error) {
|
||||||
|
res, err := client.query(statements.PG_TABLE_CONSTRAINTS, table)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) Sequences() ([]string, error) {
|
||||||
|
return client.fetchRows(statements.PG_SEQUENCES)
|
||||||
|
}
|
||||||
|
|
||||||
// Returns all active queriers on the server
|
// Returns all active queriers on the server
|
||||||
func (client *Client) Activity() (*Result, error) {
|
func (client *Client) Activity() (*Result, error) {
|
||||||
return client.query(statements.PG_ACTIVITY)
|
return client.query(statements.PG_ACTIVITY)
|
||||||
@@ -191,51 +196,6 @@ func (client *Client) query(query string, args ...interface{}) (*Result, error)
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Result) Format() []map[string]interface{} {
|
|
||||||
var items []map[string]interface{}
|
|
||||||
|
|
||||||
for _, row := range res.Rows {
|
|
||||||
item := make(map[string]interface{})
|
|
||||||
|
|
||||||
for i, c := range res.Columns {
|
|
||||||
item[c] = row[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (res *Result) CSV() []byte {
|
|
||||||
buff := &bytes.Buffer{}
|
|
||||||
writer := csv.NewWriter(buff)
|
|
||||||
|
|
||||||
writer.Write(res.Columns)
|
|
||||||
|
|
||||||
for _, row := range res.Rows {
|
|
||||||
record := make([]string, len(res.Columns))
|
|
||||||
|
|
||||||
for i, item := range row {
|
|
||||||
if item != nil {
|
|
||||||
record[i] = fmt.Sprintf("%v", item)
|
|
||||||
} else {
|
|
||||||
record[i] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := writer.Write(record)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Flush()
|
|
||||||
return buff.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close database connection
|
// Close database connection
|
||||||
func (client *Client) Close() error {
|
func (client *Client) Close() error {
|
||||||
if client.db != nil {
|
if client.db != nil {
|
||||||
|
|||||||
@@ -81,6 +81,18 @@ func test_NewClientFromUrl(t *testing.T) {
|
|||||||
assert.Equal(t, url, client.ConnectionString)
|
assert.Equal(t, url, client.ConnectionString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func test_NewClientFromUrl2(t *testing.T) {
|
||||||
|
url := "postgresql://postgres@localhost/booktown?sslmode=disable"
|
||||||
|
client, err := NewFromUrl(url)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
defer client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, url, client.ConnectionString)
|
||||||
|
}
|
||||||
|
|
||||||
func test_Test(t *testing.T) {
|
func test_Test(t *testing.T) {
|
||||||
assert.Equal(t, nil, testClient.Test())
|
assert.Equal(t, nil, testClient.Test())
|
||||||
}
|
}
|
||||||
@@ -176,6 +188,28 @@ func test_TableIndexes(t *testing.T) {
|
|||||||
assert.Equal(t, 2, len(res.Rows))
|
assert.Equal(t, 2, len(res.Rows))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func test_TableConstraints(t *testing.T) {
|
||||||
|
res, err := testClient.TableConstraints("editions")
|
||||||
|
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, 1, len(res.Columns))
|
||||||
|
assert.Equal(t, 2, len(res.Rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
func test_Sequences(t *testing.T) {
|
||||||
|
res, err := testClient.Sequences()
|
||||||
|
|
||||||
|
expected := []string{
|
||||||
|
"author_ids",
|
||||||
|
"book_ids",
|
||||||
|
"shipments_ship_id_seq",
|
||||||
|
"subject_ids",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, expected, res)
|
||||||
|
}
|
||||||
|
|
||||||
func test_Query(t *testing.T) {
|
func test_Query(t *testing.T) {
|
||||||
res, err := testClient.Query("SELECT * FROM books")
|
res, err := testClient.Query("SELECT * FROM books")
|
||||||
|
|
||||||
@@ -245,6 +279,8 @@ func TestAll(t *testing.T) {
|
|||||||
test_TableRows(t)
|
test_TableRows(t)
|
||||||
test_TableInfo(t)
|
test_TableInfo(t)
|
||||||
test_TableIndexes(t)
|
test_TableIndexes(t)
|
||||||
|
test_TableConstraints(t)
|
||||||
|
test_Sequences(t)
|
||||||
test_Query(t)
|
test_Query(t)
|
||||||
test_QueryError(t)
|
test_QueryError(t)
|
||||||
test_QueryInvalidTable(t)
|
test_QueryInvalidTable(t)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/csv"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Row []interface{}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Columns []string `json:"columns"`
|
||||||
|
Rows []Row `json:"rows"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Result) Format() []map[string]interface{} {
|
||||||
|
var items []map[string]interface{}
|
||||||
|
|
||||||
|
for _, row := range res.Rows {
|
||||||
|
item := make(map[string]interface{})
|
||||||
|
|
||||||
|
for i, c := range res.Columns {
|
||||||
|
item[c] = row[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Result) CSV() []byte {
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
writer := csv.NewWriter(buff)
|
||||||
|
|
||||||
|
writer.Write(res.Columns)
|
||||||
|
|
||||||
|
for _, row := range res.Rows {
|
||||||
|
record := make([]string, len(res.Columns))
|
||||||
|
|
||||||
|
for i, item := range row {
|
||||||
|
if item != nil {
|
||||||
|
record[i] = fmt.Sprintf("%v", item)
|
||||||
|
} else {
|
||||||
|
record[i] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := writer.Write(record)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Flush()
|
||||||
|
return buff.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Result) JSON() []byte {
|
||||||
|
data, _ := json.Marshal(res.Format())
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_CSV(t *testing.T) {
|
||||||
|
result := Result{
|
||||||
|
Columns: []string{"id", "name", "email"},
|
||||||
|
Rows: []Row{
|
||||||
|
Row{1, "John", "john@example.com"},
|
||||||
|
Row{2, "Bob", "bob@example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "id,name,email\n1,John,john@example.com\n2,Bob,bob@example.com\n"
|
||||||
|
output := string(result.CSV())
|
||||||
|
|
||||||
|
assert.Equal(t, expected, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_JSON(t *testing.T) {
|
||||||
|
result := Result{
|
||||||
|
Columns: []string{"id", "name", "email"},
|
||||||
|
Rows: []Row{
|
||||||
|
Row{1, "John", "john@example.com"},
|
||||||
|
Row{2, "Bob", "bob@example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
output := result.JSON()
|
||||||
|
obj := []map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(output, &obj)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 2, len(obj))
|
||||||
|
|
||||||
|
for i, row := range obj {
|
||||||
|
for j, col := range result.Columns {
|
||||||
|
assert.Equal(t, result.Rows[i][j], row[col])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package command
|
package command
|
||||||
|
|
||||||
const VERSION = "0.5.2"
|
const VERSION = "0.7.0"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
GitCommit string
|
GitCommit string
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package connection
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
neturl "net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -28,7 +29,7 @@ func FormatUrl(opts command.Options) (string, error) {
|
|||||||
url := opts.Url
|
url := opts.Url
|
||||||
|
|
||||||
// Make sure to only accept urls in a standard format
|
// Make sure to only accept urls in a standard format
|
||||||
if !strings.Contains(url, "postgres://") {
|
if !strings.HasPrefix(url, "postgres://") && !strings.HasPrefix(url, "postgresql://") {
|
||||||
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
|
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ func BuildString(opts command.Options) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if opts.Pass != "" {
|
if opts.Pass != "" {
|
||||||
url += fmt.Sprintf(":%s", opts.Pass)
|
url += fmt.Sprintf(":%s", neturl.QueryEscape(opts.Pass))
|
||||||
}
|
}
|
||||||
|
|
||||||
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
|
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
func Test_Invalid_Url(t *testing.T) {
|
func Test_Invalid_Url(t *testing.T) {
|
||||||
opts := command.Options{}
|
opts := command.Options{}
|
||||||
examples := []string{
|
examples := []string{
|
||||||
"postgresql://foobar",
|
"postgre://foobar",
|
||||||
"foobar",
|
"foobar",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if the TCP port available on localhost
|
||||||
|
func portAvailable(port int) bool {
|
||||||
|
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", port))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if strings.Index(err.Error(), "connection refused") > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Close()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available TCP port on localhost by trying available ports in a range
|
||||||
|
func getAvailablePort(start int, limit int) (int, error) {
|
||||||
|
for i := start; i <= (start + limit); i++ {
|
||||||
|
if portAvailable(i) {
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, errors.New("No available port")
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package connection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_portAvailable(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("FIXME")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, true, portAvailable(8081))
|
||||||
|
|
||||||
|
serv, err := net.Listen("tcp", "127.0.0.1:8081")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(os.Stderr, "Unable to start test tcp listener:", err)
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer serv.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := serv.Accept()
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
serv.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
assert.Equal(t, false, portAvailable(8081))
|
||||||
|
assert.Equal(t, true, portAvailable(8082))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getAvailablePort(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("FIXME")
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := getAvailablePort(8081, 1)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, 8081, port)
|
||||||
|
|
||||||
|
serv, err := net.Listen("tcp", "127.0.0.1:8081")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(os.Stderr, "Unable to start test tcp listener:", err)
|
||||||
|
t.Fail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer serv.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := serv.Accept()
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
port, err = getAvailablePort(8081, 0)
|
||||||
|
assert.EqualError(t, err, "No available port")
|
||||||
|
assert.Equal(t, -1, port)
|
||||||
|
|
||||||
|
port, err = getAvailablePort(8081, 1)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
assert.Equal(t, 8082, port)
|
||||||
|
}
|
||||||
+283
-230
File diff suppressed because one or more lines are too long
@@ -18,6 +18,16 @@ const (
|
|||||||
|
|
||||||
PG_TABLE_INDEXES = `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = $1`
|
PG_TABLE_INDEXES = `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = $1`
|
||||||
|
|
||||||
|
PG_TABLE_CONSTRAINTS = `SELECT
|
||||||
|
pg_get_constraintdef(c.oid, true) as condef
|
||||||
|
FROM pg_constraint c
|
||||||
|
JOIN pg_namespace n ON n.oid = c.connamespace
|
||||||
|
JOIN pg_class cl ON cl.oid = c.conrelid
|
||||||
|
WHERE n.nspname = 'public'
|
||||||
|
AND relname = $1
|
||||||
|
ORDER BY contype desc`
|
||||||
|
|
||||||
|
|
||||||
PG_TABLE_INFO = `SELECT
|
PG_TABLE_INFO = `SELECT
|
||||||
pg_size_pretty(pg_table_size($1)) AS data_size
|
pg_size_pretty(pg_table_size($1)) AS data_size
|
||||||
, pg_size_pretty(pg_indexes_size($1)) AS index_size
|
, pg_size_pretty(pg_indexes_size($1)) AS index_size
|
||||||
@@ -31,6 +41,8 @@ WHERE table_name = $1`
|
|||||||
|
|
||||||
PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name`
|
PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name`
|
||||||
|
|
||||||
|
PG_SEQUENCES = `SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' ORDER BY sequence_name`
|
||||||
|
|
||||||
PG_ACTIVITY = `SELECT
|
PG_ACTIVITY = `SELECT
|
||||||
datname,
|
datname,
|
||||||
query,
|
query,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 317 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 367 KiB |
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DIR="./bin"
|
||||||
|
rm -f $DIR/*.zip
|
||||||
|
|
||||||
|
for file in $(ls $DIR)
|
||||||
|
do
|
||||||
|
fin=$DIR/$file
|
||||||
|
fout=$DIR/$file.zip
|
||||||
|
shasum -a 256 $fin
|
||||||
|
zip -9 -q -j $fout $fin
|
||||||
|
shasum -a 256 $fout
|
||||||
|
done
|
||||||
+73
-36
@@ -8,8 +8,7 @@
|
|||||||
left: 250px;
|
left: 250px;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
box-shadow: inset 0 -1px 0 0 #b8b7b5;
|
background: #79589f;
|
||||||
background: #d8d7d6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav ul {
|
#nav ul {
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
line-height: 49px;
|
line-height: 49px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 0px 14px;
|
padding: 0px 14px;
|
||||||
color: #6c6b6b;
|
color: #d6cce2;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0 1px;
|
margin: 0 1px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -41,15 +40,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#nav ul li:hover {
|
#nav ul li:hover {
|
||||||
color: rgba(0,0,0,0.9);
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav ul li.selected {
|
#nav ul li.selected {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #000;
|
color: #000;
|
||||||
border-right: 1px solid #b8b7b5;
|
|
||||||
border-left: 1px solid #b8b7b5;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +70,8 @@
|
|||||||
left: 0px;
|
left: 0px;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .title {
|
#sidebar .title {
|
||||||
@@ -83,8 +82,17 @@
|
|||||||
height: 33px;
|
height: 33px;
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
|
color: #555;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .title.main {
|
||||||
|
line-height: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background: #79589f;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: #31373d;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.tables-list {
|
#sidebar div.tables-list {
|
||||||
@@ -94,7 +102,6 @@
|
|||||||
left: 0px;
|
left: 0px;
|
||||||
bottom: 130px;
|
bottom: 130px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #31373d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.tables-list .wrap {
|
#sidebar div.tables-list .wrap {
|
||||||
@@ -114,17 +121,13 @@
|
|||||||
float: right;
|
float: right;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #95a7b7;
|
color: #fff;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.tables-list .title span.refresh:hover {
|
#sidebar div.tables-list #tables, #sequences {
|
||||||
color: #fff;
|
padding: 50px 0 0;
|
||||||
}
|
font-size: 12px;
|
||||||
|
|
||||||
#sidebar div.tables-list #tables {
|
|
||||||
padding: 33px 0 0;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.table-information {
|
#sidebar div.table-information {
|
||||||
@@ -133,7 +136,9 @@
|
|||||||
left: 0px;
|
left: 0px;
|
||||||
height: 130px;
|
height: 130px;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
background: #272c30;
|
/*border-top: 1px solid #d4d4d4;*/
|
||||||
|
/*border-right: 1px solid #d4d4d4;*/
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.table-information .wrap {
|
#sidebar div.table-information .wrap {
|
||||||
@@ -142,7 +147,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.table-information .title {
|
#sidebar div.table-information .title {
|
||||||
background: #272c30;
|
/*background: #f6f6f6;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.table-information ul {
|
#sidebar div.table-information ul {
|
||||||
@@ -156,11 +161,11 @@
|
|||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 0 8px 0 12px;
|
padding: 0 8px 0 12px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: #637D94 !important;
|
color: #7F7E7F !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar div.table-information ul li span {
|
#sidebar div.table-information ul li span {
|
||||||
color: #95a7b7;
|
color: #4F4F4F;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
@@ -177,19 +182,18 @@
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #95a7b7 !important;
|
color: #4F4E4F !important;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar li.selected {
|
#sidebar li.selected {
|
||||||
color: #fff !important;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: #272c30;
|
background: #e6e6e6;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar ul li:hover span {
|
#sidebar ul li:hover span {
|
||||||
background: #272c30;
|
background: #f1f1f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar ul li i {
|
#sidebar ul li i {
|
||||||
@@ -225,8 +229,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-top: solid 1px #e1e2e3;
|
border-bottom: solid 1px #ddd;
|
||||||
border-bottom: solid 1px #b8b7b5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#input .actions .btn {
|
#input .actions .btn {
|
||||||
@@ -235,18 +238,32 @@
|
|||||||
padding: 0px 13px;
|
padding: 0px 13px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #fff;
|
color: #555;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
background: #7eb54e;
|
background: transparent;
|
||||||
|
border: 1px solid #999;
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#input .actions .btn-default {
|
#input .actions .btn-primary {
|
||||||
background: #aaa;
|
border-color: #7eb54e;
|
||||||
|
color: #7eb54e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#input .actions .btn:hover {
|
||||||
|
background: transparent !important;
|
||||||
|
border-color: #444;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#input .actions .btn-primary:hover {
|
||||||
|
color: #64903e;
|
||||||
|
border-color: #64903e;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#input .actions #query_progress {
|
#input .actions #query_progress {
|
||||||
display: none;
|
display: none;
|
||||||
float: left;
|
float: left;
|
||||||
@@ -295,7 +312,6 @@
|
|||||||
|
|
||||||
#results tr:nth-child(even) > td {
|
#results tr:nth-child(even) > td {
|
||||||
border: none;
|
border: none;
|
||||||
background: #fafafa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#results tr:nth-child(odd) > td {
|
#results tr:nth-child(odd) > td {
|
||||||
@@ -304,18 +320,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#results th {
|
#results th {
|
||||||
background: #f3f3f3;
|
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-bottom: 1px solid #eae9e9;
|
border-bottom: 1px solid #eae9e9;
|
||||||
padding: 3px 9px;
|
padding: 3px 9px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: #6a6a6a;
|
color: #333;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
#results th.active {
|
#results th.active {
|
||||||
background: #e3e3e3;
|
background: #f3f3f3;
|
||||||
}
|
}
|
||||||
|
|
||||||
#results[data-mode="browse"] th:hover {
|
#results[data-mode="browse"] th:hover {
|
||||||
@@ -376,7 +391,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#custom_query {
|
#custom_query {
|
||||||
height: 205px;
|
height: 193px;
|
||||||
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#connection_window {
|
#connection_window {
|
||||||
@@ -394,6 +410,9 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
|
background: #79589f;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edit_connection i {
|
#edit_connection i {
|
||||||
@@ -444,4 +463,22 @@
|
|||||||
|
|
||||||
.connection-ssh-group {
|
.connection-ssh-group {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
/* Ace Customizations */
|
||||||
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.ace_gutter, .ace_gutter-cell {
|
||||||
|
background: #fff !important;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace_active-line {
|
||||||
|
background: #f9f9f9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ace_active-line {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|||||||
+19
-6
@@ -12,31 +12,34 @@
|
|||||||
<script type="text/javascript" src="/static/js/jquery.js"></script>
|
<script type="text/javascript" src="/static/js/jquery.js"></script>
|
||||||
<script type="text/javascript" src="/static/js/ace.js"></script>
|
<script type="text/javascript" src="/static/js/ace.js"></script>
|
||||||
<script type="text/javascript" src="/static/js/ace-pgsql.js"></script>
|
<script type="text/javascript" src="/static/js/ace-pgsql.js"></script>
|
||||||
|
<script type="text/javascript" src="/static/js/bootstrap-contextmenu.js"></script>
|
||||||
<script type="text/javascript" src="/static/js/app.js"></script>
|
<script type="text/javascript" src="/static/js/app.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
<ul>
|
<ul>
|
||||||
<li id="table_content">Content</li>
|
<li id="table_content">Rows</li>
|
||||||
<li id="table_structure">Structure</li>
|
<li id="table_structure">Structure</li>
|
||||||
<li id="table_indexes">Indexes</li>
|
<li id="table_indexes">Indexes</li>
|
||||||
|
<li id="table_constraints">Constraints</li>
|
||||||
<li id="table_query" class="selected">SQL Query</li>
|
<li id="table_query" class="selected">SQL Query</li>
|
||||||
<li id="table_history">History</li>
|
<li id="table_history">History</li>
|
||||||
<li id="table_activity">Activity</li>
|
<li id="table_activity">Activity</li>
|
||||||
<li id="table_connection">Connection</li>
|
<li id="table_connection">Connection</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<a href="#" id="edit_connection" class="btn btn-primary btn-sm"><i class="fa fa-gear"></i> Edit Connection</a>
|
<a href="#" id="edit_connection" class="btn btn-default btn-sm"><i class="fa fa-globe"></i> Edit Connection</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
<div class="tables-list">
|
<div class="tables-list">
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<div class="title">
|
<div class="title main">
|
||||||
<i class="fa fa-database"></i> <span class="current-database" id="current_database"></span>
|
<i class="fa fa-database"></i> <span class="current-database" id="current_database"></span>
|
||||||
<span class="refresh" id="refresh_tables" title="Refresh tables list"><i class="fa fa-refresh"></i></span>
|
<span class="refresh" id="refresh_tables" title="Refresh tables list"><i class="fa fa-refresh"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<ul id="tables"></ul>
|
<ul id="tables"></ul>
|
||||||
|
<ul id="sequences"></ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-information">
|
<div class="table-information">
|
||||||
@@ -58,7 +61,8 @@
|
|||||||
<div class="actions">
|
<div class="actions">
|
||||||
<input type="button" id="run" value="Run Query" class="btn btn-sm btn-primary" />
|
<input type="button" id="run" value="Run Query" class="btn btn-sm btn-primary" />
|
||||||
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
|
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
|
||||||
<input type="button" id="csv" value="Download CSV" class="btn btn-sm btn-default" />
|
<input type="button" id="csv" value="CSV" class="btn btn-sm btn-default" />
|
||||||
|
<input type="button" id="json" value="JSON" class="btn btn-sm btn-default" />
|
||||||
|
|
||||||
<div id="query_progress">Please wait, query is executing...</div>
|
<div id="query_progress">Please wait, query is executing...</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +85,6 @@
|
|||||||
<div class="btn-group btn-group-sm connection-group-switch">
|
<div class="btn-group btn-group-sm connection-group-switch">
|
||||||
<button type="button" data="scheme" class="btn btn-default" id="connection_scheme">Scheme</button>
|
<button type="button" data="scheme" class="btn btn-default" id="connection_scheme">Scheme</button>
|
||||||
<button type="button" data="standard" class="btn btn-default active" id="connection_standard">Standard</button>
|
<button type="button" data="standard" class="btn btn-default active" id="connection_standard">Standard</button>
|
||||||
<!--<button type="button" data="ssh" class="btn btn-default">SSH</button>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-3 control-label">Password</label>
|
<label class="col-sm-3 control-label">Password</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<input type="text" id="pg_password" class="form-control" />
|
<input type="password" id="pg_password" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -193,5 +196,15 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tables_context_menu">
|
||||||
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
<li><a href="#" data-action="export" data-format="json">Export to JSON</a></li>
|
||||||
|
<li><a href="#" data-action="export" data-format="csv">Export to CSV</a></li>
|
||||||
|
<li><a href="#" data-action="export" data-format="xml">Export to XML</a></li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li><a href="#" data-action="truncate">Truncate table</a></li>
|
||||||
|
<li><a href="#" data-action="delete">Delete table</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+152
-36
@@ -17,12 +17,18 @@ function apiCall(method, path, params, cb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTables(cb) { apiCall("get", "/tables", {}, cb); }
|
function getTables(cb) { apiCall("get", "/tables", {}, cb); }
|
||||||
function getTableRows(table, opts, cb) { apiCall("get", "/tables/" + table + "/rows", opts, cb); }
|
function getTableRows(table, opts, cb) { apiCall("get", "/tables/" + table + "/rows", opts, cb); }
|
||||||
function getTableStructure(table, cb) { apiCall("get", "/tables/" + table, {}, cb); }
|
function getTableStructure(table, cb) { apiCall("get", "/tables/" + table, {}, cb); }
|
||||||
function getTableIndexes(table, cb) { apiCall("get", "/tables/" + table + "/indexes", {}, cb); }
|
function getTableIndexes(table, cb) { apiCall("get", "/tables/" + table + "/indexes", {}, cb); }
|
||||||
function getHistory(cb) { apiCall("get", "/history", {}, cb); }
|
function getTableConstraints(table, cb) { apiCall("get", "/tables/" + table + "/constraints", {}, cb); }
|
||||||
function getBookmarks(cb) { apiCall("get", "/bookmarks", {}, cb); }
|
function getHistory(cb) { apiCall("get", "/history", {}, cb); }
|
||||||
|
function getBookmarks(cb) { apiCall("get", "/bookmarks", {}, cb); }
|
||||||
|
function getSequences(cb) { apiCall("get", "/sequences", {}, cb); }
|
||||||
|
|
||||||
|
function encodeQuery(query) {
|
||||||
|
return window.btoa(query);
|
||||||
|
}
|
||||||
|
|
||||||
function executeQuery(query, cb) {
|
function executeQuery(query, cb) {
|
||||||
apiCall("post", "/query", { query: query }, cb);
|
apiCall("post", "/query", { query: query }, cb);
|
||||||
@@ -37,7 +43,17 @@ function loadTables() {
|
|||||||
|
|
||||||
getTables(function(data) {
|
getTables(function(data) {
|
||||||
data.forEach(function(item) {
|
data.forEach(function(item) {
|
||||||
$("<li><span><i class='fa fa-table'></i> " + item + "</span></li>").appendTo("#tables");
|
$("<li><span><i class='fa fa-table'></i> " + item + " </span></li>").appendTo("#tables");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSequences() {
|
||||||
|
$("#sequences li").remove();
|
||||||
|
|
||||||
|
getSequences(function(data) {
|
||||||
|
data.forEach(function(item) {
|
||||||
|
$("<li><span><i class='fa fa-chevron-right'></i> " + item + " </span></li>").appendTo("#sequences");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,15 +84,47 @@ function resetTable() {
|
|||||||
removeClass("no-crop");
|
removeClass("no-crop");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function performTableAction(table, action, el) {
|
||||||
|
if (action == "truncate" || action == "delete") {
|
||||||
|
var message = "Are you sure you want to " + action + " table " + table + " ?";
|
||||||
|
if (!confirm(message)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(action) {
|
||||||
|
case "truncate":
|
||||||
|
executeQuery("TRUNCATE TABLE " + table, function(data) {
|
||||||
|
if (data.error) alert(data.error);
|
||||||
|
resetTable();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
executeQuery("DROP TABLE " + table, function(data) {
|
||||||
|
if (data.error) alert(data.error);
|
||||||
|
loadTables();
|
||||||
|
loadSequences();
|
||||||
|
resetTable();
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "export":
|
||||||
|
var format = el.data("format");
|
||||||
|
var filename = table + "." + format;
|
||||||
|
var query = window.encodeURI("SELECT * FROM " + table);
|
||||||
|
var url = "http://" + window.location.host + "/api/query?format=" + format + "&filename=" + filename + "&query=" + query;
|
||||||
|
var win = window.open(url, "_blank");
|
||||||
|
win.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sortArrow(direction) {
|
function sortArrow(direction) {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case "ASC":
|
case "ASC":
|
||||||
return "▲";
|
return "▲";
|
||||||
case "DESC":
|
case "DESC":
|
||||||
return "▼";
|
return "▼";
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTable(results, sortColumn, sortOrder) {
|
function buildTable(results, sortColumn, sortOrder) {
|
||||||
@@ -155,6 +203,24 @@ function showTableIndexes() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showTableConstraints() {
|
||||||
|
var name = getCurrentTable();
|
||||||
|
|
||||||
|
if (name.length == 0) {
|
||||||
|
alert("Please select a table!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTableConstraints(name, function(data) {
|
||||||
|
setCurrentTab("table_constraints");
|
||||||
|
buildTable(data);
|
||||||
|
|
||||||
|
$("#input").hide();
|
||||||
|
$("#output").addClass("full");
|
||||||
|
$("#results").addClass("no-crop");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function showTableInfo() {
|
function showTableInfo() {
|
||||||
var name = getCurrentTable();
|
var name = getCurrentTable();
|
||||||
|
|
||||||
@@ -250,13 +316,13 @@ function showActivityPanel() {
|
|||||||
function runQuery() {
|
function runQuery() {
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", true);
|
$("#run, #explain, #csv, #json").prop("disabled", true);
|
||||||
$("#query_progress").show();
|
$("#query_progress").show();
|
||||||
|
|
||||||
var query = $.trim(editor.getValue());
|
var query = $.trim(editor.getSelectedText() || editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -264,7 +330,7 @@ function runQuery() {
|
|||||||
executeQuery(query, function(data) {
|
executeQuery(query, function(data) {
|
||||||
buildTable(data);
|
buildTable(data);
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
$("#input").show();
|
$("#input").show();
|
||||||
$("#output").removeClass("full");
|
$("#output").removeClass("full");
|
||||||
@@ -278,6 +344,7 @@ function runQuery() {
|
|||||||
// Refresh tables list if table was added or removed
|
// Refresh tables list if table was added or removed
|
||||||
if (query.match(re)) {
|
if (query.match(re)) {
|
||||||
loadTables();
|
loadTables();
|
||||||
|
loadSequences();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -285,13 +352,13 @@ function runQuery() {
|
|||||||
function runExplain() {
|
function runExplain() {
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", true);
|
$("#run, #explain, #csv, #json").prop("disabled", true);
|
||||||
$("#query_progress").show();
|
$("#query_progress").show();
|
||||||
|
|
||||||
var query = $.trim(editor.getValue());
|
var query = $.trim(editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -299,7 +366,7 @@ function runExplain() {
|
|||||||
explainQuery(query, function(data) {
|
explainQuery(query, function(data) {
|
||||||
buildTable(data);
|
buildTable(data);
|
||||||
|
|
||||||
$("#run, #explain, #csv").prop("disabled", false);
|
$("#run, #explain, #csv, #json").prop("disabled", false);
|
||||||
$("#query_progress").hide();
|
$("#query_progress").hide();
|
||||||
$("#input").show();
|
$("#input").show();
|
||||||
$("#output").removeClass("full");
|
$("#output").removeClass("full");
|
||||||
@@ -307,17 +374,14 @@ function runExplain() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportToCSV() {
|
function exportTo(format) {
|
||||||
var query = $.trim(editor.getValue());
|
var query = $.trim(editor.getValue());
|
||||||
|
|
||||||
if (query.length == 0) {
|
if (query.length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace line breaks with spaces and properly encode query
|
var url = "http://" + window.location.host + "/api/query?format=" + format + "&query=" + encodeQuery(query);
|
||||||
query = window.encodeURI(query.replace(/\n/g, " "));
|
|
||||||
|
|
||||||
var url = "http://" + window.location.host + "/api/query?format=csv&query=" + query;
|
|
||||||
var win = window.open(url, '_blank');
|
var win = window.open(url, '_blank');
|
||||||
|
|
||||||
setCurrentTab("table_query");
|
setCurrentTab("table_query");
|
||||||
@@ -325,11 +389,16 @@ function exportToCSV() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initEditor() {
|
function initEditor() {
|
||||||
|
var writeQueryTimeout = null;
|
||||||
editor = ace.edit("custom_query");
|
editor = ace.edit("custom_query");
|
||||||
|
|
||||||
|
editor.setFontSize(13);
|
||||||
|
editor.setTheme("ace/theme/tomorrow");
|
||||||
|
editor.setShowPrintMargin(false);
|
||||||
editor.getSession().setMode("ace/mode/pgsql");
|
editor.getSession().setMode("ace/mode/pgsql");
|
||||||
editor.getSession().setTabSize(2);
|
editor.getSession().setTabSize(2);
|
||||||
editor.getSession().setUseSoftTabs(true);
|
editor.getSession().setUseSoftTabs(true);
|
||||||
|
|
||||||
editor.commands.addCommands([{
|
editor.commands.addCommands([{
|
||||||
name: "run_query",
|
name: "run_query",
|
||||||
bindKey: {
|
bindKey: {
|
||||||
@@ -349,6 +418,22 @@ function initEditor() {
|
|||||||
runExplain();
|
runExplain();
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
editor.on("change", function() {
|
||||||
|
if (writeQueryTimeout) {
|
||||||
|
clearTimeout(writeQueryTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeQueryTimeout = setTimeout(function() {
|
||||||
|
localStorage.setItem("pgweb_query", editor.getValue());
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
var query = localStorage.getItem("pgweb_query");
|
||||||
|
if (query && query.length > 0) {
|
||||||
|
editor.setValue(query);
|
||||||
|
editor.clearSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addShortcutTooltips() {
|
function addShortcutTooltips() {
|
||||||
@@ -403,7 +488,7 @@ function getConnectionString() {
|
|||||||
var host = $("#pg_host").val();
|
var host = $("#pg_host").val();
|
||||||
var port = $("#pg_port").val();
|
var port = $("#pg_port").val();
|
||||||
var user = $("#pg_user").val();
|
var user = $("#pg_user").val();
|
||||||
var pass = $("#pg_password").val();
|
var pass = encodeURIComponent($("#pg_password").val());
|
||||||
var db = $("#pg_db").val();
|
var db = $("#pg_db").val();
|
||||||
|
|
||||||
if (port.length == 0) {
|
if (port.length == 0) {
|
||||||
@@ -424,13 +509,14 @@ function getConnectionString() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$("#table_content").on("click", function() { showTableContent(); });
|
$("#table_content").on("click", function() { showTableContent(); });
|
||||||
$("#table_structure").on("click", function() { showTableStructure(); });
|
$("#table_structure").on("click", function() { showTableStructure(); });
|
||||||
$("#table_indexes").on("click", function() { showTableIndexes(); });
|
$("#table_indexes").on("click", function() { showTableIndexes(); });
|
||||||
$("#table_history").on("click", function() { showQueryHistory(); });
|
$("#table_constraints").on("click", function() { showTableConstraints(); });
|
||||||
$("#table_query").on("click", function() { showQueryPanel(); });
|
$("#table_history").on("click", function() { showQueryHistory(); });
|
||||||
$("#table_connection").on("click", function() { showConnectionPanel(); });
|
$("#table_query").on("click", function() { showQueryPanel(); });
|
||||||
$("#table_activity").on("click", function() { showActivityPanel(); });
|
$("#table_connection").on("click", function() { showConnectionPanel(); });
|
||||||
|
$("#table_activity").on("click", function() { showActivityPanel(); });
|
||||||
|
|
||||||
$("#run").on("click", function() {
|
$("#run").on("click", function() {
|
||||||
runQuery();
|
runQuery();
|
||||||
@@ -441,9 +527,13 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#csv").on("click", function() {
|
$("#csv").on("click", function() {
|
||||||
exportToCSV();
|
exportTo("csv");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#json").on("click", function() {
|
||||||
|
exportTo("json");
|
||||||
|
})
|
||||||
|
|
||||||
$("#results").on("click", "tr", function() {
|
$("#results").on("click", "tr", function() {
|
||||||
$("#results tr.selected").removeClass();
|
$("#results tr.selected").removeClass();
|
||||||
$(this).addClass("selected");
|
$(this).addClass("selected");
|
||||||
@@ -489,6 +579,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
$("#tables").on("click", "li", function() {
|
$("#tables").on("click", "li", function() {
|
||||||
$("#tables li.selected").removeClass("selected");
|
$("#tables li.selected").removeClass("selected");
|
||||||
|
$("#sequences li.selected").removeClass("selected");
|
||||||
$(this).addClass("selected");
|
$(this).addClass("selected");
|
||||||
$("#tables").attr("data-current", $.trim($(this).text()));
|
$("#tables").attr("data-current", $.trim($(this).text()));
|
||||||
|
|
||||||
@@ -496,8 +587,31 @@ $(document).ready(function() {
|
|||||||
showTableInfo();
|
showTableInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#tables").contextmenu({
|
||||||
|
target: "#tables_context_menu",
|
||||||
|
scopes: "li",
|
||||||
|
onItem: function(context, e) {
|
||||||
|
var el = $(e.target);
|
||||||
|
var table = $.trim($(context[0]).text());
|
||||||
|
var action = el.data("action");
|
||||||
|
performTableAction(table, action, el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#sequences").on("click", "li", function() {
|
||||||
|
$("#tables li.selected").removeClass("selected");
|
||||||
|
$("#sequences li.selected").removeClass("selected");
|
||||||
|
|
||||||
|
$(this).addClass("selected");
|
||||||
|
$("#tables").attr("data-current", $.trim($(this).text()));
|
||||||
|
|
||||||
|
showTableContent();
|
||||||
|
$(".table-information ul").hide();
|
||||||
|
});
|
||||||
|
|
||||||
$("#refresh_tables").on("click", function() {
|
$("#refresh_tables").on("click", function() {
|
||||||
loadTables();
|
loadTables();
|
||||||
|
loadSequences();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#edit_connection").on("click", function() {
|
$("#edit_connection").on("click", function() {
|
||||||
@@ -596,6 +710,7 @@ $(document).ready(function() {
|
|||||||
else {
|
else {
|
||||||
connected = true;
|
connected = true;
|
||||||
loadTables();
|
loadTables();
|
||||||
|
loadSequences();
|
||||||
|
|
||||||
$("#connection_window").hide();
|
$("#connection_window").hide();
|
||||||
$("#current_database").text(resp.current_database);
|
$("#current_database").text(resp.current_database);
|
||||||
@@ -615,6 +730,7 @@ $(document).ready(function() {
|
|||||||
else {
|
else {
|
||||||
connected = true;
|
connected = true;
|
||||||
loadTables();
|
loadTables();
|
||||||
|
loadSequences();
|
||||||
|
|
||||||
$("#current_database").text(resp.current_database);
|
$("#current_database").text(resp.current_database);
|
||||||
$("#main").show();
|
$("#main").show();
|
||||||
|
|||||||
Vendored
+205
@@ -0,0 +1,205 @@
|
|||||||
|
/*!
|
||||||
|
* Bootstrap Context Menu
|
||||||
|
* Author: @sydcanem
|
||||||
|
* https://github.com/sydcanem/bootstrap-contextmenu
|
||||||
|
*
|
||||||
|
* Inspired by Bootstrap's dropdown plugin.
|
||||||
|
* Bootstrap (http://getbootstrap.com).
|
||||||
|
*
|
||||||
|
* Licensed under MIT
|
||||||
|
* ========================================================= */
|
||||||
|
|
||||||
|
;(function($) {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/* CONTEXTMENU CLASS DEFINITION
|
||||||
|
* ============================ */
|
||||||
|
var toggle = '[data-toggle="context"]';
|
||||||
|
|
||||||
|
var ContextMenu = function (element, options) {
|
||||||
|
this.$element = $(element);
|
||||||
|
|
||||||
|
this.before = options.before || this.before;
|
||||||
|
this.onItem = options.onItem || this.onItem;
|
||||||
|
this.scopes = options.scopes || null;
|
||||||
|
|
||||||
|
if (options.target) {
|
||||||
|
this.$element.data('target', options.target);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listen();
|
||||||
|
};
|
||||||
|
|
||||||
|
ContextMenu.prototype = {
|
||||||
|
|
||||||
|
constructor: ContextMenu
|
||||||
|
,show: function(e) {
|
||||||
|
|
||||||
|
var $menu
|
||||||
|
, evt
|
||||||
|
, tp
|
||||||
|
, items
|
||||||
|
, relatedTarget = { relatedTarget: this, target: e.currentTarget };
|
||||||
|
|
||||||
|
if (this.isDisabled()) return;
|
||||||
|
|
||||||
|
this.closemenu();
|
||||||
|
|
||||||
|
if (this.before.call(this,e,$(e.currentTarget)) === false) return;
|
||||||
|
|
||||||
|
$menu = this.getMenu();
|
||||||
|
$menu.trigger(evt = $.Event('show.bs.context', relatedTarget));
|
||||||
|
|
||||||
|
tp = this.getPosition(e, $menu);
|
||||||
|
items = 'li:not(.divider)';
|
||||||
|
$menu.attr('style', '')
|
||||||
|
.css(tp)
|
||||||
|
.addClass('open')
|
||||||
|
.on('click.context.data-api', items, $.proxy(this.onItem, this, $(e.currentTarget)))
|
||||||
|
.trigger('shown.bs.context', relatedTarget);
|
||||||
|
|
||||||
|
// Delegating the `closemenu` only on the currently opened menu.
|
||||||
|
// This prevents other opened menus from closing.
|
||||||
|
$('html')
|
||||||
|
.on('click.context.data-api', $menu.selector, $.proxy(this.closemenu, this));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
,closemenu: function(e) {
|
||||||
|
var $menu
|
||||||
|
, evt
|
||||||
|
, items
|
||||||
|
, relatedTarget;
|
||||||
|
|
||||||
|
$menu = this.getMenu();
|
||||||
|
|
||||||
|
if(!$menu.hasClass('open')) return;
|
||||||
|
|
||||||
|
relatedTarget = { relatedTarget: this };
|
||||||
|
$menu.trigger(evt = $.Event('hide.bs.context', relatedTarget));
|
||||||
|
|
||||||
|
items = 'li:not(.divider)';
|
||||||
|
$menu.removeClass('open')
|
||||||
|
.off('click.context.data-api', items)
|
||||||
|
.trigger('hidden.bs.context', relatedTarget);
|
||||||
|
|
||||||
|
$('html')
|
||||||
|
.off('click.context.data-api', $menu.selector);
|
||||||
|
// Don't propagate click event so other currently
|
||||||
|
// opened menus won't close.
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
,keydown: function(e) {
|
||||||
|
if (e.which == 27) this.closemenu(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
,before: function(e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
,onItem: function(e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
,listen: function () {
|
||||||
|
this.$element.on('contextmenu.context.data-api', this.scopes, $.proxy(this.show, this));
|
||||||
|
$('html').on('click.context.data-api', $.proxy(this.closemenu, this));
|
||||||
|
$('html').on('keydown.context.data-api', $.proxy(this.keydown, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
,destroy: function() {
|
||||||
|
this.$element.off('.context.data-api').removeData('context');
|
||||||
|
$('html').off('.context.data-api');
|
||||||
|
}
|
||||||
|
|
||||||
|
,isDisabled: function() {
|
||||||
|
return this.$element.hasClass('disabled') ||
|
||||||
|
this.$element.attr('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
,getMenu: function () {
|
||||||
|
var selector = this.$element.data('target')
|
||||||
|
, $menu;
|
||||||
|
|
||||||
|
if (!selector) {
|
||||||
|
selector = this.$element.attr('href');
|
||||||
|
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, ''); //strip for ie7
|
||||||
|
}
|
||||||
|
|
||||||
|
$menu = $(selector);
|
||||||
|
|
||||||
|
return $menu && $menu.length ? $menu : this.$element.find(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
,getPosition: function(e, $menu) {
|
||||||
|
var mouseX = e.clientX
|
||||||
|
, mouseY = e.clientY
|
||||||
|
, boundsX = $(window).width()
|
||||||
|
, boundsY = $(window).height()
|
||||||
|
, menuWidth = $menu.find('.dropdown-menu').outerWidth()
|
||||||
|
, menuHeight = $menu.find('.dropdown-menu').outerHeight()
|
||||||
|
, tp = {"position":"absolute","z-index":9999}
|
||||||
|
, Y, X, parentOffset;
|
||||||
|
|
||||||
|
if (mouseY + menuHeight > boundsY) {
|
||||||
|
Y = {"top": mouseY - menuHeight + $(window).scrollTop()};
|
||||||
|
} else {
|
||||||
|
Y = {"top": mouseY + $(window).scrollTop()};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mouseX + menuWidth > boundsX) && ((mouseX - menuWidth) > 0)) {
|
||||||
|
X = {"left": mouseX - menuWidth + $(window).scrollLeft()};
|
||||||
|
} else {
|
||||||
|
X = {"left": mouseX + $(window).scrollLeft()};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If context-menu's parent is positioned using absolute or relative positioning,
|
||||||
|
// the calculated mouse position will be incorrect.
|
||||||
|
// Adjust the position of the menu by its offset parent position.
|
||||||
|
parentOffset = $menu.offsetParent().offset();
|
||||||
|
X.left = X.left - parentOffset.left;
|
||||||
|
Y.top = Y.top - parentOffset.top;
|
||||||
|
|
||||||
|
return $.extend(tp, Y, X);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/* CONTEXT MENU PLUGIN DEFINITION
|
||||||
|
* ========================== */
|
||||||
|
|
||||||
|
$.fn.contextmenu = function (option,e) {
|
||||||
|
return this.each(function () {
|
||||||
|
var $this = $(this)
|
||||||
|
, data = $this.data('context')
|
||||||
|
, options = (typeof option == 'object') && option;
|
||||||
|
|
||||||
|
if (!data) $this.data('context', (data = new ContextMenu($this, options)));
|
||||||
|
if (typeof option == 'string') data[option].call(data, e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.contextmenu.Constructor = ContextMenu;
|
||||||
|
|
||||||
|
/* APPLY TO STANDARD CONTEXT MENU ELEMENTS
|
||||||
|
* =================================== */
|
||||||
|
|
||||||
|
$(document)
|
||||||
|
.on('contextmenu.context.data-api', function() {
|
||||||
|
$(toggle).each(function () {
|
||||||
|
var data = $(this).data('context');
|
||||||
|
if (!data) return;
|
||||||
|
data.closemenu();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('contextmenu.context.data-api', toggle, function(e) {
|
||||||
|
$(this).contextmenu('show', e);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
}(jQuery));
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ace.define("ace/theme/tomorrow",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-tomorrow",t.cssText=".ace-tomorrow .ace_gutter {background: #f6f6f6;color: #4D4D4C}.ace-tomorrow .ace_print-margin {width: 1px;background: #f6f6f6}.ace-tomorrow {background-color: #FFFFFF;color: #4D4D4C}.ace-tomorrow .ace_cursor {color: #AEAFAD}.ace-tomorrow .ace_marker-layer .ace_selection {background: #D6D6D6}.ace-tomorrow.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #FFFFFF;}.ace-tomorrow .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-tomorrow .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #D1D1D1}.ace-tomorrow .ace_marker-layer .ace_active-line {background: #EFEFEF}.ace-tomorrow .ace_gutter-active-line {background-color : #dcdcdc}.ace-tomorrow .ace_marker-layer .ace_selected-word {border: 1px solid #D6D6D6}.ace-tomorrow .ace_invisible {color: #D1D1D1}.ace-tomorrow .ace_keyword,.ace-tomorrow .ace_meta,.ace-tomorrow .ace_storage,.ace-tomorrow .ace_storage.ace_type,.ace-tomorrow .ace_support.ace_type {color: #8959A8}.ace-tomorrow .ace_keyword.ace_operator {color: #3E999F}.ace-tomorrow .ace_constant.ace_character,.ace-tomorrow .ace_constant.ace_language,.ace-tomorrow .ace_constant.ace_numeric,.ace-tomorrow .ace_keyword.ace_other.ace_unit,.ace-tomorrow .ace_support.ace_constant,.ace-tomorrow .ace_variable.ace_parameter {color: #F5871F}.ace-tomorrow .ace_constant.ace_other {color: #666969}.ace-tomorrow .ace_invalid {color: #FFFFFF;background-color: #C82829}.ace-tomorrow .ace_invalid.ace_deprecated {color: #FFFFFF;background-color: #8959A8}.ace-tomorrow .ace_fold {background-color: #4271AE;border-color: #4D4D4C}.ace-tomorrow .ace_entity.ace_name.ace_function,.ace-tomorrow .ace_support.ace_function,.ace-tomorrow .ace_variable {color: #4271AE}.ace-tomorrow .ace_support.ace_class,.ace-tomorrow .ace_support.ace_type {color: #C99E00}.ace-tomorrow .ace_heading,.ace-tomorrow .ace_markup.ace_heading,.ace-tomorrow .ace_string {color: #718C00}.ace-tomorrow .ace_entity.ace_name.ace_tag,.ace-tomorrow .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow .ace_meta.ace_tag,.ace-tomorrow .ace_string.ace_regexp,.ace-tomorrow .ace_variable {color: #C82829}.ace-tomorrow .ace_comment {color: #8E908C}.ace-tomorrow .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
|
||||||
Reference in New Issue
Block a user