Compare commits

...
90 Commits
Author SHA1 Message Date
Dan Sosedoff 68ceb212b5 Version bump: 0.6.2 2015-07-15 18:21:08 -05:00
Dan Sosedoff e52114e969 Update changelog 2015-07-14 23:05:36 -05:00
Dan Sosedoff e3b7dd013b Merge pull request #93 from sosedoff/travis-containers
Use container-based travis infrastructure
2015-07-14 22:58:54 -05:00
Dan Sosedoff cea3b6b43e Use container-based travis infrastructure 2015-07-14 22:56:40 -05:00
Dan Sosedoff 06d39e0ab7 Merge pull request #92 from sosedoff/postgresql-prefix
Support both postgresql:// and posgres:// url prefix
2015-07-14 22:54:58 -05:00
Dan Sosedoff 785108002e Support both postgresql:// and posgres:// url prefix 2015-07-14 22:42:46 -05:00
Dan Sosedoff c12853d0b0 Regenerate bindata 2015-07-14 22:27:48 -05:00
Dan Sosedoff f1afa0ac33 Merge pull request #87 from Supme/patch-1
Update index.html
2015-07-14 22:27:18 -05:00
Dan Sosedoff 5265016003 Merge pull request #91 from sosedoff/update-pq
Update pg dependency to latest
2015-07-14 22:27:11 -05:00
Dan Sosedoff c6cb3db624 Expose default port in dockerfile 2015-07-14 22:26:32 -05:00
Dan Sosedoff d35f58a2b5 Update pg dependency to latest 2015-07-14 22:11:02 -05:00
Dan Sosedoff d9ada2e58a Skip port tests on windows for now 2015-07-14 21:38:15 -05:00
Dan Sosedoff 4d6c688bf0 Merge pull request #90 from freeformz/patch-2
Some app.json updates to consider
2015-07-14 21:19:55 -05:00
Dan Sosedoff 3eec0947d2 Merge pull request #89 from freeformz/patch-1
Update app.json
2015-07-14 21:19:27 -05:00
Edward Muller 5979272115 Some app.json updates to consider
a) Consider using the secret generator so that deploys don't default to a known secret.
b) tag it with golang
2015-07-14 12:52:28 -07:00
Edward Muller 5fb70ace3c Update app.json
This is no longer needed as we now officially support Go: https://blog.heroku.com/archives/2015/7/7/go_support_now_official_on_heroku
2015-07-14 12:50:32 -07:00
Dan Sosedoff 5e4398ceae Update changelog 2015-06-18 13:15:35 -05:00
Dan Sosedoff 67a9820c98 Print SHA sum for binary when packaging 2015-06-18 13:14:54 -05:00
Dan Sosedoff 665f79d26c Version bump: 0.6.1 2015-06-18 13:10:02 -05:00
Alexey Agafonov 6152819e45 Update index.html 2015-06-10 17:06:28 +03:00
Dan Sosedoff 427a08029a Moar readme 2015-06-03 20:43:22 -05:00
Dan Sosedoff df7ef6d8f0 Tweak screens page 2015-06-03 20:34:45 -05:00
Dan Sosedoff b9b646bb11 Fix readme link 2015-06-03 20:33:01 -05:00
Dan Sosedoff ae62f8ea3d Add separate screens 2015-06-03 20:32:33 -05:00
Dan Sosedoff 0939632399 Update changelog 2015-05-31 20:48:06 -05:00
Dan Sosedoff 4b5a2b96a8 Print SHA256 of the zip archive, not the binary 2015-05-31 20:42:55 -05:00
Dan Sosedoff 79bf430dee Version bump: 0.6.0 2015-05-31 20:40:10 -05:00
Dan Sosedoff 62a168d44f Contributing guidelines 2015-05-20 21:50:22 -05:00
Dan Sosedoff 502c8dae91 Set test command for appveyor 2015-05-19 15:19:12 -05:00
Dan Sosedoff afdfc54802 Update screenshots 2015-05-19 12:57:24 -05:00
Dan Sosedoff 651b65a882 Implement table item context menu
- Add ability to export table contents into CSV
- Add ability to truncate table
- Add ability to delete table
2015-05-19 12:24:59 -05:00
Dan Sosedoff c2290acae3 Add port checking functionality and tests 2015-05-18 17:13:17 -05:00
Dan Sosedoff e1276afd71 Change sidebar color theme to light one 2015-05-18 16:57:53 -05:00
Dan Sosedoff dea0fb8b78 Tweak readme 2015-05-18 16:28:05 -05:00
Dan Sosedoff faebfb3e62 Regenerate assets 2015-05-18 16:22:19 -05:00
Dan Sosedoff 6fd7b7d427 Merge pull request #85 from brianlow/selected-text-1
Run selected text. Implements sosedoff/pgweb#78.
2015-05-18 16:20:38 -05:00
brianlow 385b415676 Run selected text. Implements sosedoff/pgweb#78.
In the Query tab, only run the selected text. If nothing selected, run all text in the editor.
2015-05-18 14:58:03 -06:00
Dan Sosedoff 50f0f5f6dc Shields 2015-05-09 22:12:38 -05:00
Dan Sosedoff 8889847c71 Fix version 2015-05-08 09:00:24 -05:00
Dan Sosedoff d26bb75ebc Tweak packaging script 2015-05-06 21:31:05 -05:00
Dan Sosedoff a65cca44e5 Add script to package binaries 2015-05-06 21:24:15 -05:00
Dan Sosedoff 18b0260d1f Changelog 2015-05-06 21:03:00 -05:00
Dan Sosedoff c9d4ef5e89 Tweak table row styles 2015-05-06 20:58:21 -05:00
Dan Sosedoff c0f3f027ae Move version definition into command package, include build timestamp, add /api/info endpoint 2015-05-05 00:34:23 -05:00
Dan Sosedoff 0bd5360315 Update styles for connection window 2015-05-05 00:15:30 -05:00
Dan Sosedoff 29b46ba59c Update bindata 2015-05-02 20:50:01 -05:00
Dan Sosedoff 04fe0023b7 DRY up api module 2015-05-02 20:32:16 -05:00
Dan Sosedoff cb3c3e0e2e Fix function names in api module 2015-05-02 20:13:04 -05:00
Dan Sosedoff 0ac9d72deb Refactor and simplify asset serving 2015-05-02 20:10:14 -05:00
Dan Sosedoff dd2200bdb8 Tweak appveyor config 2015-04-30 21:24:53 -05:00
Dan Sosedoff ca894c621f Inject git version when building a release 2015-04-30 20:40:06 -05:00
Dan Sosedoff 4224b3d326 Fix docker build and specify bind interface for command 2015-04-30 20:22:08 -05:00
Dan Sosedoff 891f45c1ca Split up api into smaller files 2015-04-30 19:59:48 -05:00
Dan Sosedoff 595f51d76c Add test task to makefile readme 2015-04-30 12:10:25 -05:00
Dan Sosedoff 50cdf99913 Fix all tests 2015-04-30 12:09:29 -05:00
Dan Sosedoff e9f2f8eb83 Test all 2015-04-30 11:53:21 -05:00
Dan Sosedoff 73d2aa8fe1 Check in compiled assets 2015-04-30 11:51:17 -05:00
Dan Sosedoff 579eaf9141 Do not uri encode password for now 2015-04-30 11:51:02 -05:00
Dan Sosedoff c513930e27 Restructure application 2015-04-30 11:47:07 -05:00
Dan Sosedoff 7a75447364 Encode connection password so it could be passed via get request 2015-04-29 20:03:39 -05:00
Dan Sosedoff ec6bb5590d Tweak js formatting, highlight header with sort order 2015-04-28 10:01:34 -05:00
Dan Sosedoff 637e7a4e24 Change default http port to 8081 to not conflict with rethinkdb web ui 2015-04-28 09:49:36 -05:00
Dan Sosedoff 08a70baf3f Remove go tip from travis config for now 2015-04-20 21:30:55 -05:00
Dan Sosedoff bc43ca6f0f Changelog 2015-04-13 09:03:52 -05:00
Dan Sosedoff 65ba084d80 Version bump: 0.5.2 2015-04-13 09:03:30 -05:00
Dan Sosedoff 961ad31c13 Changelog 2015-04-09 08:46:18 -05:00
Dan Sosedoff 40509cc5fd Rebuild bindata.go 2015-04-09 08:36:32 -05:00
Dan Sosedoff 7606a88293 Merge pull request #83 from benjamin-thomas/sort-columns
Sort columns
2015-04-09 08:34:31 -05:00
Benjamin Thomas d3beb57f15 Update bindata 2015-04-09 09:00:58 +02:00
Benjamin Thomas 691106326f Add sort functionality, within content tab 2015-04-09 08:59:10 +02:00
Benjamin Thomas c82205ce4f Remove trailing whitespace 2015-04-09 08:49:17 +02:00
Dan Sosedoff f0552ab72d Add /api/schemas endpoint to list all database schemas 2015-03-30 23:58:04 -05:00
Dan Sosedoff 786081aafa Changelog 2015-03-21 19:51:33 -05:00
Dan Sosedoff 0251a8bb73 Merge pull request #82 from sosedoff/query-activity
Query activity
2015-03-21 19:48:47 -05:00
Dan Sosedoff 2a78500fdf Add activity tab 2015-03-21 19:14:11 -05:00
Dan Sosedoff f68d81ff17 Update assets 2015-03-21 11:46:32 -05:00
Dan Sosedoff 38ecb5ffc4 Add new endpoint /activity that returns active queries 2015-03-21 11:46:14 -05:00
Dan Sosedoff fa8f3a2997 Add docker-release to usage 2015-03-17 00:01:28 -05:00
Dan Sosedoff 00f4413f12 Add docker-release target to makefile 2015-03-16 23:59:46 -05:00
Dan Sosedoff d4d97edeef Update dockerfile base to golang:1.4.2 2015-03-16 23:51:24 -05:00
Dan Sosedoff 14cc8d5c24 Move all test data under ./data dir 2015-03-16 23:41:53 -05:00
Dan Sosedoff cc983f9ad6 Hide sql query when switching to table structure view 2015-03-09 14:03:54 -05:00
Dan Sosedoff 0764b9263d Run travis on 1.4.1/1.4.2 2015-03-04 09:06:48 -06:00
Dan Sosedoff a207b66b7e Fix bookmarks tests 2015-03-04 08:58:53 -06:00
Dan Sosedoff d42bd2e803 Add sample coverage file to gitignore 2015-03-04 08:53:41 -06:00
Dan Sosedoff 27b3cac1e0 Run coverage analysis with tests 2015-03-04 08:53:16 -06:00
Dan Sosedoff 866cdccd49 Make bookmark tests platform-independent 2015-03-03 21:01:20 -06:00
Dan Sosedoff 1e58c94598 Add bookmark tests 2015-03-03 20:44:14 -06:00
Dan Sosedoff 96bcaa5baa Reduce amount of error printing when reading bookmarks 2015-03-03 20:14:18 -06:00
Dan Sosedoff fe9d7e459d Update changelog 2015-02-23 20:17:20 -06:00
63 changed files with 2534 additions and 911 deletions
+1
View File
@@ -2,3 +2,4 @@
pgweb
bin
tmp/
cover.out
+3 -3
View File
@@ -1,12 +1,12 @@
language: go
sudo: false
addons:
postgresql: "9.3"
go:
- 1.3
- 1.4
- tip
- 1.4.1
- 1.4.2
install:
- make setup
+34 -1
View File
@@ -1,4 +1,37 @@
## 0.5.1 - Unreleased
## 0.6.2 - Unreleased
- 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
- Adds a new endpoint /activity that retuns active queries
- Adds tab to view active queries
- Adds column sorting when browsing table contents
- Fixes SQL query view when switching to table structure view
## 0.5.1 - 2015-02-23
- Upgrades Gin framework dependency to 0.5.0
- Fixes server crash if another pgweb server is running
+6
View File
@@ -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
+6 -4
View File
@@ -1,10 +1,12 @@
FROM golang:1.4.1
FROM golang:1.4.2
COPY . /go/src/pgweb
WORKDIR /go/src/pgweb
COPY . /go/src/github.com/sosedoff/pgweb
WORKDIR /go/src/github.com/sosedoff/pgweb
RUN go get github.com/tools/godep
RUN godep restore
RUN godep go build && godep go install
CMD ["pgweb"]
EXPOSE 8081
CMD ["pgweb", "--bind", "0.0.0.0"]
+2 -2
View File
@@ -28,8 +28,8 @@
},
{
"ImportPath": "github.com/lib/pq",
"Comment": "go1.0-cutoff-13-g19eeca3",
"Rev": "19eeca3e30d2577b1761db471ec130810e67f532"
"Comment": "go1.0-cutoff-56-gdc50b6a",
"Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
},
{
"ImportPath": "github.com/mitchellh/go-homedir",
+13 -6
View File
@@ -44,13 +44,20 @@ env:
- PGUSER=postgres
- PQGOSSLTESTS=1
- PQSSLCERTTEST_PATH=$PWD/certs
- PGHOST=127.0.0.1
matrix:
- PGVERSION=9.4
- PGVERSION=9.3
- PGVERSION=9.2
- PGVERSION=9.1
- PGVERSION=9.0
- PGVERSION=8.4
- PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=no
script:
- go test -v ./...
+5
View File
@@ -57,10 +57,13 @@ code still exists in here.
* Brad Fitzpatrick (bradfitz)
* Charlie Melbye (cmelbye)
* Chris Bandy (cbandy)
* Chris Gilling (cgilling)
* Chris Walsh (cwds)
* Dan Sosedoff (sosedoff)
* Daniel Farina (fdr)
* Eric Chlebek (echlebek)
* Eric Garrido (minusnine)
* Eric Urban (hydrogen18)
* Everyone at The Go Team
* Evan Shaw (edsrzf)
* Ewan Chou (coocood)
@@ -94,5 +97,7 @@ code still exists in here.
* Ryan Smith (ryandotsmith)
* Samuel Stauffer (samuel)
* Timothée Peignier (cyberdelia)
* Travis Cline (tmc)
* TruongSinh Tran-Nguyen (truongsinh)
* Yaismel Miranda (ympons)
* notedit (notedit)
+7 -6
View File
@@ -7,7 +7,6 @@ import (
"bytes"
"database/sql"
"database/sql/driver"
"github.com/lib/pq/oid"
"io"
"math/rand"
"net"
@@ -17,6 +16,8 @@ import (
"sync"
"testing"
"time"
"github.com/lib/pq/oid"
)
var (
@@ -324,7 +325,7 @@ var testIntBytes = []byte("1234")
func BenchmarkDecodeInt64(b *testing.B) {
for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testIntBytes, oid.T_int8)
decode(&parameterStatus{}, testIntBytes, oid.T_int8, formatText)
}
}
@@ -332,7 +333,7 @@ var testFloatBytes = []byte("3.14159")
func BenchmarkDecodeFloat64(b *testing.B) {
for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testFloatBytes, oid.T_float8)
decode(&parameterStatus{}, testFloatBytes, oid.T_float8, formatText)
}
}
@@ -340,7 +341,7 @@ var testBoolBytes = []byte{'t'}
func BenchmarkDecodeBool(b *testing.B) {
for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testBoolBytes, oid.T_bool)
decode(&parameterStatus{}, testBoolBytes, oid.T_bool, formatText)
}
}
@@ -357,7 +358,7 @@ var testTimestamptzBytes = []byte("2013-09-17 22:15:32.360754-07")
func BenchmarkDecodeTimestamptz(b *testing.B) {
for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz)
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText)
}
}
@@ -370,7 +371,7 @@ func BenchmarkDecodeTimestamptzMultiThread(b *testing.B) {
f := func(wg *sync.WaitGroup, loops int) {
defer wg.Done()
for i := 0; i < loops; i++ {
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz)
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText)
}
}
+23 -6
View File
@@ -3,6 +3,7 @@ package pq
import (
"bytes"
"encoding/binary"
"github.com/lib/pq/oid"
)
@@ -46,28 +47,44 @@ func (b *readBuf) byte() byte {
return b.next(1)[0]
}
type writeBuf []byte
type writeBuf struct {
buf []byte
pos int
}
func (b *writeBuf) int32(n int) {
x := make([]byte, 4)
binary.BigEndian.PutUint32(x, uint32(n))
*b = append(*b, x...)
b.buf = append(b.buf, x...)
}
func (b *writeBuf) int16(n int) {
x := make([]byte, 2)
binary.BigEndian.PutUint16(x, uint16(n))
*b = append(*b, x...)
b.buf = append(b.buf, x...)
}
func (b *writeBuf) string(s string) {
*b = append(*b, (s + "\000")...)
b.buf = append(b.buf, (s + "\000")...)
}
func (b *writeBuf) byte(c byte) {
*b = append(*b, c)
b.buf = append(b.buf, c)
}
func (b *writeBuf) bytes(v []byte) {
*b = append(*b, v...)
b.buf = append(b.buf, v...)
}
func (b *writeBuf) wrap() []byte {
p := b.buf[b.pos:]
binary.BigEndian.PutUint32(p, uint32(len(p)))
return b.buf
}
func (b *writeBuf) next(c byte) {
p := b.buf[b.pos:]
binary.BigEndian.PutUint32(p, uint32(len(p)))
b.pos = len(b.buf) + 1
b.buf = append(b.buf, c, 0, 0, 0, 0)
}
+470 -197
View File
@@ -10,7 +10,6 @@ import (
"encoding/binary"
"errors"
"fmt"
"github.com/lib/pq/oid"
"io"
"io/ioutil"
"net"
@@ -22,6 +21,8 @@ import (
"strings"
"time"
"unicode"
"github.com/lib/pq/oid"
)
// Common error types
@@ -105,12 +106,49 @@ type conn struct {
// If true, this connection is bad and all public-facing functions should
// return ErrBadConn.
bad bool
// If set, this connection should never use the binary format when
// receiving query results from prepared statements. Only provided for
// debugging.
disablePreparedBinaryResult bool
// Whether to always send []byte parameters over as binary. Enables single
// round-trip mode for non-prepared Query calls.
binaryParameters bool
}
// Handle driver-side settings in parsed connection string.
func (c *conn) handleDriverSettings(o values) (err error) {
boolSetting := func(key string, val *bool) error {
if value := o.Get(key); value != "" {
if value == "yes" {
*val = true
} else if value == "no" {
*val = false
} else {
return fmt.Errorf("unrecognized value %q for %s", value, key)
}
}
return nil
}
err = boolSetting("disable_prepared_binary_result", &c.disablePreparedBinaryResult)
if err != nil {
return err
}
err = boolSetting("binary_parameters", &c.binaryParameters)
if err != nil {
return err
}
return nil
}
func (c *conn) writeBuf(b byte) *writeBuf {
c.scratch[0] = b
w := writeBuf(c.scratch[:5])
return &w
return &writeBuf{
buf: c.scratch[:5],
pos: 1,
}
}
func Open(name string) (_ driver.Conn, err error) {
@@ -118,22 +156,11 @@ func Open(name string) (_ driver.Conn, err error) {
}
func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
defer func() {
// Handle any panics during connection initialization. Note that we
// specifically do *not* want to use errRecover(), as that would turn
// any connection errors into ErrBadConns, hiding the real error
// message from the user.
e := recover()
if e == nil {
// Do nothing
return
}
var ok bool
err, ok = e.(error)
if !ok {
err = fmt.Errorf("pq: unexpected error: %#v", e)
}
}()
// Handle any panics during connection initialization. Note that we
// specifically do *not* want to use errRecover(), as that would turn any
// connection errors into ErrBadConns, hiding the real error message from
// the user.
defer errRecoverNoErrBadConn(&err)
o := make(values)
@@ -151,7 +178,7 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
o.Set(k, v)
}
if strings.HasPrefix(name, "postgres://") {
if strings.HasPrefix(name, "postgres://") || strings.HasPrefix(name, "postgresql://") {
name, err = ParseURL(name)
if err != nil {
return nil, err
@@ -202,27 +229,36 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
}
}
c, err := dial(d, o)
cn := &conn{}
err = cn.handleDriverSettings(o)
if err != nil {
return nil, err
}
cn := &conn{c: c}
cn.c, err = dial(d, o)
if err != nil {
return nil, err
}
cn.ssl(o)
cn.buf = bufio.NewReader(cn.c)
cn.startup(o)
// reset the deadline, in case one was set (see dial)
err = cn.c.SetDeadline(time.Time{})
if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" {
err = cn.c.SetDeadline(time.Time{})
}
return cn, err
}
func dial(d Dialer, o values) (net.Conn, error) {
ntw, addr := network(o)
timeout := o.Get("connect_timeout")
// SSL is not necessary or supported over UNIX domain sockets
if ntw == "unix" {
o["sslmode"] = "disable"
}
// Zero or not specified means wait indefinitely.
if timeout != "" && timeout != "0" {
if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" {
seconds, err := strconv.ParseInt(timeout, 10, 0)
if err != nil {
return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err)
@@ -436,6 +472,9 @@ func (cn *conn) Commit() (err error) {
_, commandTag, err := cn.simpleExec("COMMIT")
if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err
}
if commandTag != "COMMIT" {
@@ -455,6 +494,9 @@ func (cn *conn) Rollback() (err error) {
cn.checkIsInTransaction(true)
_, commandTag, err := cn.simpleExec("ROLLBACK")
if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err
}
if commandTag != "ROLLBACK" {
@@ -494,7 +536,7 @@ func (cn *conn) simpleExec(q string) (res driver.Result, commandTag string, err
}
}
func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
func (cn *conn) simpleQuery(q string) (res *rows, err error) {
defer cn.errRecover(&err)
st := &stmt{cn: cn, name: ""}
@@ -515,7 +557,13 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
cn.bad = true
errorf("unexpected message %q in simple query execution", t)
}
res = &rows{st: st, done: true}
res = &rows{
cn: cn,
colNames: st.colNames,
colTyps: st.colTyps,
colFmts: st.colFmts,
done: true,
}
case 'Z':
cn.processReadyForQuery(r)
// done
@@ -534,8 +582,8 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
case 'T':
// res might be non-nil here if we received a previous
// CommandComplete, but that's fine; just overwrite it
res = &rows{st: st}
st.cols, st.rowTyps = parseMeta(r)
res = &rows{cn: cn}
res.colNames, res.colFmts, res.colTyps = parsePortalRowDescribe(r)
// To work around a bug in QueryRow in Go 1.2 and earlier, wait
// until the first DataRow has been received.
@@ -546,47 +594,74 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
}
}
func (cn *conn) prepareTo(q, stmtName string) (_ *stmt, err error) {
// Decides which column formats to use for a prepared statement. The input is
// an array of type oids, one element per result column.
func decideColumnFormats(colTyps []oid.Oid, forceText bool) (colFmts []format, colFmtData []byte) {
if len(colTyps) == 0 {
return nil, colFmtDataAllText
}
colFmts = make([]format, len(colTyps))
if forceText {
return colFmts, colFmtDataAllText
}
allBinary := true
allText := true
for i, o := range colTyps {
switch o {
// This is the list of types to use binary mode for when receiving them
// through a prepared statement. If a type appears in this list, it
// must also be implemented in binaryDecode in encode.go.
case oid.T_bytea:
fallthrough
case oid.T_int8:
fallthrough
case oid.T_int4:
fallthrough
case oid.T_int2:
colFmts[i] = formatBinary
allText = false
default:
allBinary = false
}
}
if allBinary {
return colFmts, colFmtDataAllBinary
} else if allText {
return colFmts, colFmtDataAllText
} else {
colFmtData = make([]byte, 2+len(colFmts)*2)
binary.BigEndian.PutUint16(colFmtData, uint16(len(colFmts)))
for i, v := range colFmts {
binary.BigEndian.PutUint16(colFmtData[2+i*2:], uint16(v))
}
return colFmts, colFmtData
}
}
func (cn *conn) prepareTo(q, stmtName string) *stmt {
st := &stmt{cn: cn, name: stmtName}
b := cn.writeBuf('P')
b.string(st.name)
b.string(q)
b.int16(0)
cn.send(b)
b = cn.writeBuf('D')
b.next('D')
b.byte('S')
b.string(st.name)
b.next('S')
cn.send(b)
cn.send(cn.writeBuf('S'))
for {
t, r := cn.recv1()
switch t {
case '1':
case 't':
nparams := r.int16()
st.paramTyps = make([]oid.Oid, nparams)
for i := range st.paramTyps {
st.paramTyps[i] = r.oid()
}
case 'T':
st.cols, st.rowTyps = parseMeta(r)
case 'n':
// no data
case 'Z':
cn.processReadyForQuery(r)
return st, err
case 'E':
err = parseError(r)
default:
cn.bad = true
errorf("unexpected describe rows response: %q", t)
}
}
cn.readParseResponse()
st.paramTyps, st.colNames, st.colTyps = cn.readStatementDescribeResponse()
st.colFmts, st.colFmtData = decideColumnFormats(st.colTyps, cn.disablePreparedBinaryResult)
cn.readReadyForQuery()
return st
}
func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) {
@@ -598,7 +673,7 @@ func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) {
if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") {
return cn.prepareCopyIn(q)
}
return cn.prepareTo(q, cn.gname())
return cn.prepareTo(q, cn.gname()), nil
}
func (cn *conn) Close() (err error) {
@@ -630,17 +705,29 @@ func (cn *conn) Query(query string, args []driver.Value) (_ driver.Rows, err err
return cn.simpleQuery(query)
}
st, err := cn.prepareTo(query, "")
if err != nil {
panic(err)
}
if cn.binaryParameters {
cn.sendBinaryModeQuery(query, args)
st.exec(args)
return &rows{st: st}, nil
cn.readParseResponse()
cn.readBindResponse()
rows := &rows{cn: cn}
rows.colNames, rows.colFmts, rows.colTyps = cn.readPortalDescribeResponse()
cn.postExecuteWorkaround()
return rows, nil
} else {
st := cn.prepareTo(query, "")
st.exec(args)
return &rows{
cn: cn,
colNames: st.colNames,
colTyps: st.colTyps,
colFmts: st.colFmts,
}, nil
}
}
// Implement the optional "Execer" interface for one-shot queries
func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err error) {
func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) {
if cn.bad {
return nil, driver.ErrBadConn
}
@@ -654,32 +741,42 @@ func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err er
return r, err
}
// Use the unnamed statement to defer planning until bind
// time, or else value-based selectivity estimates cannot be
// used.
st, err := cn.prepareTo(query, "")
if err != nil {
panic(err)
}
if cn.binaryParameters {
cn.sendBinaryModeQuery(query, args)
r, err := st.Exec(args)
if err != nil {
panic(err)
cn.readParseResponse()
cn.readBindResponse()
cn.readPortalDescribeResponse()
cn.postExecuteWorkaround()
res, _, err = cn.readExecuteResponse("Execute")
return res, err
} else {
// Use the unnamed statement to defer planning until bind
// time, or else value-based selectivity estimates cannot be
// used.
st := cn.prepareTo(query, "")
r, err := st.Exec(args)
if err != nil {
panic(err)
}
return r, err
}
return r, err
}
// Assumes len(*m) is > 5
func (cn *conn) send(m *writeBuf) {
b := (*m)[1:]
binary.BigEndian.PutUint32(b, uint32(len(b)))
_, err := cn.c.Write(m.wrap())
if err != nil {
panic(err)
}
}
if (*m)[0] == 0 {
*m = b
func (cn *conn) sendStartupPacket(m *writeBuf) {
// sanity check
if m.buf[0] != 0 {
panic("oops")
}
_, err := cn.c.Write(*m)
_, err := cn.c.Write((m.wrap())[1:])
if err != nil {
panic(err)
}
@@ -819,7 +916,7 @@ func (cn *conn) ssl(o values) {
w := cn.writeBuf(0)
w.int32(80877103)
cn.send(w)
cn.sendStartupPacket(w)
b := cn.scratch[:1]
_, err := io.ReadFull(cn.c, b)
@@ -956,6 +1053,10 @@ func isDriverSetting(key string) bool {
return true
case "connect_timeout":
return true
case "disable_prepared_binary_result":
return true
case "binary_parameters":
return true
default:
return false
@@ -983,7 +1084,7 @@ func (cn *conn) startup(o values) {
w.string(v)
}
w.string("")
cn.send(w)
cn.sendStartupPacket(w)
for {
t, r := cn.recv()
@@ -1038,13 +1139,26 @@ func (cn *conn) auth(r *readBuf, o values) {
}
}
type format int
const formatText format = 0
const formatBinary format = 1
// One result-column format code with the value 1 (i.e. all binary).
var colFmtDataAllBinary []byte = []byte{0, 1, 0, 1}
// No result-column format codes (i.e. all text).
var colFmtDataAllText []byte = []byte{0, 0}
type stmt struct {
cn *conn
name string
cols []string
rowTyps []oid.Oid
paramTyps []oid.Oid
closed bool
cn *conn
name string
colNames []string
colFmts []format
colFmtData []byte
colTyps []oid.Oid
paramTyps []oid.Oid
closed bool
}
func (st *stmt) Close() (err error) {
@@ -1087,7 +1201,12 @@ func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) {
defer st.cn.errRecover(&err)
st.exec(v)
return &rows{st: st}, nil
return &rows{
cn: st.cn,
colNames: st.colNames,
colTyps: st.colTyps,
colFmts: st.colFmts,
}, nil
}
func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) {
@@ -1097,25 +1216,8 @@ func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) {
defer st.cn.errRecover(&err)
st.exec(v)
for {
t, r := st.cn.recv1()
switch t {
case 'E':
err = parseError(r)
case 'C':
res, _ = st.cn.parseComplete(r.string())
case 'Z':
st.cn.processReadyForQuery(r)
// done
return
case 'T', 'D', 'I':
// ignore any results
default:
st.cn.bad = true
errorf("unknown exec response: %q", t)
}
}
res, _, err = st.cn.readExecuteResponse("simple query")
return res, err
}
func (st *stmt) exec(v []driver.Value) {
@@ -1126,84 +1228,38 @@ func (st *stmt) exec(v []driver.Value) {
errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps))
}
w := st.cn.writeBuf('B')
w.string("")
cn := st.cn
w := cn.writeBuf('B')
w.byte(0) // unnamed portal
w.string(st.name)
w.int16(0)
w.int16(len(v))
for i, x := range v {
if x == nil {
w.int32(-1)
} else {
b := encode(&st.cn.parameterStatus, x, st.paramTyps[i])
w.int32(len(b))
w.bytes(b)
if cn.binaryParameters {
cn.sendBinaryParameters(w, v)
} else {
w.int16(0)
w.int16(len(v))
for i, x := range v {
if x == nil {
w.int32(-1)
} else {
b := encode(&cn.parameterStatus, x, st.paramTyps[i])
w.int32(len(b))
w.bytes(b)
}
}
}
w.int16(0)
st.cn.send(w)
w.bytes(st.colFmtData)
w = st.cn.writeBuf('E')
w.string("")
w.next('E')
w.byte(0)
w.int32(0)
st.cn.send(w)
st.cn.send(st.cn.writeBuf('S'))
w.next('S')
cn.send(w)
var err error
for {
t, r := st.cn.recv1()
switch t {
case 'E':
err = parseError(r)
case '2':
if err != nil {
panic(err)
}
goto workaround
case 'Z':
st.cn.processReadyForQuery(r)
if err != nil {
panic(err)
}
return
default:
st.cn.bad = true
errorf("unexpected bind response: %q", t)
}
}
cn.readBindResponse()
cn.postExecuteWorkaround()
// Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores
// any errors from rows.Next, which masks errors that happened during the
// execution of the query. To avoid the problem in common cases, we wait
// here for one more message from the database. If it's not an error the
// query will likely succeed (or perhaps has already, if it's a
// CommandComplete), so we push the message into the conn struct; recv1
// will return it as the next message for rows.Next or rows.Close.
// However, if it's an error, we wait until ReadyForQuery and then return
// the error to our caller.
workaround:
for {
t, r := st.cn.recv1()
switch t {
case 'E':
err = parseError(r)
case 'C', 'D', 'I':
// the query didn't fail, but we can't process this message
st.cn.saveMessage(t, r)
return
case 'Z':
if err == nil {
st.cn.bad = true
errorf("unexpected ReadyForQuery during extended query execution")
}
st.cn.processReadyForQuery(r)
panic(err)
default:
st.cn.bad = true
errorf("unexpected message during query execution: %q", t)
}
}
}
func (st *stmt) NumInput() int {
@@ -1260,9 +1316,12 @@ func (cn *conn) parseComplete(commandTag string) (driver.Result, string) {
}
type rows struct {
st *stmt
done bool
rb readBuf
cn *conn
colNames []string
colTyps []oid.Oid
colFmts []format
done bool
rb readBuf
}
func (rs *rows) Close() error {
@@ -1280,7 +1339,7 @@ func (rs *rows) Close() error {
}
func (rs *rows) Columns() []string {
return rs.st.cols
return rs.colNames
}
func (rs *rows) Next(dest []driver.Value) (err error) {
@@ -1288,7 +1347,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) {
return io.EOF
}
conn := rs.st.cn
conn := rs.cn
if conn.bad {
return driver.ErrBadConn
}
@@ -1319,7 +1378,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) {
dest[i] = nil
continue
}
dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.st.rowTyps[i])
dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.colTyps[i], rs.colFmts[i])
}
return
default:
@@ -1352,6 +1411,68 @@ func md5s(s string) string {
return fmt.Sprintf("%x", h.Sum(nil))
}
func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) {
// Do one pass over the parameters to see if we're going to send any of
// them over in binary. If we are, create a paramFormats array at the
// same time.
var paramFormats []int
for i, x := range args {
_, ok := x.([]byte)
if ok {
if paramFormats == nil {
paramFormats = make([]int, len(args))
}
paramFormats[i] = 1
}
}
if paramFormats == nil {
b.int16(0)
} else {
b.int16(len(paramFormats))
for _, x := range paramFormats {
b.int16(x)
}
}
b.int16(len(args))
for _, x := range args {
if x == nil {
b.int32(-1)
} else {
datum := binaryEncode(&cn.parameterStatus, x)
b.int32(len(datum))
b.bytes(datum)
}
}
}
func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) {
if len(args) >= 65536 {
errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args))
}
b := cn.writeBuf('P')
b.byte(0) // unnamed statement
b.string(query)
b.int16(0)
b.next('B')
b.int16(0) // unnamed portal and statement
cn.sendBinaryParameters(b, args)
b.bytes(colFmtDataAllText)
b.next('D')
b.byte('P')
b.byte(0) // unnamed portal
b.next('E')
b.byte(0)
b.int32(0)
b.next('S')
cn.send(b)
}
func (c *conn) processParameterStatus(r *readBuf) {
var err error
@@ -1381,15 +1502,167 @@ func (c *conn) processReadyForQuery(r *readBuf) {
c.txnStatus = transactionStatus(r.byte())
}
func parseMeta(r *readBuf) (cols []string, rowTyps []oid.Oid) {
func (cn *conn) readReadyForQuery() {
t, r := cn.recv1()
switch t {
case 'Z':
cn.processReadyForQuery(r)
return
default:
cn.bad = true
errorf("unexpected message %q; expected ReadyForQuery", t)
}
}
func (cn *conn) readParseResponse() {
t, r := cn.recv1()
switch t {
case '1':
return
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Parse response %q", t)
}
}
func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames []string, colTyps []oid.Oid) {
for {
t, r := cn.recv1()
switch t {
case 't':
nparams := r.int16()
paramTyps = make([]oid.Oid, nparams)
for i := range paramTyps {
paramTyps[i] = r.oid()
}
case 'n':
return paramTyps, nil, nil
case 'T':
colNames, colTyps = parseStatementRowDescribe(r)
return paramTyps, colNames, colTyps
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Describe statement response %q", t)
}
}
}
func (cn *conn) readPortalDescribeResponse() (colNames []string, colFmts []format, colTyps []oid.Oid) {
t, r := cn.recv1()
switch t {
case 'T':
return parsePortalRowDescribe(r)
case 'n':
return nil, nil, nil
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Describe response %q", t)
}
panic("not reached")
}
func (cn *conn) readBindResponse() {
t, r := cn.recv1()
switch t {
case '2':
return
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Bind response %q", t)
}
}
func (cn *conn) postExecuteWorkaround() {
// Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores
// any errors from rows.Next, which masks errors that happened during the
// execution of the query. To avoid the problem in common cases, we wait
// here for one more message from the database. If it's not an error the
// query will likely succeed (or perhaps has already, if it's a
// CommandComplete), so we push the message into the conn struct; recv1
// will return it as the next message for rows.Next or rows.Close.
// However, if it's an error, we wait until ReadyForQuery and then return
// the error to our caller.
for {
t, r := cn.recv1()
switch t {
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
case 'C', 'D', 'I':
// the query didn't fail, but we can't process this message
cn.saveMessage(t, r)
return
default:
cn.bad = true
errorf("unexpected message during extended query execution: %q", t)
}
}
}
// Only for Exec(), since we ignore the returned data
func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, commandTag string, err error) {
for {
t, r := cn.recv1()
switch t {
case 'C':
res, commandTag = cn.parseComplete(r.string())
case 'Z':
cn.processReadyForQuery(r)
return res, commandTag, err
case 'E':
err = parseError(r)
case 'T', 'D', 'I':
// ignore any results
default:
cn.bad = true
errorf("unknown %s response: %q", protocolState, t)
}
}
}
func parseStatementRowDescribe(r *readBuf) (colNames []string, colTyps []oid.Oid) {
n := r.int16()
cols = make([]string, n)
rowTyps = make([]oid.Oid, n)
for i := range cols {
cols[i] = r.string()
colNames = make([]string, n)
colTyps = make([]oid.Oid, n)
for i := range colNames {
colNames[i] = r.string()
r.next(6)
rowTyps[i] = r.oid()
r.next(8)
colTyps[i] = r.oid()
r.next(6)
// format code not known when describing a statement; always 0
r.next(2)
}
return
}
func parsePortalRowDescribe(r *readBuf) (colNames []string, colFmts []format, colTyps []oid.Oid) {
n := r.int16()
colNames = make([]string, n)
colFmts = make([]format, n)
colTyps = make([]oid.Oid, n)
for i := range colNames {
colNames[i] = r.string()
r.next(6)
colTyps[i] = r.oid()
r.next(6)
colFmts[i] = format(r.int16())
}
return
}
+47 -27
View File
@@ -7,6 +7,7 @@ import (
"io"
"os"
"reflect"
"strings"
"testing"
"time"
)
@@ -15,21 +16,31 @@ type Fatalistic interface {
Fatal(args ...interface{})
}
func forceBinaryParameters() bool {
bp := os.Getenv("PQTEST_BINARY_PARAMETERS")
if bp == "yes" {
return true
} else if bp == "" || bp == "no" {
return false
} else {
panic("unexpected value for PQTEST_BINARY_PARAMETERS")
}
}
func openTestConnConninfo(conninfo string) (*sql.DB, error) {
datname := os.Getenv("PGDATABASE")
sslmode := os.Getenv("PGSSLMODE")
timeout := os.Getenv("PGCONNECT_TIMEOUT")
if datname == "" {
os.Setenv("PGDATABASE", "pqgotest")
defaultTo := func(envvar string, value string) {
if os.Getenv(envvar) == "" {
os.Setenv(envvar, value)
}
}
defaultTo("PGDATABASE", "pqgotest")
defaultTo("PGSSLMODE", "disable")
defaultTo("PGCONNECT_TIMEOUT", "20")
if sslmode == "" {
os.Setenv("PGSSLMODE", "disable")
}
if timeout == "" {
os.Setenv("PGCONNECT_TIMEOUT", "20")
if forceBinaryParameters() &&
!strings.HasPrefix(conninfo, "postgres://") &&
!strings.HasPrefix(conninfo, "postgresql://") {
conninfo = conninfo + " binary_parameters=yes"
}
return sql.Open("postgres", conninfo)
@@ -106,18 +117,22 @@ func TestCommitInFailedTransaction(t *testing.T) {
}
func TestOpenURL(t *testing.T) {
db, err := openTestConnConninfo("postgres://")
if err != nil {
t.Fatal(err)
testURL := func(url string) {
db, err := openTestConnConninfo(url)
if err != nil {
t.Fatal(err)
}
defer db.Close()
// database/sql might not call our Open at all unless we do something with
// the connection
txn, err := db.Begin()
if err != nil {
t.Fatal(err)
}
txn.Rollback()
}
defer db.Close()
// database/sql might not call our Open at all unless we do something with
// the connection
txn, err := db.Begin()
if err != nil {
t.Fatal(err)
}
txn.Rollback()
testURL("postgres://")
testURL("postgresql://")
}
func TestExec(t *testing.T) {
@@ -342,6 +357,7 @@ func TestEncodeDecode(t *testing.T) {
'2000-1-1 01:02:03.04-7'::timestamptz,
0::boolean,
123,
-321,
3.14::float8
WHERE
E'\\000\\001\\002'::bytea = $1
@@ -370,9 +386,9 @@ func TestEncodeDecode(t *testing.T) {
var got2 string
var got3 = sql.NullInt64{Valid: true}
var got4 time.Time
var got5, got6, got7 interface{}
var got5, got6, got7, got8 interface{}
err = r.Scan(&got1, &got2, &got3, &got4, &got5, &got6, &got7)
err = r.Scan(&got1, &got2, &got3, &got4, &got5, &got6, &got7, &got8)
if err != nil {
t.Fatal(err)
}
@@ -401,8 +417,12 @@ func TestEncodeDecode(t *testing.T) {
t.Fatalf("expected 123, got %d", got6)
}
if got7 != float64(3.14) {
t.Fatalf("expected 3.14, got %f", got7)
if got7 != int64(-321) {
t.Fatalf("expected -321, got %d", got7)
}
if got8 != float64(3.14) {
t.Fatalf("expected 3.14, got %f", got8)
}
}
+2
View File
@@ -150,6 +150,8 @@ func (ci *copyin) resploop() {
switch t {
case 'C':
// complete
case 'N':
// NoticeResponse
case 'Z':
ci.cn.processReadyForQuery(&r)
ci.done <- true
+88 -6
View File
@@ -94,6 +94,86 @@ func TestCopyInMultipleValues(t *testing.T) {
}
}
func TestCopyInRaiseStmtTrigger(t *testing.T) {
db := openTestConn(t)
defer db.Close()
if getServerVersion(t, db) < 90000 {
var exists int
err := db.QueryRow("SELECT 1 FROM pg_language WHERE lanname = 'plpgsql'").Scan(&exists)
if err == sql.ErrNoRows {
t.Skip("language PL/PgSQL does not exist; skipping TestCopyInRaiseStmtTrigger")
} else if err != nil {
t.Fatal(err)
}
}
txn, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer txn.Rollback()
_, err = txn.Exec("CREATE TEMP TABLE temp (a int, b varchar)")
if err != nil {
t.Fatal(err)
}
_, err = txn.Exec(`
CREATE OR REPLACE FUNCTION pg_temp.temptest()
RETURNS trigger AS
$BODY$ begin
raise notice 'Hello world';
return new;
end $BODY$
LANGUAGE plpgsql`)
if err != nil {
t.Fatal(err)
}
_, err = txn.Exec(`
CREATE TRIGGER temptest_trigger
BEFORE INSERT
ON temp
FOR EACH ROW
EXECUTE PROCEDURE pg_temp.temptest()`)
if err != nil {
t.Fatal(err)
}
stmt, err := txn.Prepare(CopyIn("temp", "a", "b"))
if err != nil {
t.Fatal(err)
}
longString := strings.Repeat("#", 500)
_, err = stmt.Exec(int64(1), longString)
if err != nil {
t.Fatal(err)
}
_, err = stmt.Exec()
if err != nil {
t.Fatal(err)
}
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
var num int
err = txn.QueryRow("SELECT COUNT(*) FROM temp").Scan(&num)
if err != nil {
t.Fatal(err)
}
if num != 1 {
t.Fatalf("expected 1 items, not %d", num)
}
}
func TestCopyInTypes(t *testing.T) {
db := openTestConn(t)
defer db.Close()
@@ -307,12 +387,14 @@ func TestCopyRespLoopConnectionError(t *testing.T) {
t.Fatal(err)
}
// We have to try and send something over, since postgres won't process
// SIGTERMs while it's waiting for CopyData/CopyEnd messages; see
// tcop/postgres.c.
_, err = stmt.Exec(1)
if err != nil {
t.Fatal(err)
if getServerVersion(t, db) < 90500 {
// We have to try and send something over, since postgres before
// version 9.5 won't process SIGTERMs while it's waiting for
// CopyData/CopyEnd messages; see tcop/postgres.c.
_, err = stmt.Exec(1)
if err != nil {
t.Fatal(err)
}
}
_, err = stmt.Exec()
if err == nil {
+2 -1
View File
@@ -5,8 +5,9 @@ In most cases clients will use the database/sql package instead of
using this package directly. For example:
import (
_ "github.com/lib/pq"
"database/sql"
_ "github.com/lib/pq"
)
func main() {
+135 -34
View File
@@ -3,24 +3,34 @@ package pq
import (
"bytes"
"database/sql/driver"
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/lib/pq/oid"
"math"
"strconv"
"strings"
"sync"
"time"
"github.com/lib/pq/oid"
)
func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte {
switch v := x.(type) {
case []byte:
return v
default:
return encode(parameterStatus, x, oid.T_unknown)
}
panic("not reached")
}
func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte {
switch v := x.(type) {
case int64:
return []byte(fmt.Sprintf("%d", v))
case float32:
return []byte(fmt.Sprintf("%.9f", v))
return strconv.AppendInt(nil, v, 10)
case float64:
return []byte(fmt.Sprintf("%.17f", v))
return strconv.AppendFloat(nil, v, 'f', -1, 64)
case []byte:
if pgtypOid == oid.T_bytea {
return encodeBytea(parameterStatus.serverVersion, v)
@@ -34,7 +44,7 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [
return []byte(v)
case bool:
return []byte(fmt.Sprintf("%t", v))
return strconv.AppendBool(nil, v)
case time.Time:
return formatTs(v)
@@ -45,7 +55,33 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [
panic("not reached")
}
func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} {
func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid, f format) interface{} {
if f == formatBinary {
return binaryDecode(parameterStatus, s, typ)
} else {
return textDecode(parameterStatus, s, typ)
}
}
func binaryDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} {
switch typ {
case oid.T_bytea:
return s
case oid.T_int8:
return int64(binary.BigEndian.Uint64(s))
case oid.T_int4:
return int64(int32(binary.BigEndian.Uint32(s)))
case oid.T_int2:
return int64(int16(binary.BigEndian.Uint16(s)))
default:
errorf("don't know how to decode binary parameter of type %u", uint32(typ))
}
panic("not reached")
}
func textDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} {
switch typ {
case oid.T_bytea:
return parseBytea(s)
@@ -59,7 +95,7 @@ func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{}
return mustParse("15:04:05-07", typ, s)
case oid.T_bool:
return s[0] == 't'
case oid.T_int8, oid.T_int2, oid.T_int4:
case oid.T_int8, oid.T_int4, oid.T_int2:
i, err := strconv.ParseInt(string(s), 10, 64)
if err != nil {
errorf("%s", err)
@@ -86,8 +122,6 @@ func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface
switch v := x.(type) {
case int64:
return strconv.AppendInt(buf, v, 10)
case float32:
return strconv.AppendFloat(buf, float64(v), 'f', -1, 32)
case float64:
return strconv.AppendFloat(buf, v, 'f', -1, 64)
case []byte:
@@ -149,12 +183,6 @@ func appendEscapedText(buf []byte, text string) []byte {
func mustParse(f string, typ oid.Oid, s []byte) time.Time {
str := string(s)
// Special case until time.Parse bug is fixed:
// http://code.google.com/p/go/issues/detail?id=3487
if str[len(str)-2] == '.' {
str += "0"
}
// check for a 30-minute-offset timezone
if (typ == oid.T_timestamptz || typ == oid.T_timetz) &&
str[len(str)-3] == ':' {
@@ -212,11 +240,72 @@ func (c *locationCache) getLocation(offset int) *time.Location {
return location
}
var infinityTsEnabled = false
var infinityTsNegative time.Time
var infinityTsPositive time.Time
const (
infinityTsEnabledAlready = "pq: infinity timestamp enabled already"
infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive"
)
/*
* If EnableInfinityTs is not called, "-infinity" and "infinity" will return
* []byte("-infinity") and []byte("infinity") respectively, and potentially
* cause error "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time",
* when scanning into a time.Time value.
*
* Once EnableInfinityTs has been called, all connections created using this
* driver will decode Postgres' "-infinity" and "infinity" for "timestamp",
* "timestamp with time zone" and "date" types to the predefined minimum and
* maximum times, respectively. When encoding time.Time values, any time which
* equals or preceeds the predefined minimum time will be encoded to
* "-infinity". Any values at or past the maximum time will similarly be
* encoded to "infinity".
*
*
* If EnableInfinityTs is called with negative >= positive, it will panic.
* Calling EnableInfinityTs after a connection has been established results in
* undefined behavior. If EnableInfinityTs is called more than once, it will
* panic.
*/
func EnableInfinityTs(negative time.Time, positive time.Time) {
if infinityTsEnabled {
panic(infinityTsEnabledAlready)
}
if !negative.Before(positive) {
panic(infinityTsNegativeMustBeSmaller)
}
infinityTsEnabled = true
infinityTsNegative = negative
infinityTsPositive = positive
}
/*
* Testing might want to toggle infinityTsEnabled
*/
func disableInfinityTs() {
infinityTsEnabled = false
}
// This is a time function specific to the Postgres default DateStyle
// setting ("ISO, MDY"), the only one we currently support. This
// accounts for the discrepancies between the parsing available with
// time.Parse and the Postgres date formatting quirks.
func parseTs(currentLocation *time.Location, str string) (result time.Time) {
func parseTs(currentLocation *time.Location, str string) interface{} {
switch str {
case "-infinity":
if infinityTsEnabled {
return infinityTsNegative
}
return []byte(str)
case "infinity":
if infinityTsEnabled {
return infinityTsPositive
}
return []byte(str)
}
monSep := strings.IndexRune(str, '-')
// this is Gregorian year, not ISO Year
// In Gregorian system, the year 1 BC is followed by AD 1
@@ -310,10 +399,18 @@ func parseTs(currentLocation *time.Location, str string) (result time.Time) {
return t
}
// formatTs formats t as time.RFC3339Nano and appends time zone seconds if
// needed.
// formatTs formats t into a format postgres understands.
func formatTs(t time.Time) (b []byte) {
b = []byte(t.Format(time.RFC3339Nano))
if infinityTsEnabled {
// t <= -infinity : ! (t > -infinity)
if !t.After(infinityTsNegative) {
return []byte("-infinity")
}
// t >= infinity : ! (!t < infinity)
if !t.Before(infinityTsPositive) {
return []byte("infinity")
}
}
// Need to send dates before 0001 A.D. with " BC" suffix, instead of the
// minus sign preferred by Go.
// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on
@@ -324,25 +421,26 @@ func formatTs(t time.Time) (b []byte) {
bc = true
}
b = []byte(t.Format(time.RFC3339Nano))
if bc {
b = append(b, " BC"...)
}
_, offset := t.Zone()
offset = offset % 60
if offset == 0 {
return b
if offset != 0 {
// RFC3339Nano already printed the minus sign
if offset < 0 {
offset = -offset
}
b = append(b, ':')
if offset < 10 {
b = append(b, '0')
}
b = strconv.AppendInt(b, int64(offset), 10)
}
if offset < 0 {
offset = -offset
if bc {
b = append(b, " BC"...)
}
b = append(b, ':')
if offset < 10 {
b = append(b, '0')
}
return strconv.AppendInt(b, int64(offset), 10)
return b
}
// Parse a bytea value received from the server. Both "hex" and the legacy
@@ -397,7 +495,10 @@ func parseBytea(s []byte) (result []byte) {
func encodeBytea(serverVersion int, v []byte) (result []byte) {
if serverVersion >= 90000 {
// Use the hex format if we know that the server supports it
result = []byte(fmt.Sprintf("\\x%x", v))
result = make([]byte, 2+hex.EncodedLen(len(v)))
result[0] = '\\'
result[1] = 'x'
hex.Encode(result[2:], v)
} else {
// .. or resort to "escape"
for _, b := range v {
+300 -14
View File
@@ -1,12 +1,13 @@
package pq
import (
"github.com/lib/pq/oid"
"bytes"
"database/sql"
"fmt"
"testing"
"time"
"github.com/lib/pq/oid"
)
func TestScanTimestamp(t *testing.T) {
@@ -78,7 +79,11 @@ func tryParse(str string) (t time.Time, err error) {
return
}
}()
t = parseTs(nil, str)
i := parseTs(nil, str)
t, ok := i.(time.Time)
if !ok {
err = fmt.Errorf("Not a time.Time type, got %#v", i)
}
return
}
@@ -132,8 +137,18 @@ var formatTimeTests = []struct {
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"},
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"},
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"},
{time.Date(1, time.January, 1, 0, 0, 0, 0, time.FixedZone("", 19*60+32)), "0001-01-01T00:00:00+00:19:32"},
{time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z BC"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00 BC"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00 BC"},
{time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09"},
{time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09 BC"},
}
func TestFormatTs(t *testing.T) {
@@ -249,6 +264,131 @@ func TestTimestampWithOutTimezone(t *testing.T) {
test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033")
}
func TestInfinityTimestamp(t *testing.T) {
db := openTestConn(t)
defer db.Close()
var err error
var resultT time.Time
expectedError := fmt.Errorf(`sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time`)
type testCases []struct {
Query string
Param string
ExpectedErr error
ExpectedVal interface{}
}
tc := testCases{
{"SELECT $1::timestamp", "-infinity", expectedError, "-infinity"},
{"SELECT $1::timestamptz", "-infinity", expectedError, "-infinity"},
{"SELECT $1::timestamp", "infinity", expectedError, "infinity"},
{"SELECT $1::timestamptz", "infinity", expectedError, "infinity"},
}
// try to assert []byte to time.Time
for _, q := range tc {
err = db.QueryRow(q.Query, q.Param).Scan(&resultT)
if err.Error() != q.ExpectedErr.Error() {
t.Errorf("Scanning -/+infinity, expected error, %q, got %q", q.ExpectedErr, err)
}
}
// yield []byte
for _, q := range tc {
var resultI interface{}
err = db.QueryRow(q.Query, q.Param).Scan(&resultI)
if err != nil {
t.Errorf("Scanning -/+infinity, expected no error, got %q", err)
}
result, ok := resultI.([]byte)
if !ok {
t.Errorf("Scanning -/+infinity, expected []byte, got %#v", resultI)
}
if string(result) != q.ExpectedVal {
t.Errorf("Scanning -/+infinity, expected %q, got %q", q.ExpectedVal, result)
}
}
y1500 := time.Date(1500, time.January, 1, 0, 0, 0, 0, time.UTC)
y2500 := time.Date(2500, time.January, 1, 0, 0, 0, 0, time.UTC)
EnableInfinityTs(y1500, y2500)
err = db.QueryRow("SELECT $1::timestamp", "infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning infinity, expected no error, got %q", err)
}
if !resultT.Equal(y2500) {
t.Errorf("Scanning infinity, expected %q, got %q", y2500, resultT)
}
err = db.QueryRow("SELECT $1::timestamptz", "infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning infinity, expected no error, got %q", err)
}
if !resultT.Equal(y2500) {
t.Errorf("Scanning Infinity, expected time %q, got %q", y2500, resultT.String())
}
err = db.QueryRow("SELECT $1::timestamp", "-infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning -infinity, expected no error, got %q", err)
}
if !resultT.Equal(y1500) {
t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String())
}
err = db.QueryRow("SELECT $1::timestamptz", "-infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning -infinity, expected no error, got %q", err)
}
if !resultT.Equal(y1500) {
t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String())
}
y_1500 := time.Date(-1500, time.January, 1, 0, 0, 0, 0, time.UTC)
y11500 := time.Date(11500, time.January, 1, 0, 0, 0, 0, time.UTC)
var s string
err = db.QueryRow("SELECT $1::timestamp::text", y_1500).Scan(&s)
if err != nil {
t.Errorf("Encoding -infinity, expected no error, got %q", err)
}
if s != "-infinity" {
t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s)
}
err = db.QueryRow("SELECT $1::timestamptz::text", y_1500).Scan(&s)
if err != nil {
t.Errorf("Encoding -infinity, expected no error, got %q", err)
}
if s != "-infinity" {
t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s)
}
err = db.QueryRow("SELECT $1::timestamp::text", y11500).Scan(&s)
if err != nil {
t.Errorf("Encoding infinity, expected no error, got %q", err)
}
if s != "infinity" {
t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s)
}
err = db.QueryRow("SELECT $1::timestamptz::text", y11500).Scan(&s)
if err != nil {
t.Errorf("Encoding infinity, expected no error, got %q", err)
}
if s != "infinity" {
t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s)
}
disableInfinityTs()
var panicErrorString string
func() {
defer func() {
panicErrorString, _ = recover().(string)
}()
EnableInfinityTs(y2500, y1500)
}()
if panicErrorString != infinityTsNegativeMustBeSmaller {
t.Errorf("Expected error, %q, got %q", infinityTsNegativeMustBeSmaller, panicErrorString)
}
}
func TestStringWithNul(t *testing.T) {
db := openTestConn(t)
defer db.Close()
@@ -261,7 +401,7 @@ func TestStringWithNul(t *testing.T) {
}
}
func TestByteaToText(t *testing.T) {
func TestByteSliceToText(t *testing.T) {
db := openTestConn(t)
defer db.Close()
@@ -279,7 +419,7 @@ func TestByteaToText(t *testing.T) {
}
}
func TestTextToBytea(t *testing.T) {
func TestStringToBytea(t *testing.T) {
db := openTestConn(t)
defer db.Close()
@@ -297,6 +437,136 @@ func TestTextToBytea(t *testing.T) {
}
}
func TestTextByteSliceToUUID(t *testing.T) {
db := openTestConn(t)
defer db.Close()
b := []byte("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
row := db.QueryRow("SELECT $1::uuid", b)
var result string
err := row.Scan(&result)
if forceBinaryParameters() {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22P03" {
t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code)
}
} else {
if err != nil {
t.Fatal(err)
}
if result != string(b) {
t.Fatalf("expected %v but got %v", b, result)
}
}
}
func TestBinaryByteSlicetoUUID(t *testing.T) {
db := openTestConn(t)
defer db.Close()
b := []byte{'\xa0','\xee','\xbc','\x99',
'\x9c', '\x0b',
'\x4e', '\xf8',
'\xbb', '\x00', '\x6b',
'\xb9', '\xbd', '\x38', '\x0a', '\x11'}
row := db.QueryRow("SELECT $1::uuid", b)
var result string
err := row.Scan(&result)
if forceBinaryParameters() {
if err != nil {
t.Fatal(err)
}
if result != string("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11") {
t.Fatalf("expected %v but got %v", b, result)
}
} else {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22021" {
t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code)
}
}
}
func TestStringToUUID(t *testing.T) {
db := openTestConn(t)
defer db.Close()
s := "a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11"
row := db.QueryRow("SELECT $1::uuid", s)
var result string
err := row.Scan(&result)
if err != nil {
t.Fatal(err)
}
if result != s {
t.Fatalf("expected %v but got %v", s, result)
}
}
func TestTextByteSliceToInt(t *testing.T) {
db := openTestConn(t)
defer db.Close()
expected := 12345678
b := []byte(fmt.Sprintf("%d", expected))
row := db.QueryRow("SELECT $1::int", b)
var result int
err := row.Scan(&result)
if forceBinaryParameters() {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22P03" {
t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code)
}
} else {
if err != nil {
t.Fatal(err)
}
if result != expected {
t.Fatalf("expected %v but got %v", expected, result)
}
}
}
func TestBinaryByteSliceToInt(t *testing.T) {
db := openTestConn(t)
defer db.Close()
expected := 12345678
b := []byte{'\x00', '\xbc', '\x61', '\x4e'}
row := db.QueryRow("SELECT $1::int", b)
var result int
err := row.Scan(&result)
if forceBinaryParameters() {
if err != nil {
t.Fatal(err)
}
if result != expected {
t.Fatalf("expected %v but got %v", expected, result)
}
} else {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22021" {
t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code)
}
}
}
func TestByteaOutputFormatEncoding(t *testing.T) {
input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123")
want := []byte("\\x5c78000102fffe6162636465666730313233")
@@ -321,7 +591,7 @@ func TestByteaOutputFormats(t *testing.T) {
return
}
testByteaOutputFormat := func(f string) {
testByteaOutputFormat := func(f string, usePrepared bool) {
expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08")
sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')"
@@ -338,8 +608,18 @@ func TestByteaOutputFormats(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// use Query; QueryRow would hide the actual error
rows, err := txn.Query(sqlQuery)
var rows *sql.Rows
var stmt *sql.Stmt
if usePrepared {
stmt, err = txn.Prepare(sqlQuery)
if err != nil {
t.Fatal(err)
}
rows, err = stmt.Query()
} else {
// use Query; QueryRow would hide the actual error
rows, err = txn.Query(sqlQuery)
}
if err != nil {
t.Fatal(err)
}
@@ -357,13 +637,21 @@ func TestByteaOutputFormats(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if stmt != nil {
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
}
if !bytes.Equal(data, expectedData) {
t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData)
}
}
testByteaOutputFormat("hex")
testByteaOutputFormat("escape")
testByteaOutputFormat("hex", false)
testByteaOutputFormat("escape", false)
testByteaOutputFormat("hex", true)
testByteaOutputFormat("escape", true)
}
func TestAppendEncodedText(t *testing.T) {
@@ -371,15 +659,13 @@ func TestAppendEncodedText(t *testing.T) {
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, int64(10))
buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, float32(42.0000000001))
buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, 42.0000000001)
buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, "hello\tworld")
buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255})
if string(buf) != "10\t42\t42.0000000001\thello\\tworld\t\\\\x0080ff" {
if string(buf) != "10\t42.0000000001\thello\\tworld\t\\\\x0080ff" {
t.Fatal(string(buf))
}
}
+13
View File
@@ -459,6 +459,19 @@ func errorf(s string, args ...interface{}) {
panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)))
}
func errRecoverNoErrBadConn(err *error) {
e := recover()
if e == nil {
// Do nothing
return
}
var ok bool
*err, ok = e.(error)
if !ok {
*err = fmt.Errorf("pq: unexpected error: %#v", e)
}
}
func (c *conn) errRecover(err *error) {
e := recover()
switch v := e.(type) {
+2 -1
View File
@@ -2,9 +2,10 @@ package hstore
import (
"database/sql"
_ "github.com/lib/pq"
"os"
"testing"
_ "github.com/lib/pq"
)
type Fatalistic interface {
+2 -2
View File
@@ -18,11 +18,11 @@ mechanism to avoid polling the database while waiting for more work to arrive.
package main
import (
"github.com/lib/pq"
"database/sql"
"fmt"
"time"
"github.com/lib/pq"
)
func doWork(db *sql.DB, work int64) {
+30 -16
View File
@@ -6,7 +6,6 @@ package pq
import (
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
@@ -87,12 +86,16 @@ func NewListenerConn(name string, notificationChan chan<- *Notification) (*Liste
// Returns an error if an unrecoverable error has occurred and the ListenerConn
// should be abandoned.
func (l *ListenerConn) acquireSenderLock() error {
l.connectionLock.Lock()
defer l.connectionLock.Unlock()
if l.err != nil {
return l.err
}
// we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery
l.senderLock.Lock()
l.connectionLock.Lock()
err := l.err
l.connectionLock.Unlock()
if err != nil {
l.senderLock.Unlock()
return err
}
return nil
}
@@ -125,7 +128,7 @@ func (l *ListenerConn) setState(newState int32) bool {
// away or should be discarded because we couldn't agree on the state with the
// server backend.
func (l *ListenerConn) listenerConnLoop() (err error) {
defer l.cn.errRecover(&err)
defer errRecoverNoErrBadConn(&err)
r := &readBuf{}
for {
@@ -140,6 +143,9 @@ func (l *ListenerConn) listenerConnLoop() (err error) {
// about the scratch buffer being overwritten.
l.notificationChan <- recvNotification(r)
case 'T', 'D':
// only used by tests; ignore
case 'E':
// We might receive an ErrorResponse even when not in a query; it
// is expected that the server will close the connection after
@@ -238,7 +244,7 @@ func (l *ListenerConn) Ping() error {
// The caller must be holding senderLock (see acquireSenderLock and
// releaseSenderLock).
func (l *ListenerConn) sendSimpleQuery(q string) (err error) {
defer l.cn.errRecover(&err)
defer errRecoverNoErrBadConn(&err)
// must set connection state before sending the query
if !l.setState(connStateExpectResponse) {
@@ -247,8 +253,10 @@ func (l *ListenerConn) sendSimpleQuery(q string) (err error) {
// Can't use l.cn.writeBuf here because it uses the scratch buffer which
// might get overwritten by listenerConnLoop.
data := writeBuf([]byte("Q\x00\x00\x00\x00"))
b := &data
b := &writeBuf{
buf: []byte("Q\x00\x00\x00\x00"),
pos: 1,
}
b.string(q)
l.cn.send(b)
@@ -277,13 +285,13 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
// We can't know what state the protocol is in, so we need to abandon
// this connection.
l.connectionLock.Lock()
defer l.connectionLock.Unlock()
// Set the error pointer if it hasn't been set already; see
// listenerConnMain.
if l.err == nil {
l.err = err
}
l.cn.Close()
l.connectionLock.Unlock()
l.cn.c.Close()
return false, err
}
@@ -292,8 +300,11 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
m, ok := <-l.replyChan
if !ok {
// We lost the connection to server, don't bother waiting for a
// a response.
return false, io.EOF
// a response. err should have been set already.
l.connectionLock.Lock()
err := l.err
l.connectionLock.Unlock()
return false, err
}
switch m.typ {
case 'Z':
@@ -320,12 +331,15 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
func (l *ListenerConn) Close() error {
l.connectionLock.Lock()
defer l.connectionLock.Unlock()
if l.err != nil {
l.connectionLock.Unlock()
return errListenerConnClosed
}
l.err = errListenerConnClosed
return l.cn.Close()
l.connectionLock.Unlock()
// We can't send anything on the connection without holding senderLock.
// Simply close the net.Conn to wake up everyone operating on it.
return l.cn.c.Close()
}
// Err() returns the reason the connection was closed. It is not safe to call
+73 -1
View File
@@ -5,6 +5,9 @@ import (
"fmt"
"io"
"os"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
@@ -43,7 +46,7 @@ func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEven
}
return nil
case <-time.After(1500 * time.Millisecond):
return fmt.Errorf("timeout")
panic("expectEvent timeout")
}
}
@@ -210,6 +213,75 @@ func TestConnPing(t *testing.T) {
}
}
// Test for deadlock where a query fails while another one is queued
func TestConnExecDeadlock(t *testing.T) {
l, _ := newTestListenerConn(t)
defer l.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
l.ExecSimpleQuery("SELECT pg_sleep(60)")
wg.Done()
}()
runtime.Gosched()
go func() {
l.ExecSimpleQuery("SELECT 1")
wg.Done()
}()
// give the two goroutines some time to get into position
runtime.Gosched()
// calls Close on the net.Conn; equivalent to a network failure
l.Close()
var done int32 = 0
go func() {
time.Sleep(10 * time.Second)
if atomic.LoadInt32(&done) != 1 {
panic("timed out")
}
}()
wg.Wait()
atomic.StoreInt32(&done, 1)
}
// Test for ListenerConn being closed while a slow query is executing
func TestListenerConnCloseWhileQueryIsExecuting(t *testing.T) {
l, _ := newTestListenerConn(t)
defer l.Close()
var wg sync.WaitGroup
wg.Add(1)
go func() {
sent, err := l.ExecSimpleQuery("SELECT pg_sleep(60)")
if sent {
panic("expected sent=false")
}
// could be any of a number of errors
if err == nil {
panic("expected error")
}
wg.Done()
}()
// give the above goroutine some time to get into position
runtime.Gosched()
err := l.Close()
if err != nil {
t.Fatal(err)
}
var done int32 = 0
go func() {
time.Sleep(10 * time.Second)
if atomic.LoadInt32(&done) != 1 {
panic("timed out")
}
}()
wg.Wait()
atomic.StoreInt32(&done, 1)
}
func TestNotifyExtra(t *testing.T) {
db := openTestConn(t)
defer db.Close()
+1 -1
View File
@@ -5,12 +5,12 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"os/exec"
"database/sql"
_ "github.com/lib/pq"
)
+1 -1
View File
@@ -34,7 +34,7 @@ func ParseURL(url string) (string, error) {
return "", err
}
if u.Scheme != "postgres" {
if u.Scheme != "postgres" && u.Scheme != "postgresql" {
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
}
+22 -5
View File
@@ -1,3 +1,7 @@
TARGETS = darwin/amd64 darwin/386 linux/amd64 linux/386 windows/amd64 windows/386
GIT_COMMIT = $(shell git rev-parse HEAD)
BUILD_TIME = $(shell date -u +"%Y-%m-%dT%H:%M:%SZ" | tr -d '\n')
DOCKER_RELEASE_TAG = "sosedoff/pgweb:$(shell git describe --abbrev=0 --tags | sed 's/v//')"
BINDATA_IGNORE = $(shell git ls-files -io --exclude-standard $< | sed 's/^/-ignore=/;s/[.]/[.]/g')
usage:
@@ -10,17 +14,19 @@ usage:
@echo "make build : Generate production build for current OS"
@echo "make bootstrap : Install cross-compilation toolchain"
@echo "make release : Generate binaries for all supported OSes"
@echo "make test : Execute test suite"
@echo "make clean : Remove all build files and reset assets"
@echo "make assets : Generate production assets file"
@echo "make dev-assets : Generate development assets file"
@echo "make docker : Build docker image"
@echo "make docker-release : Build and tag docker image"
@echo ""
test:
godep go test
godep go test -cover ./...
assets: static/
go-bindata $(BINDATA_OPTS) $(BINDATA_IGNORE) -ignore=[.]gitignore -ignore=[.]gitkeep $<...
go-bindata -o pkg/data/bindata.go -pkg data $(BINDATA_OPTS) $(BINDATA_IGNORE) -ignore=[.]gitignore -ignore=[.]gitkeep $<...
dev-assets:
@$(MAKE) --no-print-directory assets BINDATA_OPTS="-debug"
@@ -34,22 +40,33 @@ build: assets
@echo "You can now execute ./pgweb"
release: assets
gox -osarch="darwin/amd64 darwin/386 linux/amd64 linux/386 windows/amd64 windows/386" -output="./bin/pgweb_{{.OS}}_{{.Arch}}"
@echo "Building binaries..."
@gox \
-osarch="$(TARGETS)" \
-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}}"
@echo "\nPackaging binaries...\n"
@./script/package.sh
bootstrap:
gox -build-toolchain
setup:
go get github.com/tools/godep
go get golang.org/x/tools/cmd/cover
godep get github.com/mitchellh/gox
godep get github.com/jteeuwen/go-bindata/...
godep restore
clean:
rm -f ./pgweb
rm -f ./bin/*
rm -rf ./bin/*
rm -f bindata.go
make assets
docker:
docker build -t pgweb .
docker build -t pgweb .
docker-release:
docker build -t $(DOCKER_RELEASE_TAG) .
+19 -16
View File
@@ -2,7 +2,9 @@
Web-based PostgreSQL database browser written in Go.
[![Release](https://img.shields.io/github/release/sosedoff/pgweb.svg)](https://github.com/sosedoff/pgweb/releases)
[![Release](https://img.shields.io/github/release/sosedoff/pgweb.svg?label=Release)](https://github.com/sosedoff/pgweb/releases)
[![Linux Build](https://img.shields.io/travis/sosedoff/pgweb.svg?label=Linux)](https://travis-ci.org/sosedoff/pgweb)
[![Windows Build](https://img.shields.io/appveyor/ci/sosedoff/pgweb/master.svg?label=Windows)](https://ci.appveyor.com/project/sosedoff/pgweb)
## 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
application to work with local or remote PostgreSQL databases.
<img src="screenshots/browse.png" width="345px" />
<img src="screenshots/query.png" width="345px" />
[See application screenshots](SCREENS.md)
## Features
@@ -21,10 +22,10 @@ application to work with local or remote PostgreSQL databases.
- Zero dependencies
- Simple installation (distributes as a single binary)
- Connect to local or remote servers
- Browse tables and table data
- Browse tables and table rows
- Get table details: structure, size, indeces, row count
- Run / analyze custom queries
- Export query results to CSV
- Run and analyze custom SQL queries
- Export table rows and query results as CSV
- Query history
- Server bookmarks
@@ -63,23 +64,25 @@ pgweb --url postgres://user:password@host:port/database?sslmode=[mode]
## 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
```
Continuous integration status:
- Travis (Linux) - [![Build Status](https://img.shields.io/travis/sosedoff/pgweb.svg)](https://travis-ci.org/sosedoff/pgweb)
- Appveyor (Windows) - [![Build Status](https://img.shields.io/appveyor/ci/sosedoff/pgweb/master.svg)](https://ci.appveyor.com/project/sosedoff/pgweb)
## Contribute
- Fork repository
- Create a feature or bugfix branch
- Open a new pull request
- Use [github issues](https://github.com/sosedoff/pgweb/issues) for any questions
- Fork this repository
- Create a new feature branch for a new functionality or bugfix
- Commit your changes
- 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
+7
View File
@@ -0,0 +1,7 @@
# Screenshots
### Browse table rows
<img src="screenshots/browse.png" />
### Write SQL queries
<img src="screenshots/query.png" />
-332
View File
@@ -1,332 +0,0 @@
package main
import (
"errors"
"fmt"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
var extraMimeTypes = map[string]string{
".icon": "image-x-icon",
".ttf": "application/x-font-ttf",
".woff": "application/x-font-woff",
".eot": "application/vnd.ms-fontobject",
".svg": "image/svg+xml",
}
type Error struct {
Message string `json:"error"`
}
func NewError(err error) Error {
return Error{err.Error()}
}
func assetContentType(name string) string {
ext := filepath.Ext(name)
result := mime.TypeByExtension(ext)
if result == "" {
result = extraMimeTypes[ext]
}
if result == "" {
result = "text/plain; charset=utf-8"
}
return result
}
func setupRoutes(router *gin.Engine) {
router.GET("/", API_Home)
router.GET("/static/*path", API_ServeAsset)
api := router.Group("/api")
{
api.Use(ApiMiddleware())
api.POST("/connect", API_Connect)
api.GET("/databases", API_GetDatabases)
api.GET("/connection", API_ConnectionInfo)
api.GET("/tables", API_GetTables)
api.GET("/tables/:table", API_GetTable)
api.GET("/tables/:table/rows", API_GetTableRows)
api.GET("/tables/:table/info", API_GetTableInfo)
api.GET("/tables/:table/indexes", API_TableIndexes)
api.GET("/query", API_RunQuery)
api.POST("/query", API_RunQuery)
api.GET("/explain", API_ExplainQuery)
api.POST("/explain", API_ExplainQuery)
api.GET("/history", API_History)
api.GET("/bookmarks", API_Bookmarks)
}
}
// Middleware function to check database connection status before running queries
func ApiMiddleware() gin.HandlerFunc {
allowedPaths := []string{
"/api/connect",
"/api/bookmarks",
"/api/history",
}
return func(c *gin.Context) {
if dbClient != nil {
c.Next()
return
}
currentPath := c.Request.URL.Path
allowed := false
for _, path := range allowedPaths {
if path == currentPath {
allowed = true
break
}
}
if allowed {
c.Next()
} else {
c.JSON(400, Error{"Not connected"})
c.Abort()
}
return
}
}
func API_Home(c *gin.Context) {
data, err := Asset("static/index.html")
if err != nil {
c.String(400, err.Error())
return
}
c.Data(200, "text/html; charset=utf-8", data)
}
func API_Connect(c *gin.Context) {
url := c.Request.FormValue("url")
if url == "" {
c.JSON(400, Error{"Url parameter is required"})
return
}
opts := Options{Url: url}
url, err := formatConnectionUrl(opts)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
client, err := NewClientFromUrl(url)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
err = client.Test()
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
info, err := client.Info()
if err == nil {
if dbClient != nil {
dbClient.db.Close()
}
dbClient = client
}
c.JSON(200, info.Format()[0])
}
func API_GetDatabases(c *gin.Context) {
names, err := dbClient.Databases()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, names)
}
func API_RunQuery(c *gin.Context) {
query := strings.TrimSpace(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, errors.New("Query parameter is missing"))
return
}
API_HandleQuery(query, c)
}
func API_ExplainQuery(c *gin.Context) {
query := strings.TrimSpace(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, errors.New("Query parameter is missing"))
return
}
API_HandleQuery(fmt.Sprintf("EXPLAIN ANALYZE %s", query), c)
}
func API_GetTables(c *gin.Context) {
names, err := dbClient.Tables()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, names)
}
func API_GetTable(c *gin.Context) {
res, err := dbClient.Table(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res)
}
func API_GetTableRows(c *gin.Context) {
limit := 1000 // Number of rows to fetch
limitVal := c.Request.FormValue("limit")
if limitVal != "" {
num, err := strconv.Atoi(limitVal)
if err != nil {
c.JSON(400, Error{"Invalid limit value"})
return
}
if num <= 0 {
c.JSON(400, Error{"Limit should be greater than 0"})
return
}
limit = num
}
opts := RowsOptions{
Limit: limit,
SortColumn: c.Request.FormValue("sort_column"),
SortOrder: c.Request.FormValue("sort_order"),
}
res, err := dbClient.TableRows(c.Params.ByName("table"), opts)
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res)
}
func API_GetTableInfo(c *gin.Context) {
res, err := dbClient.TableInfo(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res.Format()[0])
}
func API_History(c *gin.Context) {
c.JSON(200, dbClient.history)
}
func API_ConnectionInfo(c *gin.Context) {
res, err := dbClient.Info()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res.Format()[0])
}
func API_TableIndexes(c *gin.Context) {
res, err := dbClient.TableIndexes(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res)
}
func API_HandleQuery(query string, c *gin.Context) {
result, err := dbClient.Query(query)
if err != nil {
c.JSON(400, NewError(err))
return
}
q := c.Request.URL.Query()
if len(q["format"]) > 0 && q["format"][0] == "csv" {
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix())
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
c.Data(200, "text/csv", result.CSV())
return
}
c.JSON(200, result)
}
func API_Bookmarks(c *gin.Context) {
bookmarks, err := readAllBookmarks()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, bookmarks)
}
func API_ServeAsset(c *gin.Context) {
path := "static" + c.Params.ByName("path")
data, err := Asset(path)
if err != nil {
c.String(400, err.Error())
return
}
if len(data) == 0 {
c.String(404, "Asset is empty")
return
}
c.Data(200, assetContentType(path), data)
}
+3 -3
View File
@@ -8,20 +8,20 @@
"keywords": [
"postgres",
"database",
"golang",
"frontend"
],
"addons": [
"heroku-postgresql"
],
"env": {
"BUILDPACK_URL": "https://github.com/kr/heroku-buildpack-go",
"AUTH_USER": {
"description": "A username to log into pgweb.",
"value": "admin"
},
"AUTH_PASS": {
"description": "The login password. Please change this.",
"value": "pgweb"
"generator": "secret"
}
}
}
}
+4 -2
View File
@@ -1,5 +1,7 @@
clone_folder: c:\go\src\github.com\sosedoff\pgweb
environment:
GOPATH: c:\projects
GOPATH: c:\go
services:
- postgresql
@@ -18,6 +20,6 @@ build_script:
- godep go build
test_script:
- godep go test
- godep go test -cover ./...
deploy: off
+5
View File
@@ -0,0 +1,5 @@
host = "localhost"
port = "5432"
user = "postgres"
database = "mydatabase"
ssl = "disable"
+1
View File
@@ -0,0 +1 @@
url = "postgres://username:password@host:port/database?sslmode=disable"
+1
View File
@@ -0,0 +1 @@
invalid encoding
+5
View File
@@ -0,0 +1,5 @@
host = "localhost"
port = 5432
user = "postgres"
database = "mydatabase"
ssl = "disable"
-21
View File
@@ -1,21 +0,0 @@
package main
import (
"time"
)
type HistoryRecord struct {
Query string `json:"query"`
Timestamp string `json:"timestamp"`
}
func NewHistory() []HistoryRecord {
return make([]HistoryRecord, 0)
}
func NewHistoryRecord(query string) HistoryRecord {
return HistoryRecord{
Query: query,
Timestamp: time.Now().String(),
}
}
+32 -42
View File
@@ -7,31 +7,14 @@ import (
"os/signal"
"github.com/gin-gonic/gin"
"github.com/jessevdk/go-flags"
_ "github.com/lib/pq"
"github.com/sosedoff/pgweb/pkg/api"
"github.com/sosedoff/pgweb/pkg/client"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
"github.com/sosedoff/pgweb/pkg/util"
)
const VERSION = "0.5.1"
type Options struct {
Version bool `short:"v" long:"version" description:"Print version"`
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
Url string `long:"url" description:"Database connection string"`
Host string `long:"host" description:"Server hostname or IP"`
Port int `long:"port" description:"Server port" default:"5432"`
User string `long:"user" description:"Database user"`
Pass string `long:"pass" description:"Password for user"`
DbName string `long:"db" description:"Database name"`
Ssl string `long:"ssl" description:"SSL option"`
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8080"`
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
}
var dbClient *Client
var options Options
var options command.Options
func exitWithMessage(message string) {
fmt.Println("Error:", message)
@@ -39,49 +22,57 @@ func exitWithMessage(message string) {
}
func initClient() {
if connectionSettingsBlank(options) {
if connection.IsBlank(command.Opts) {
return
}
client, err := NewClient()
cl, err := client.New()
if err != nil {
exitWithMessage(err.Error())
}
if options.Debug {
fmt.Println("Server connection string:", client.connectionString)
if command.Opts.Debug {
fmt.Println("Server connection string:", cl.ConnectionString)
}
fmt.Println("Connecting to server...")
err = client.Test()
err = cl.Test()
if err != nil {
exitWithMessage(err.Error())
}
fmt.Println("Checking tables...")
_, err = client.Tables()
_, err = cl.Tables()
if err != nil {
exitWithMessage(err.Error())
}
dbClient = client
api.DbClient = cl
}
func initOptions() {
_, err := flags.ParseArgs(&options, os.Args)
err := command.ParseOptions()
if err != nil {
os.Exit(1)
}
if options.Url == "" {
options.Url = os.Getenv("DATABASE_URL")
}
options = command.Opts
if options.Version {
fmt.Printf("pgweb v%s\n", VERSION)
printVersion()
os.Exit(0)
}
printVersion()
}
func printVersion() {
str := fmt.Sprintf("Pgweb v%s", command.VERSION)
if command.GitCommit != "" {
str += fmt.Sprintf(" (git: %s)", command.GitCommit)
}
fmt.Println(str)
}
func startServer() {
@@ -93,7 +84,7 @@ func startServer() {
router.Use(gin.BasicAuth(auth))
}
setupRoutes(router)
api.SetupRoutes(router)
fmt.Println("Starting server...")
go func() {
@@ -129,20 +120,19 @@ func openPage() {
func main() {
initOptions()
fmt.Println("Pgweb version", VERSION)
initClient()
if dbClient != nil {
defer dbClient.db.Close()
if api.DbClient != nil {
defer api.DbClient.Close()
}
if !options.Debug {
gin.SetMode("release")
}
// Print memory usage every 30 seconds with debug flag
if options.Debug {
startRuntimeProfiler()
util.StartProfiler()
}
startServer()
+213
View File
@@ -0,0 +1,213 @@
package api
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/bookmarks"
"github.com/sosedoff/pgweb/pkg/client"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
)
var DbClient *client.Client
func GetHome(c *gin.Context) {
serveStaticAsset("/index.html", c)
}
func GetAsset(c *gin.Context) {
serveStaticAsset(c.Params.ByName("path"), c)
}
func Connect(c *gin.Context) {
url := c.Request.FormValue("url")
if url == "" {
c.JSON(400, Error{"Url parameter is required"})
return
}
opts := command.Options{Url: url}
url, err := connection.FormatUrl(opts)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
cl, err := client.NewFromUrl(url)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
err = cl.Test()
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
info, err := cl.Info()
if err == nil {
if DbClient != nil {
DbClient.Close()
}
DbClient = cl
}
c.JSON(200, info.Format()[0])
}
func GetDatabases(c *gin.Context) {
names, err := DbClient.Databases()
serveResult(names, err, c)
}
func RunQuery(c *gin.Context) {
query := strings.TrimSpace(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, errors.New("Query parameter is missing"))
return
}
HandleQuery(query, c)
}
func ExplainQuery(c *gin.Context) {
query := strings.TrimSpace(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, errors.New("Query parameter is missing"))
return
}
HandleQuery(fmt.Sprintf("EXPLAIN ANALYZE %s", query), c)
}
func GetSchemas(c *gin.Context) {
names, err := DbClient.Schemas()
serveResult(names, err, c)
}
func GetTables(c *gin.Context) {
names, err := DbClient.Tables()
serveResult(names, err, c)
}
func GetTable(c *gin.Context) {
res, err := DbClient.Table(c.Params.ByName("table"))
serveResult(res, err, c)
}
func GetTableRows(c *gin.Context) {
limit := 1000 // Number of rows to fetch
limitVal := c.Request.FormValue("limit")
if limitVal != "" {
num, err := strconv.Atoi(limitVal)
if err != nil {
c.JSON(400, Error{"Invalid limit value"})
return
}
if num <= 0 {
c.JSON(400, Error{"Limit should be greater than 0"})
return
}
limit = num
}
opts := client.RowsOptions{
Limit: limit,
SortColumn: c.Request.FormValue("sort_column"),
SortOrder: c.Request.FormValue("sort_order"),
}
res, err := DbClient.TableRows(c.Params.ByName("table"), opts)
serveResult(res, err, c)
}
func GetTableInfo(c *gin.Context) {
res, err := DbClient.TableInfo(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res.Format()[0])
}
func GetHistory(c *gin.Context) {
c.JSON(200, DbClient.History)
}
func GetConnectionInfo(c *gin.Context) {
res, err := DbClient.Info()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res.Format()[0])
}
func GetActivity(c *gin.Context) {
res, err := DbClient.Activity()
serveResult(res, err, c)
}
func GetTableIndexes(c *gin.Context) {
res, err := DbClient.TableIndexes(c.Params.ByName("table"))
serveResult(res, err, c)
}
func HandleQuery(query string, c *gin.Context) {
result, err := DbClient.Query(query)
if err != nil {
c.JSON(400, NewError(err))
return
}
q := c.Request.URL.Query()
if len(q["format"]) > 0 && q["format"][0] == "csv" {
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix())
if len(q["filename"]) > 0 && q["filename"][0] != "" {
filename = q["filename"][0]
}
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
c.Data(200, "text/csv", result.CSV())
return
}
c.JSON(200, result)
}
func GetBookmarks(c *gin.Context) {
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
serveResult(bookmarks, err, c)
}
func GetInfo(c *gin.Context) {
info := map[string]string{
"version": command.VERSION,
"git_sha": command.GitCommit,
"build_time": command.BuildTime,
}
c.JSON(200, info)
}
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package api
import (
"testing"
+96
View File
@@ -0,0 +1,96 @@
package api
import (
"mime"
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/data"
)
var extraMimeTypes = map[string]string{
".icon": "image-x-icon",
".ttf": "application/x-font-ttf",
".woff": "application/x-font-woff",
".eot": "application/vnd.ms-fontobject",
".svg": "image/svg+xml",
".html": "text/html; charset-utf-8",
}
type Error struct {
Message string `json:"error"`
}
func assetContentType(name string) string {
ext := filepath.Ext(name)
result := mime.TypeByExtension(ext)
if result == "" {
result = extraMimeTypes[ext]
}
if result == "" {
result = "text/plain; charset=utf-8"
}
return result
}
func NewError(err error) Error {
return Error{err.Error()}
}
// Middleware function to check database connection status before running queries
func dbCheckMiddleware() gin.HandlerFunc {
allowedPaths := []string{
"/api/info",
"/api/connect",
"/api/bookmarks",
"/api/history",
}
return func(c *gin.Context) {
if DbClient != nil {
c.Next()
return
}
currentPath := c.Request.URL.Path
allowed := false
for _, path := range allowedPaths {
if path == currentPath {
allowed = true
break
}
}
if allowed {
c.Next()
} else {
c.JSON(400, Error{"Not connected"})
c.Abort()
}
return
}
}
func serveStaticAsset(path string, c *gin.Context) {
data, err := data.Asset("static" + path)
if err != nil {
c.String(400, err.Error())
return
}
c.Data(200, assetContentType(path), data)
}
func serveResult(result interface{}, err error, c *gin.Context) {
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, result)
}
+33
View File
@@ -0,0 +1,33 @@
package api
import (
"github.com/gin-gonic/gin"
)
func SetupRoutes(router *gin.Engine) {
router.GET("/", GetHome)
router.GET("/static/*path", GetAsset)
api := router.Group("/api")
{
api.Use(dbCheckMiddleware())
api.GET("/info", GetInfo)
api.POST("/connect", Connect)
api.GET("/databases", GetDatabases)
api.GET("/connection", GetConnectionInfo)
api.GET("/activity", GetActivity)
api.GET("/schemas", GetSchemas)
api.GET("/tables", GetTables)
api.GET("/tables/:table", GetTable)
api.GET("/tables/:table/rows", GetTableRows)
api.GET("/tables/:table/info", GetTableInfo)
api.GET("/tables/:table/indexes", GetTableIndexes)
api.GET("/query", RunQuery)
api.POST("/query", RunQuery)
api.GET("/explain", ExplainQuery)
api.POST("/explain", ExplainQuery)
api.GET("/history", GetHistory)
api.GET("/bookmarks", GetBookmarks)
}
}
+5 -12
View File
@@ -1,4 +1,4 @@
package main
package bookmarks
import (
"fmt"
@@ -22,19 +22,13 @@ type Bookmark struct {
func readServerConfig(path string) (Bookmark, error) {
bookmark := Bookmark{}
buff, err := ioutil.ReadFile(path)
buff, err := ioutil.ReadFile(path)
if err != nil {
fmt.Println(err)
return bookmark, err
}
_, err = toml.Decode(string(buff), &bookmark)
if err != nil {
fmt.Println(err)
}
return bookmark, err
}
@@ -43,13 +37,12 @@ func fileBasename(path string) string {
return strings.Replace(filename, filepath.Ext(path), "", 1)
}
func bookmarksPath() string {
func Path() string {
path, _ := homedir.Dir()
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
}
func readAllBookmarks() (map[string]Bookmark, error) {
path := bookmarksPath()
func ReadAll(path string) (map[string]Bookmark, error) {
results := map[string]Bookmark{}
files, err := ioutil.ReadDir(path)
@@ -67,7 +60,7 @@ func readAllBookmarks() (map[string]Bookmark, error) {
config, err := readServerConfig(fullPath)
if err != nil {
fmt.Println(err)
fmt.Printf("%s parse error: %s\n", fullPath, err)
continue
}
+71
View File
@@ -0,0 +1,71 @@
package bookmarks
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Invalid_Bookmark_Files(t *testing.T) {
_, err := readServerConfig("foobar")
assert.Error(t, err)
_, err = readServerConfig("../../data/invalid.toml")
assert.Error(t, err)
assert.Equal(t, "Near line 1, key 'invalid encoding': Near line 2: Expected key separator '=', but got '\\n' instead.", err.Error())
_, err = readServerConfig("../../data/invalid_port.toml")
assert.Error(t, err)
assert.Equal(t, "Type mismatch for 'bookmarks.Bookmark.Port': Expected string but found 'int64'.", err.Error())
}
func Test_Bookmark(t *testing.T) {
bookmark, err := readServerConfig("../../data/bookmark.toml")
assert.Equal(t, nil, err)
assert.Equal(t, "localhost", bookmark.Host)
assert.Equal(t, "5432", bookmark.Port)
assert.Equal(t, "postgres", bookmark.User)
assert.Equal(t, "mydatabase", bookmark.Database)
assert.Equal(t, "disable", bookmark.Ssl)
assert.Equal(t, "", bookmark.Password)
assert.Equal(t, "", bookmark.Url)
}
func Test_Bookmark_URL(t *testing.T) {
bookmark, err := readServerConfig("../../data/bookmark_url.toml")
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://username:password@host:port/database?sslmode=disable", bookmark.Url)
assert.Equal(t, "", bookmark.Host)
assert.Equal(t, "", bookmark.Port)
assert.Equal(t, "", bookmark.User)
assert.Equal(t, "", bookmark.Database)
assert.Equal(t, "", bookmark.Ssl)
assert.Equal(t, "", bookmark.Password)
}
func Test_Bookmarks_Path(t *testing.T) {
assert.NotEqual(t, "/.pgweb/bookmarks", Path())
}
func Test_Basename(t *testing.T) {
assert.Equal(t, "filename", fileBasename("filename.toml"))
assert.Equal(t, "filename", fileBasename("path/filename.toml"))
assert.Equal(t, "filename", fileBasename("~/long/path/filename.toml"))
assert.Equal(t, "filename", fileBasename("filename"))
}
func Test_ReadBookmarks_Invalid(t *testing.T) {
bookmarks, err := ReadAll("foobar")
assert.Error(t, err)
assert.Equal(t, 0, len(bookmarks))
}
func Test_ReadBookmarks(t *testing.T) {
bookmarks, err := ReadAll("../../data")
assert.Equal(t, nil, err)
assert.Equal(t, 2, len(bookmarks))
}
+42 -20
View File
@@ -1,4 +1,4 @@
package main
package client
import (
"bytes"
@@ -6,13 +6,19 @@ import (
"fmt"
"reflect"
_ "github.com/lib/pq"
"github.com/jmoiron/sqlx"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
"github.com/sosedoff/pgweb/pkg/history"
"github.com/sosedoff/pgweb/pkg/statements"
)
type Client struct {
db *sqlx.DB
history []HistoryRecord
connectionString string
History []history.Record
ConnectionString string
}
type Row []interface{}
@@ -29,10 +35,10 @@ type RowsOptions struct {
SortOrder string // Sort direction (ASC, DESC)
}
func NewClient() (*Client, error) {
str, err := buildConnectionString(options)
func New() (*Client, error) {
str, err := connection.BuildString(command.Opts)
if options.Debug && str != "" {
if command.Opts.Debug && str != "" {
fmt.Println("Creating a new client for:", str)
}
@@ -48,28 +54,27 @@ func NewClient() (*Client, error) {
client := Client{
db: db,
connectionString: str,
history: NewHistory(),
ConnectionString: str,
History: history.New(),
}
return &client, nil
}
func NewClientFromUrl(url string) (*Client, error) {
if options.Debug {
func NewFromUrl(url string) (*Client, error) {
if command.Opts.Debug {
fmt.Println("Creating a new client for:", url)
}
db, err := sqlx.Open("postgres", url)
if err != nil {
return nil, err
}
client := Client{
db: db,
connectionString: url,
history: NewHistory(),
ConnectionString: url,
History: history.New(),
}
return &client, nil
@@ -80,19 +85,23 @@ func (client *Client) Test() error {
}
func (client *Client) Info() (*Result, error) {
return client.query(PG_INFO)
return client.query(statements.PG_INFO)
}
func (client *Client) Databases() ([]string, error) {
return client.fetchRows(PG_DATABASES)
return client.fetchRows(statements.PG_DATABASES)
}
func (client *Client) Schemas() ([]string, error) {
return client.fetchRows(statements.PG_SCHEMAS)
}
func (client *Client) Tables() ([]string, error) {
return client.fetchRows(PG_TABLES)
return client.fetchRows(statements.PG_TABLES)
}
func (client *Client) Table(table string) (*Result, error) {
return client.query(PG_TABLE_SCHEMA, table)
return client.query(statements.PG_TABLE_SCHEMA, table)
}
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
@@ -114,11 +123,11 @@ func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error)
}
func (client *Client) TableInfo(table string) (*Result, error) {
return client.query(PG_TABLE_INFO, table)
return client.query(statements.PG_TABLE_INFO, table)
}
func (client *Client) TableIndexes(table string) (*Result, error) {
res, err := client.query(PG_TABLE_INDEXES, table)
res, err := client.query(statements.PG_TABLE_INDEXES, table)
if err != nil {
return nil, err
@@ -127,12 +136,17 @@ func (client *Client) TableIndexes(table string) (*Result, error) {
return res, err
}
// Returns all active queriers on the server
func (client *Client) Activity() (*Result, error) {
return client.query(statements.PG_ACTIVITY)
}
func (client *Client) Query(query string) (*Result, error) {
res, err := client.query(query)
// Save history records only if query did not fail
if err == nil {
client.history = append(client.history, NewHistoryRecord(query))
client.History = append(client.History, history.NewRecord(query))
}
return res, err
@@ -222,6 +236,14 @@ func (res *Result) CSV() []byte {
return buff.Bytes()
}
// Close database connection
func (client *Client) Close() error {
if client.db != nil {
return client.db.Close()
}
return nil
}
// Fetch all rows as strings for a single column
func (client *Client) fetchRows(q string) ([]string, error) {
res, err := client.query(q)
+24 -10
View File
@@ -1,4 +1,4 @@
package main
package client
import (
"fmt"
@@ -10,8 +10,10 @@ import (
"github.com/stretchr/testify/assert"
)
var testClient *Client
var testCommands map[string]string
var (
testClient *Client
testCommands map[string]string
)
func setupCommands() {
testCommands = map[string]string{
@@ -40,7 +42,7 @@ func setup() {
os.Exit(1)
}
out, err = exec.Command(testCommands["psql"], "-U", "postgres", "-h", "localhost", "-f", "./sql/booktown.sql", "booktown").CombinedOutput()
out, err = exec.Command(testCommands["psql"], "-U", "postgres", "-h", "localhost", "-f", "../../data/booktown.sql", "booktown").CombinedOutput()
if err != nil {
fmt.Println("Database import failed:", string(out))
@@ -50,7 +52,7 @@ func setup() {
}
func setupClient() {
testClient, _ = NewClientFromUrl("postgres://postgres@localhost/booktown?sslmode=disable")
testClient, _ = NewFromUrl("postgres://postgres@localhost/booktown?sslmode=disable")
}
func teardownClient() {
@@ -69,14 +71,26 @@ func teardown() {
func test_NewClientFromUrl(t *testing.T) {
url := "postgres://postgres@localhost/booktown?sslmode=disable"
client, err := NewClientFromUrl(url)
client, err := NewFromUrl(url)
if err != nil {
defer client.db.Close()
defer client.Close()
}
assert.Equal(t, nil, err)
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) {
@@ -209,7 +223,7 @@ func test_ResultCsv(t *testing.T) {
func test_History(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books")
query := testClient.history[len(testClient.history)-1].Query
query := testClient.History[len(testClient.History)-1].Query
assert.Equal(t, nil, err)
assert.Equal(t, "SELECT * FROM books", query)
@@ -217,7 +231,7 @@ func test_History(t *testing.T) {
func test_HistoryError(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books123")
query := testClient.history[len(testClient.history)-1].Query
query := testClient.History[len(testClient.History)-1].Query
assert.NotEqual(t, nil, err)
assert.NotEqual(t, "SELECT * FROM books123", query)
+39
View File
@@ -0,0 +1,39 @@
package command
import (
"os"
"github.com/jessevdk/go-flags"
)
type Options struct {
Version bool `short:"v" long:"version" description:"Print version"`
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
Url string `long:"url" description:"Database connection string"`
Host string `long:"host" description:"Server hostname or IP"`
Port int `long:"port" description:"Server port" default:"5432"`
User string `long:"user" description:"Database user"`
Pass string `long:"pass" description:"Password for user"`
DbName string `long:"db" description:"Database name"`
Ssl string `long:"ssl" description:"SSL option"`
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
}
var Opts Options
func ParseOptions() error {
_, err := flags.ParseArgs(&Opts, os.Args)
if err != nil {
return err
}
if Opts.Url == "" {
Opts.Url = os.Getenv("DATABASE_URL")
}
return nil
}
+8
View File
@@ -0,0 +1,8 @@
package command
const VERSION = "0.6.2"
var (
GitCommit string
BuildTime string
)
@@ -1,4 +1,4 @@
package main
package connection
import (
"errors"
@@ -6,6 +6,8 @@ import (
"os"
"os/user"
"strings"
"github.com/sosedoff/pgweb/pkg/command"
)
func currentUser() (string, error) {
@@ -22,11 +24,11 @@ func currentUser() (string, error) {
return "", errors.New("Unable to detect OS user")
}
func formatConnectionUrl(opts Options) (string, error) {
func FormatUrl(opts command.Options) (string, error) {
url := opts.Url
// 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")
}
@@ -50,13 +52,13 @@ func formatConnectionUrl(opts Options) (string, error) {
return url, nil
}
func connectionSettingsBlank(opts Options) bool {
func IsBlank(opts command.Options) bool {
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == ""
}
func buildConnectionString(opts Options) (string, error) {
func BuildString(opts command.Options) (string, error) {
if opts.Url != "" {
return formatConnectionUrl(opts)
return FormatUrl(opts)
}
// Try to detect user from current OS user
@@ -1,23 +1,24 @@
package main
package connection
import (
"fmt"
"os/user"
"testing"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/stretchr/testify/assert"
)
func Test_Invalid_Url(t *testing.T) {
opts := Options{}
opts := command.Options{}
examples := []string{
"postgresql://foobar",
"postgre://foobar",
"foobar",
}
for _, val := range examples {
opts.Url = val
str, err := buildConnectionString(opts)
str, err := BuildString(opts)
assert.Equal(t, "", str)
assert.Error(t, err)
@@ -27,14 +28,14 @@ func Test_Invalid_Url(t *testing.T) {
func Test_Valid_Url(t *testing.T) {
url := "postgres://myhost/database"
str, err := buildConnectionString(Options{Url: url})
str, err := BuildString(command.Options{Url: url})
assert.Equal(t, nil, err)
assert.Equal(t, url, str)
}
func Test_Url_And_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{
str, err := BuildString(command.Options{
Url: "postgres://myhost/database",
Ssl: "disable",
})
@@ -44,14 +45,14 @@ func Test_Url_And_Ssl_Flag(t *testing.T) {
}
func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{
str, err := BuildString(command.Options{
Url: "postgres://localhost/database",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
str, err = buildConnectionString(Options{
str, err = BuildString(command.Options{
Url: "postgres://127.0.0.1/database",
})
@@ -60,7 +61,7 @@ func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
}
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{
str, err := BuildString(command.Options{
Url: "postgres://localhost/database",
Ssl: "require",
})
@@ -68,7 +69,7 @@ func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
str, err = buildConnectionString(Options{
str, err = BuildString(command.Options{
Url: "postgres://127.0.0.1/database",
Ssl: "require",
})
@@ -78,14 +79,14 @@ func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
}
func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
str, err := buildConnectionString(Options{
str, err := BuildString(command.Options{
Url: "postgres://localhost/database?sslmode=require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
str, err = buildConnectionString(Options{
str, err = BuildString(command.Options{
Url: "postgres://127.0.0.1/database?sslmode=require",
})
@@ -94,7 +95,7 @@ func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
}
func Test_Flag_Args(t *testing.T) {
str, err := buildConnectionString(Options{
str, err := BuildString(command.Options{
Host: "host",
Port: 5432,
User: "user",
@@ -107,7 +108,7 @@ func Test_Flag_Args(t *testing.T) {
}
func Test_Localhost(t *testing.T) {
opts := Options{
opts := command.Options{
Host: "localhost",
Port: 5432,
User: "user",
@@ -115,18 +116,18 @@ func Test_Localhost(t *testing.T) {
DbName: "db",
}
str, err := buildConnectionString(opts)
str, err := BuildString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
opts.Host = "127.0.0.1"
str, err = buildConnectionString(opts)
str, err = BuildString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
}
func Test_Localhost_And_Ssl(t *testing.T) {
opts := Options{
opts := command.Options{
Host: "localhost",
Port: 5432,
User: "user",
@@ -135,31 +136,31 @@ func Test_Localhost_And_Ssl(t *testing.T) {
Ssl: "require",
}
str, err := buildConnectionString(opts)
str, err := BuildString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
}
func Test_No_User(t *testing.T) {
opts := Options{Host: "host", Port: 5432, DbName: "db"}
opts := command.Options{Host: "host", Port: 5432, DbName: "db"}
u, _ := user.Current()
str, err := buildConnectionString(opts)
str, err := BuildString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str)
}
func Test_Port(t *testing.T) {
opts := Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
str, err := buildConnectionString(opts)
opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
str, err := BuildString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user@host:5000/db", str)
}
func Test_Blank(t *testing.T) {
assert.Equal(t, true, connectionSettingsBlank(Options{}))
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user"}))
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user", DbName: "db"}))
assert.Equal(t, false, connectionSettingsBlank(Options{Url: "url"}))
assert.Equal(t, true, IsBlank(command.Options{}))
assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user"}))
assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user", DbName: "db"}))
assert.Equal(t, false, IsBlank(command.Options{Url: "url"}))
}
+33
View File
@@ -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")
}
+74
View File
@@ -0,0 +1,74 @@
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()
}
}
}()
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)
}
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
package history
import (
"time"
)
type Record struct {
Query string `json:"query"`
Timestamp string `json:"timestamp"`
}
func New() []Record {
return make([]Record, 0)
}
func NewRecord(query string) Record {
return Record{
Query: query,
Timestamp: time.Now().String(),
}
}
+17 -1
View File
@@ -1,8 +1,10 @@
package main
package statements
const (
PG_DATABASES = `SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY datname ASC`
PG_SCHEMAS = `SELECT schema_name FROM information_schema.schemata ORDER BY schema_name ASC`
PG_INFO = `SELECT
session_user
, current_user
@@ -28,4 +30,18 @@ FROM information_schema.columns
WHERE table_name = $1`
PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name`
PG_ACTIVITY = `SELECT
datname,
query,
state,
waiting,
query_start,
state_change,
pid,
datid,
application_name,
client_addr
FROM pg_stat_activity
WHERE state IS NOT NULL`
)
+32
View File
@@ -0,0 +1,32 @@
package util
import (
"log"
"os"
"runtime"
"time"
)
const MEGABYTE = 1024 * 1024
func runProfiler() {
logger := log.New(os.Stdout, "", 0)
m := &runtime.MemStats{}
for {
runtime.ReadMemStats(m)
logger.Printf(
"[DEBUG] Goroutines: %v, Mem used: %v (%v mb), Mem acquired: %v (%v mb)\n",
runtime.NumGoroutine(),
m.Alloc, m.Alloc/MEGABYTE,
m.Sys, m.Sys/MEGABYTE,
)
time.Sleep(time.Second * 30)
}
}
func StartProfiler() {
go runProfiler()
}
-30
View File
@@ -1,30 +0,0 @@
package main
import (
"log"
"os"
"runtime"
"time"
)
const MEGABYTE = 1024 * 1024
func startRuntimeProfiler() {
go func() {
logger := log.New(os.Stdout, "", 0)
m := &runtime.MemStats{}
for {
runtime.ReadMemStats(m)
logger.Printf(
"[DEBUG] Goroutines: %v, Mem used: %v (%v mb), Mem acquired: %v (%v mb)\n",
runtime.NumGoroutine(),
m.Alloc, m.Alloc/MEGABYTE,
m.Sys, m.Sys/MEGABYTE,
)
time.Sleep(time.Second * 30)
}
}()
}
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

+15
View File
@@ -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
+38 -20
View File
@@ -17,7 +17,6 @@
padding: 0px;
height: 50px;
display: block;
width: 550px;
}
#nav ul li {
@@ -74,6 +73,8 @@
left: 0px;
top: 0px;
bottom: 0px;
border-right: 1px solid #d4d4d4;
box-sizing: border-box;
}
#sidebar .title {
@@ -84,8 +85,9 @@
height: 33px;
padding: 0px 8px;
padding-left: 12px;
color: #fff;
background: #31373d;
color: #000;
font-weight: bold;
background: #f6f6f6;
}
#sidebar div.tables-list {
@@ -95,7 +97,7 @@
left: 0px;
bottom: 130px;
overflow: auto;
background: #31373d;
background: #f6f6f6;
}
#sidebar div.tables-list .wrap {
@@ -115,17 +117,17 @@
float: right;
margin-right: 4px;
cursor: pointer;
color: #95a7b7;
color: #aaa;
display: none;
}
#sidebar div.tables-list .title span.refresh:hover {
color: #fff;
color: #555;
}
#sidebar div.tables-list #tables {
padding: 33px 0 0;
font-size: 13px;
font-size: 12px;
}
#sidebar div.table-information {
@@ -134,7 +136,10 @@
left: 0px;
height: 130px;
bottom: 0px;
background: #272c30;
background: #f6f6f6;
border-top: 1px solid #d4d4d4;
border-right: 1px solid #d4d4d4;
box-sizing: border-box;
}
#sidebar div.table-information .wrap {
@@ -143,7 +148,7 @@
}
#sidebar div.table-information .title {
background: #272c30;
background: #f6f6f6;
}
#sidebar div.table-information ul {
@@ -157,11 +162,11 @@
height: 24px;
padding: 0 8px 0 12px;
cursor: default;
color: #637D94 !important;
color: #7F7E7F !important;
}
#sidebar div.table-information ul li span {
color: #95a7b7;
color: #4F4F4F;
min-width: 0;
padding-left: 0px;
}
@@ -178,19 +183,18 @@
padding: 0px;
margin: 0px;
cursor: pointer;
color: #95a7b7 !important;
color: #4F4E4F !important;
white-space: nowrap;
}
#sidebar li.selected {
color: #fff !important;
font-weight: bold;
background: #272c30;
background: #e6e6e6;
-webkit-font-smoothing: antialiased;
}
#sidebar ul li:hover span {
background: #272c30;
background: #f1f1f1;
}
#sidebar ul li i {
@@ -296,7 +300,7 @@
#results tr:nth-child(even) > td {
border: none;
background: #f8f8f8;
background: #fafafa;
}
#results tr:nth-child(odd) > td {
@@ -315,10 +319,23 @@
-webkit-font-smoothing: antialiased;
}
#results th.active {
background: #e3e3e3;
}
#results[data-mode="browse"] th:hover {
cursor: pointer;
background: #fafafa;
}
#results tbody tr:hover td {
background: #ffe;
}
#results tbody tr {
border-bottom: 1px solid #f2f2f2;
}
#results tr.selected td {
background: #3874d7 !important;
color: #fff !important;
@@ -370,7 +387,7 @@
#connection_window {
z-index: 3;
position: fixed;
background: #ebeef0;
background: #fff;
top: 0px;
left: 0px;
bottom: 0px;
@@ -410,12 +427,12 @@
text-align: center;
text-shadow: 0px 1px 0px #fff;
margin-bottom: 25px;
color: #95A7B7;
color: #999;
font-weight: normal;
}
.connection-settings form {
background: #fff;
background: #f6f6f6;
padding: 25px;
border-radius: 4px;
-moz-border-radius: 4px;
@@ -432,4 +449,5 @@
.connection-ssh-group {
display: none;
}
z-index: 1000;
}
+12 -3
View File
@@ -12,17 +12,19 @@
<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-pgsql.js"></script>
<script type="text/javascript" src="/static/js/bootstrap-contextmenu.js"></script>
<script type="text/javascript" src="/static/js/app.js"></script>
</head>
<body>
<div id="main">
<div id="nav">
<ul>
<li id="table_content">Content</li>
<li id="table_content">Rows</li>
<li id="table_structure">Structure</li>
<li id="table_indexes">Indexes</li>
<li id="table_query" class="selected">SQL Query</li>
<li id="table_history">History</li>
<li id="table_activity">Activity</li>
<li id="table_connection">Connection</li>
</ul>
@@ -80,7 +82,6 @@
<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="standard" class="btn btn-default active" id="connection_standard">Standard</button>
<!--<button type="button" data="ssh" class="btn btn-default">SSH</button>-->
</div>
</div>
@@ -121,7 +122,7 @@
<div class="form-group">
<label class="col-sm-3 control-label">Password</label>
<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>
@@ -192,5 +193,13 @@
</form>
</div>
</div>
<div id="tables_context_menu">
<ul class="dropdown-menu" role="menu">
<li><a href="#" data-action="export">Export to CSV</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>
</html>
+102 -13
View File
@@ -4,7 +4,7 @@ var bookmarks = {};
function apiCall(method, path, params, cb) {
$.ajax({
url: "/api" + path,
url: "/api" + path,
method: method,
cache: false,
data: params,
@@ -37,7 +37,7 @@ function loadTables() {
getTables(function(data) {
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");
});
});
}
@@ -68,7 +68,48 @@ function resetTable() {
removeClass("no-crop");
}
function buildTable(results) {
function performTableAction(table, action) {
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();
resetTable();
});
break;
case "export":
var filename = table + ".csv"
var query = window.encodeURI("SELECT * FROM " + table);
var url = "http://" + window.location.host + "/api/query?format=csv&filename=" + filename + "&query=" + query;
var win = window.open(url, "_blank");
win.focus();
break;
}
}
function sortArrow(direction) {
switch (direction) {
case "ASC":
return "&#x25B2;";
case "DESC":
return "&#x25BC;";
default:
return "";
}
}
function buildTable(results, sortColumn, sortOrder) {
resetTable();
if (results.error) {
@@ -87,7 +128,12 @@ function buildTable(results) {
var rows = "";
results.columns.forEach(function(col) {
cols += "<th data='" + col + "'>" + col + "</th>";
if (col === sortColumn) {
cols += "<th class='active' data='" + col + "'" + "data-sort-order=" + sortOrder + ">" + col + "&nbsp;" + sortArrow(sortOrder) + "</th>";
}
else {
cols += "<th data='" + col + "'>" + col + "</th>";
}
});
results.rows.forEach(function(row) {
@@ -113,8 +159,8 @@ function showQueryHistory() {
}
buildTable({ columns: ["id", "query", "timestamp"], rows: rows });
setCurrentTab("table_history");
setCurrentTab("table_history");
$("#input").hide();
$("#output").addClass("full");
$("#results").addClass("no-crop");
@@ -157,7 +203,7 @@ function showTableInfo() {
});
}
function showTableContent() {
function showTableContent(sortColumn, sortOrder) {
var name = getCurrentTable();
if (name.length == 0) {
@@ -165,8 +211,8 @@ function showTableContent() {
return;
}
getTableRows(name, { limit: 100 }, function(data) {
buildTable(data);
getTableRows(name, { limit: 100, sort_column: sortColumn, sort_order: sortOrder }, function(data) {
buildTable(data, sortColumn, sortOrder);
setCurrentTab("table_content");
$("#results").attr("data-mode", "browse");
@@ -186,6 +232,10 @@ function showTableStructure() {
getTableStructure(name, function(data) {
setCurrentTab("table_structure");
buildTable(data);
$("#input").hide();
$("#output").addClass("full");
$("#results").addClass("no-crop");
});
}
@@ -217,13 +267,23 @@ function showConnectionPanel() {
});
}
function showActivityPanel() {
setCurrentTab("table_activity");
apiCall("get", "/activity", {}, function(data) {
buildTable(data);
$("#input").hide();
$("#output").addClass("full");
});
}
function runQuery() {
setCurrentTab("table_query");
$("#run, #explain, #csv").prop("disabled", true);
$("#query_progress").show();
var query = $.trim(editor.getValue());
var query = $.trim(editor.getSelectedText() || editor.getValue());
if (query.length == 0) {
$("#run, #explain, #csv").prop("disabled", false);
@@ -400,6 +460,7 @@ $(document).ready(function() {
$("#table_history").on("click", function() { showQueryHistory(); });
$("#table_query").on("click", function() { showQueryPanel(); });
$("#table_connection").on("click", function() { showConnectionPanel(); });
$("#table_activity").on("click", function() { showActivityPanel(); });
$("#run").on("click", function() {
runQuery();
@@ -418,6 +479,24 @@ $(document).ready(function() {
$(this).addClass("selected");
});
$("#results").on("click", "th", function(e) {
var sortColumn = this.attributes['data'].value;
var contentTab = $('#table_content').hasClass('selected');
if (!contentTab) {
return;
}
if (this.dataset.sortOrder === "ASC") {
this.dataset.sortOrder = "DESC"
}
else {
this.dataset.sortOrder = "ASC"
}
showTableContent(sortColumn, this.dataset.sortOrder);
});
$("#results").on("dblclick", "td > div", function() {
if ($(this).has("textarea").length > 0) {
return;
@@ -447,6 +526,16 @@ $(document).ready(function() {
showTableInfo();
});
$("#tables").contextmenu({
target: "#tables_context_menu",
scopes: "li",
onItem: function(context, e) {
var table = $.trim($(context[0]).text());
var action = $(e.target).data("action");
performTableAction(table, action);
}
});
$("#refresh_tables").on("click", function() {
loadTables();
});
@@ -529,7 +618,7 @@ $(document).ready(function() {
var button = $(this).children("button");
var url = getConnectionString();
if (url.length == 0) {
return;
}
@@ -557,7 +646,7 @@ $(document).ready(function() {
initEditor();
addShortcutTooltips();
apiCall("get", "/connection", {}, function(resp) {
if (resp.error) {
connected = false;
@@ -571,4 +660,4 @@ $(document).ready(function() {
$("#main").show();
}
});
});
});
+205
View File
@@ -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));