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 pgweb
bin bin
tmp/ tmp/
cover.out
+3 -3
View File
@@ -1,12 +1,12 @@
language: go language: go
sudo: false
addons: addons:
postgresql: "9.3" postgresql: "9.3"
go: go:
- 1.3 - 1.4.1
- 1.4 - 1.4.2
- tip
install: install:
- make setup - 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 - Upgrades Gin framework dependency to 0.5.0
- Fixes server crash if another pgweb server is running - 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 COPY . /go/src/github.com/sosedoff/pgweb
WORKDIR /go/src/pgweb WORKDIR /go/src/github.com/sosedoff/pgweb
RUN go get github.com/tools/godep RUN go get github.com/tools/godep
RUN godep restore RUN godep restore
RUN godep go build && godep go install RUN godep go build && godep go install
CMD ["pgweb"] EXPOSE 8081
CMD ["pgweb", "--bind", "0.0.0.0"]
+2 -2
View File
@@ -28,8 +28,8 @@
}, },
{ {
"ImportPath": "github.com/lib/pq", "ImportPath": "github.com/lib/pq",
"Comment": "go1.0-cutoff-13-g19eeca3", "Comment": "go1.0-cutoff-56-gdc50b6a",
"Rev": "19eeca3e30d2577b1761db471ec130810e67f532" "Rev": "dc50b6ad2d3ee836442cf3389009c7cd1e64bb43"
}, },
{ {
"ImportPath": "github.com/mitchellh/go-homedir", "ImportPath": "github.com/mitchellh/go-homedir",
+13 -6
View File
@@ -44,13 +44,20 @@ env:
- PGUSER=postgres - PGUSER=postgres
- PQGOSSLTESTS=1 - PQGOSSLTESTS=1
- PQSSLCERTTEST_PATH=$PWD/certs - PQSSLCERTTEST_PATH=$PWD/certs
- PGHOST=127.0.0.1
matrix: matrix:
- PGVERSION=9.4 - PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.3 - PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.2 - PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.1 - PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.0 - PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=8.4 - PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=yes
- PGVERSION=9.4 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.3 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.2 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.1 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=9.0 PQTEST_BINARY_PARAMETERS=no
- PGVERSION=8.4 PQTEST_BINARY_PARAMETERS=no
script: script:
- go test -v ./... - go test -v ./...
+5
View File
@@ -57,10 +57,13 @@ code still exists in here.
* Brad Fitzpatrick (bradfitz) * Brad Fitzpatrick (bradfitz)
* Charlie Melbye (cmelbye) * Charlie Melbye (cmelbye)
* Chris Bandy (cbandy) * Chris Bandy (cbandy)
* Chris Gilling (cgilling)
* Chris Walsh (cwds) * Chris Walsh (cwds)
* Dan Sosedoff (sosedoff) * Dan Sosedoff (sosedoff)
* Daniel Farina (fdr) * Daniel Farina (fdr)
* Eric Chlebek (echlebek) * Eric Chlebek (echlebek)
* Eric Garrido (minusnine)
* Eric Urban (hydrogen18)
* Everyone at The Go Team * Everyone at The Go Team
* Evan Shaw (edsrzf) * Evan Shaw (edsrzf)
* Ewan Chou (coocood) * Ewan Chou (coocood)
@@ -94,5 +97,7 @@ code still exists in here.
* Ryan Smith (ryandotsmith) * Ryan Smith (ryandotsmith)
* Samuel Stauffer (samuel) * Samuel Stauffer (samuel)
* Timothée Peignier (cyberdelia) * Timothée Peignier (cyberdelia)
* Travis Cline (tmc)
* TruongSinh Tran-Nguyen (truongsinh) * TruongSinh Tran-Nguyen (truongsinh)
* Yaismel Miranda (ympons)
* notedit (notedit) * notedit (notedit)
+7 -6
View File
@@ -7,7 +7,6 @@ import (
"bytes" "bytes"
"database/sql" "database/sql"
"database/sql/driver" "database/sql/driver"
"github.com/lib/pq/oid"
"io" "io"
"math/rand" "math/rand"
"net" "net"
@@ -17,6 +16,8 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/lib/pq/oid"
) )
var ( var (
@@ -324,7 +325,7 @@ var testIntBytes = []byte("1234")
func BenchmarkDecodeInt64(b *testing.B) { func BenchmarkDecodeInt64(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testIntBytes, oid.T_int8) decode(&parameterStatus{}, testIntBytes, oid.T_int8, formatText)
} }
} }
@@ -332,7 +333,7 @@ var testFloatBytes = []byte("3.14159")
func BenchmarkDecodeFloat64(b *testing.B) { func BenchmarkDecodeFloat64(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testFloatBytes, oid.T_float8) decode(&parameterStatus{}, testFloatBytes, oid.T_float8, formatText)
} }
} }
@@ -340,7 +341,7 @@ var testBoolBytes = []byte{'t'}
func BenchmarkDecodeBool(b *testing.B) { func BenchmarkDecodeBool(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testBoolBytes, oid.T_bool) decode(&parameterStatus{}, testBoolBytes, oid.T_bool, formatText)
} }
} }
@@ -357,7 +358,7 @@ var testTimestamptzBytes = []byte("2013-09-17 22:15:32.360754-07")
func BenchmarkDecodeTimestamptz(b *testing.B) { func BenchmarkDecodeTimestamptz(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz) decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText)
} }
} }
@@ -370,7 +371,7 @@ func BenchmarkDecodeTimestamptzMultiThread(b *testing.B) {
f := func(wg *sync.WaitGroup, loops int) { f := func(wg *sync.WaitGroup, loops int) {
defer wg.Done() defer wg.Done()
for i := 0; i < loops; i++ { for i := 0; i < loops; i++ {
decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz) decode(&parameterStatus{}, testTimestamptzBytes, oid.T_timestamptz, formatText)
} }
} }
+23 -6
View File
@@ -3,6 +3,7 @@ package pq
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"github.com/lib/pq/oid" "github.com/lib/pq/oid"
) )
@@ -46,28 +47,44 @@ func (b *readBuf) byte() byte {
return b.next(1)[0] return b.next(1)[0]
} }
type writeBuf []byte type writeBuf struct {
buf []byte
pos int
}
func (b *writeBuf) int32(n int) { func (b *writeBuf) int32(n int) {
x := make([]byte, 4) x := make([]byte, 4)
binary.BigEndian.PutUint32(x, uint32(n)) binary.BigEndian.PutUint32(x, uint32(n))
*b = append(*b, x...) b.buf = append(b.buf, x...)
} }
func (b *writeBuf) int16(n int) { func (b *writeBuf) int16(n int) {
x := make([]byte, 2) x := make([]byte, 2)
binary.BigEndian.PutUint16(x, uint16(n)) binary.BigEndian.PutUint16(x, uint16(n))
*b = append(*b, x...) b.buf = append(b.buf, x...)
} }
func (b *writeBuf) string(s string) { func (b *writeBuf) string(s string) {
*b = append(*b, (s + "\000")...) b.buf = append(b.buf, (s + "\000")...)
} }
func (b *writeBuf) byte(c byte) { func (b *writeBuf) byte(c byte) {
*b = append(*b, c) b.buf = append(b.buf, c)
} }
func (b *writeBuf) bytes(v []byte) { func (b *writeBuf) bytes(v []byte) {
*b = append(*b, v...) b.buf = append(b.buf, v...)
}
func (b *writeBuf) wrap() []byte {
p := b.buf[b.pos:]
binary.BigEndian.PutUint32(p, uint32(len(p)))
return b.buf
}
func (b *writeBuf) next(c byte) {
p := b.buf[b.pos:]
binary.BigEndian.PutUint32(p, uint32(len(p)))
b.pos = len(b.buf) + 1
b.buf = append(b.buf, c, 0, 0, 0, 0)
} }
+470 -197
View File
@@ -10,7 +10,6 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"github.com/lib/pq/oid"
"io" "io"
"io/ioutil" "io/ioutil"
"net" "net"
@@ -22,6 +21,8 @@ import (
"strings" "strings"
"time" "time"
"unicode" "unicode"
"github.com/lib/pq/oid"
) )
// Common error types // Common error types
@@ -105,12 +106,49 @@ type conn struct {
// If true, this connection is bad and all public-facing functions should // If true, this connection is bad and all public-facing functions should
// return ErrBadConn. // return ErrBadConn.
bad bool bad bool
// If set, this connection should never use the binary format when
// receiving query results from prepared statements. Only provided for
// debugging.
disablePreparedBinaryResult bool
// Whether to always send []byte parameters over as binary. Enables single
// round-trip mode for non-prepared Query calls.
binaryParameters bool
}
// Handle driver-side settings in parsed connection string.
func (c *conn) handleDriverSettings(o values) (err error) {
boolSetting := func(key string, val *bool) error {
if value := o.Get(key); value != "" {
if value == "yes" {
*val = true
} else if value == "no" {
*val = false
} else {
return fmt.Errorf("unrecognized value %q for %s", value, key)
}
}
return nil
}
err = boolSetting("disable_prepared_binary_result", &c.disablePreparedBinaryResult)
if err != nil {
return err
}
err = boolSetting("binary_parameters", &c.binaryParameters)
if err != nil {
return err
}
return nil
} }
func (c *conn) writeBuf(b byte) *writeBuf { func (c *conn) writeBuf(b byte) *writeBuf {
c.scratch[0] = b c.scratch[0] = b
w := writeBuf(c.scratch[:5]) return &writeBuf{
return &w buf: c.scratch[:5],
pos: 1,
}
} }
func Open(name string) (_ driver.Conn, err error) { func Open(name string) (_ driver.Conn, err error) {
@@ -118,22 +156,11 @@ func Open(name string) (_ driver.Conn, err error) {
} }
func DialOpen(d Dialer, name string) (_ driver.Conn, err error) { func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
defer func() { // Handle any panics during connection initialization. Note that we
// Handle any panics during connection initialization. Note that we // specifically do *not* want to use errRecover(), as that would turn any
// specifically do *not* want to use errRecover(), as that would turn // connection errors into ErrBadConns, hiding the real error message from
// any connection errors into ErrBadConns, hiding the real error // the user.
// message from the user. defer errRecoverNoErrBadConn(&err)
e := recover()
if e == nil {
// Do nothing
return
}
var ok bool
err, ok = e.(error)
if !ok {
err = fmt.Errorf("pq: unexpected error: %#v", e)
}
}()
o := make(values) o := make(values)
@@ -151,7 +178,7 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
o.Set(k, v) o.Set(k, v)
} }
if strings.HasPrefix(name, "postgres://") { if strings.HasPrefix(name, "postgres://") || strings.HasPrefix(name, "postgresql://") {
name, err = ParseURL(name) name, err = ParseURL(name)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -202,27 +229,36 @@ func DialOpen(d Dialer, name string) (_ driver.Conn, err error) {
} }
} }
c, err := dial(d, o) cn := &conn{}
err = cn.handleDriverSettings(o)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cn := &conn{c: c} cn.c, err = dial(d, o)
if err != nil {
return nil, err
}
cn.ssl(o) cn.ssl(o)
cn.buf = bufio.NewReader(cn.c) cn.buf = bufio.NewReader(cn.c)
cn.startup(o) cn.startup(o)
// reset the deadline, in case one was set (see dial) // reset the deadline, in case one was set (see dial)
err = cn.c.SetDeadline(time.Time{}) if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" {
err = cn.c.SetDeadline(time.Time{})
}
return cn, err return cn, err
} }
func dial(d Dialer, o values) (net.Conn, error) { func dial(d Dialer, o values) (net.Conn, error) {
ntw, addr := network(o) ntw, addr := network(o)
// SSL is not necessary or supported over UNIX domain sockets
timeout := o.Get("connect_timeout") if ntw == "unix" {
o["sslmode"] = "disable"
}
// Zero or not specified means wait indefinitely. // Zero or not specified means wait indefinitely.
if timeout != "" && timeout != "0" { if timeout := o.Get("connect_timeout"); timeout != "" && timeout != "0" {
seconds, err := strconv.ParseInt(timeout, 10, 0) seconds, err := strconv.ParseInt(timeout, 10, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err) return nil, fmt.Errorf("invalid value for parameter connect_timeout: %s", err)
@@ -436,6 +472,9 @@ func (cn *conn) Commit() (err error) {
_, commandTag, err := cn.simpleExec("COMMIT") _, commandTag, err := cn.simpleExec("COMMIT")
if err != nil { if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err return err
} }
if commandTag != "COMMIT" { if commandTag != "COMMIT" {
@@ -455,6 +494,9 @@ func (cn *conn) Rollback() (err error) {
cn.checkIsInTransaction(true) cn.checkIsInTransaction(true)
_, commandTag, err := cn.simpleExec("ROLLBACK") _, commandTag, err := cn.simpleExec("ROLLBACK")
if err != nil { if err != nil {
if cn.isInTransaction() {
cn.bad = true
}
return err return err
} }
if commandTag != "ROLLBACK" { if commandTag != "ROLLBACK" {
@@ -494,7 +536,7 @@ func (cn *conn) simpleExec(q string) (res driver.Result, commandTag string, err
} }
} }
func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) { func (cn *conn) simpleQuery(q string) (res *rows, err error) {
defer cn.errRecover(&err) defer cn.errRecover(&err)
st := &stmt{cn: cn, name: ""} st := &stmt{cn: cn, name: ""}
@@ -515,7 +557,13 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
cn.bad = true cn.bad = true
errorf("unexpected message %q in simple query execution", t) errorf("unexpected message %q in simple query execution", t)
} }
res = &rows{st: st, done: true} res = &rows{
cn: cn,
colNames: st.colNames,
colTyps: st.colTyps,
colFmts: st.colFmts,
done: true,
}
case 'Z': case 'Z':
cn.processReadyForQuery(r) cn.processReadyForQuery(r)
// done // done
@@ -534,8 +582,8 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
case 'T': case 'T':
// res might be non-nil here if we received a previous // res might be non-nil here if we received a previous
// CommandComplete, but that's fine; just overwrite it // CommandComplete, but that's fine; just overwrite it
res = &rows{st: st} res = &rows{cn: cn}
st.cols, st.rowTyps = parseMeta(r) res.colNames, res.colFmts, res.colTyps = parsePortalRowDescribe(r)
// To work around a bug in QueryRow in Go 1.2 and earlier, wait // To work around a bug in QueryRow in Go 1.2 and earlier, wait
// until the first DataRow has been received. // until the first DataRow has been received.
@@ -546,47 +594,74 @@ func (cn *conn) simpleQuery(q string) (res driver.Rows, err error) {
} }
} }
func (cn *conn) prepareTo(q, stmtName string) (_ *stmt, err error) { // Decides which column formats to use for a prepared statement. The input is
// an array of type oids, one element per result column.
func decideColumnFormats(colTyps []oid.Oid, forceText bool) (colFmts []format, colFmtData []byte) {
if len(colTyps) == 0 {
return nil, colFmtDataAllText
}
colFmts = make([]format, len(colTyps))
if forceText {
return colFmts, colFmtDataAllText
}
allBinary := true
allText := true
for i, o := range colTyps {
switch o {
// This is the list of types to use binary mode for when receiving them
// through a prepared statement. If a type appears in this list, it
// must also be implemented in binaryDecode in encode.go.
case oid.T_bytea:
fallthrough
case oid.T_int8:
fallthrough
case oid.T_int4:
fallthrough
case oid.T_int2:
colFmts[i] = formatBinary
allText = false
default:
allBinary = false
}
}
if allBinary {
return colFmts, colFmtDataAllBinary
} else if allText {
return colFmts, colFmtDataAllText
} else {
colFmtData = make([]byte, 2+len(colFmts)*2)
binary.BigEndian.PutUint16(colFmtData, uint16(len(colFmts)))
for i, v := range colFmts {
binary.BigEndian.PutUint16(colFmtData[2+i*2:], uint16(v))
}
return colFmts, colFmtData
}
}
func (cn *conn) prepareTo(q, stmtName string) *stmt {
st := &stmt{cn: cn, name: stmtName} st := &stmt{cn: cn, name: stmtName}
b := cn.writeBuf('P') b := cn.writeBuf('P')
b.string(st.name) b.string(st.name)
b.string(q) b.string(q)
b.int16(0) b.int16(0)
cn.send(b)
b = cn.writeBuf('D') b.next('D')
b.byte('S') b.byte('S')
b.string(st.name) b.string(st.name)
b.next('S')
cn.send(b) cn.send(b)
cn.send(cn.writeBuf('S')) cn.readParseResponse()
st.paramTyps, st.colNames, st.colTyps = cn.readStatementDescribeResponse()
for { st.colFmts, st.colFmtData = decideColumnFormats(st.colTyps, cn.disablePreparedBinaryResult)
t, r := cn.recv1() cn.readReadyForQuery()
switch t { return st
case '1':
case 't':
nparams := r.int16()
st.paramTyps = make([]oid.Oid, nparams)
for i := range st.paramTyps {
st.paramTyps[i] = r.oid()
}
case 'T':
st.cols, st.rowTyps = parseMeta(r)
case 'n':
// no data
case 'Z':
cn.processReadyForQuery(r)
return st, err
case 'E':
err = parseError(r)
default:
cn.bad = true
errorf("unexpected describe rows response: %q", t)
}
}
} }
func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) { func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) {
@@ -598,7 +673,7 @@ func (cn *conn) Prepare(q string) (_ driver.Stmt, err error) {
if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") { if len(q) >= 4 && strings.EqualFold(q[:4], "COPY") {
return cn.prepareCopyIn(q) return cn.prepareCopyIn(q)
} }
return cn.prepareTo(q, cn.gname()) return cn.prepareTo(q, cn.gname()), nil
} }
func (cn *conn) Close() (err error) { func (cn *conn) Close() (err error) {
@@ -630,17 +705,29 @@ func (cn *conn) Query(query string, args []driver.Value) (_ driver.Rows, err err
return cn.simpleQuery(query) return cn.simpleQuery(query)
} }
st, err := cn.prepareTo(query, "") if cn.binaryParameters {
if err != nil { cn.sendBinaryModeQuery(query, args)
panic(err)
}
st.exec(args) cn.readParseResponse()
return &rows{st: st}, nil cn.readBindResponse()
rows := &rows{cn: cn}
rows.colNames, rows.colFmts, rows.colTyps = cn.readPortalDescribeResponse()
cn.postExecuteWorkaround()
return rows, nil
} else {
st := cn.prepareTo(query, "")
st.exec(args)
return &rows{
cn: cn,
colNames: st.colNames,
colTyps: st.colTyps,
colFmts: st.colFmts,
}, nil
}
} }
// Implement the optional "Execer" interface for one-shot queries // Implement the optional "Execer" interface for one-shot queries
func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err error) { func (cn *conn) Exec(query string, args []driver.Value) (res driver.Result, err error) {
if cn.bad { if cn.bad {
return nil, driver.ErrBadConn return nil, driver.ErrBadConn
} }
@@ -654,32 +741,42 @@ func (cn *conn) Exec(query string, args []driver.Value) (_ driver.Result, err er
return r, err return r, err
} }
// Use the unnamed statement to defer planning until bind if cn.binaryParameters {
// time, or else value-based selectivity estimates cannot be cn.sendBinaryModeQuery(query, args)
// used.
st, err := cn.prepareTo(query, "")
if err != nil {
panic(err)
}
r, err := st.Exec(args) cn.readParseResponse()
if err != nil { cn.readBindResponse()
panic(err) cn.readPortalDescribeResponse()
cn.postExecuteWorkaround()
res, _, err = cn.readExecuteResponse("Execute")
return res, err
} else {
// Use the unnamed statement to defer planning until bind
// time, or else value-based selectivity estimates cannot be
// used.
st := cn.prepareTo(query, "")
r, err := st.Exec(args)
if err != nil {
panic(err)
}
return r, err
} }
return r, err
} }
// Assumes len(*m) is > 5
func (cn *conn) send(m *writeBuf) { func (cn *conn) send(m *writeBuf) {
b := (*m)[1:] _, err := cn.c.Write(m.wrap())
binary.BigEndian.PutUint32(b, uint32(len(b))) if err != nil {
panic(err)
}
}
if (*m)[0] == 0 { func (cn *conn) sendStartupPacket(m *writeBuf) {
*m = b // sanity check
if m.buf[0] != 0 {
panic("oops")
} }
_, err := cn.c.Write(*m) _, err := cn.c.Write((m.wrap())[1:])
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -819,7 +916,7 @@ func (cn *conn) ssl(o values) {
w := cn.writeBuf(0) w := cn.writeBuf(0)
w.int32(80877103) w.int32(80877103)
cn.send(w) cn.sendStartupPacket(w)
b := cn.scratch[:1] b := cn.scratch[:1]
_, err := io.ReadFull(cn.c, b) _, err := io.ReadFull(cn.c, b)
@@ -956,6 +1053,10 @@ func isDriverSetting(key string) bool {
return true return true
case "connect_timeout": case "connect_timeout":
return true return true
case "disable_prepared_binary_result":
return true
case "binary_parameters":
return true
default: default:
return false return false
@@ -983,7 +1084,7 @@ func (cn *conn) startup(o values) {
w.string(v) w.string(v)
} }
w.string("") w.string("")
cn.send(w) cn.sendStartupPacket(w)
for { for {
t, r := cn.recv() t, r := cn.recv()
@@ -1038,13 +1139,26 @@ func (cn *conn) auth(r *readBuf, o values) {
} }
} }
type format int
const formatText format = 0
const formatBinary format = 1
// One result-column format code with the value 1 (i.e. all binary).
var colFmtDataAllBinary []byte = []byte{0, 1, 0, 1}
// No result-column format codes (i.e. all text).
var colFmtDataAllText []byte = []byte{0, 0}
type stmt struct { type stmt struct {
cn *conn cn *conn
name string name string
cols []string colNames []string
rowTyps []oid.Oid colFmts []format
paramTyps []oid.Oid colFmtData []byte
closed bool colTyps []oid.Oid
paramTyps []oid.Oid
closed bool
} }
func (st *stmt) Close() (err error) { func (st *stmt) Close() (err error) {
@@ -1087,7 +1201,12 @@ func (st *stmt) Query(v []driver.Value) (r driver.Rows, err error) {
defer st.cn.errRecover(&err) defer st.cn.errRecover(&err)
st.exec(v) st.exec(v)
return &rows{st: st}, nil return &rows{
cn: st.cn,
colNames: st.colNames,
colTyps: st.colTyps,
colFmts: st.colFmts,
}, nil
} }
func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) { func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) {
@@ -1097,25 +1216,8 @@ func (st *stmt) Exec(v []driver.Value) (res driver.Result, err error) {
defer st.cn.errRecover(&err) defer st.cn.errRecover(&err)
st.exec(v) st.exec(v)
res, _, err = st.cn.readExecuteResponse("simple query")
for { return res, err
t, r := st.cn.recv1()
switch t {
case 'E':
err = parseError(r)
case 'C':
res, _ = st.cn.parseComplete(r.string())
case 'Z':
st.cn.processReadyForQuery(r)
// done
return
case 'T', 'D', 'I':
// ignore any results
default:
st.cn.bad = true
errorf("unknown exec response: %q", t)
}
}
} }
func (st *stmt) exec(v []driver.Value) { func (st *stmt) exec(v []driver.Value) {
@@ -1126,84 +1228,38 @@ func (st *stmt) exec(v []driver.Value) {
errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps)) errorf("got %d parameters but the statement requires %d", len(v), len(st.paramTyps))
} }
w := st.cn.writeBuf('B') cn := st.cn
w.string("") w := cn.writeBuf('B')
w.byte(0) // unnamed portal
w.string(st.name) w.string(st.name)
w.int16(0)
w.int16(len(v)) if cn.binaryParameters {
for i, x := range v { cn.sendBinaryParameters(w, v)
if x == nil { } else {
w.int32(-1) w.int16(0)
} else { w.int16(len(v))
b := encode(&st.cn.parameterStatus, x, st.paramTyps[i]) for i, x := range v {
w.int32(len(b)) if x == nil {
w.bytes(b) w.int32(-1)
} else {
b := encode(&cn.parameterStatus, x, st.paramTyps[i])
w.int32(len(b))
w.bytes(b)
}
} }
} }
w.int16(0) w.bytes(st.colFmtData)
st.cn.send(w)
w = st.cn.writeBuf('E') w.next('E')
w.string("") w.byte(0)
w.int32(0) w.int32(0)
st.cn.send(w)
st.cn.send(st.cn.writeBuf('S')) w.next('S')
cn.send(w)
var err error cn.readBindResponse()
for { cn.postExecuteWorkaround()
t, r := st.cn.recv1()
switch t {
case 'E':
err = parseError(r)
case '2':
if err != nil {
panic(err)
}
goto workaround
case 'Z':
st.cn.processReadyForQuery(r)
if err != nil {
panic(err)
}
return
default:
st.cn.bad = true
errorf("unexpected bind response: %q", t)
}
}
// Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores
// any errors from rows.Next, which masks errors that happened during the
// execution of the query. To avoid the problem in common cases, we wait
// here for one more message from the database. If it's not an error the
// query will likely succeed (or perhaps has already, if it's a
// CommandComplete), so we push the message into the conn struct; recv1
// will return it as the next message for rows.Next or rows.Close.
// However, if it's an error, we wait until ReadyForQuery and then return
// the error to our caller.
workaround:
for {
t, r := st.cn.recv1()
switch t {
case 'E':
err = parseError(r)
case 'C', 'D', 'I':
// the query didn't fail, but we can't process this message
st.cn.saveMessage(t, r)
return
case 'Z':
if err == nil {
st.cn.bad = true
errorf("unexpected ReadyForQuery during extended query execution")
}
st.cn.processReadyForQuery(r)
panic(err)
default:
st.cn.bad = true
errorf("unexpected message during query execution: %q", t)
}
}
} }
func (st *stmt) NumInput() int { func (st *stmt) NumInput() int {
@@ -1260,9 +1316,12 @@ func (cn *conn) parseComplete(commandTag string) (driver.Result, string) {
} }
type rows struct { type rows struct {
st *stmt cn *conn
done bool colNames []string
rb readBuf colTyps []oid.Oid
colFmts []format
done bool
rb readBuf
} }
func (rs *rows) Close() error { func (rs *rows) Close() error {
@@ -1280,7 +1339,7 @@ func (rs *rows) Close() error {
} }
func (rs *rows) Columns() []string { func (rs *rows) Columns() []string {
return rs.st.cols return rs.colNames
} }
func (rs *rows) Next(dest []driver.Value) (err error) { func (rs *rows) Next(dest []driver.Value) (err error) {
@@ -1288,7 +1347,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) {
return io.EOF return io.EOF
} }
conn := rs.st.cn conn := rs.cn
if conn.bad { if conn.bad {
return driver.ErrBadConn return driver.ErrBadConn
} }
@@ -1319,7 +1378,7 @@ func (rs *rows) Next(dest []driver.Value) (err error) {
dest[i] = nil dest[i] = nil
continue continue
} }
dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.st.rowTyps[i]) dest[i] = decode(&conn.parameterStatus, rs.rb.next(l), rs.colTyps[i], rs.colFmts[i])
} }
return return
default: default:
@@ -1352,6 +1411,68 @@ func md5s(s string) string {
return fmt.Sprintf("%x", h.Sum(nil)) return fmt.Sprintf("%x", h.Sum(nil))
} }
func (cn *conn) sendBinaryParameters(b *writeBuf, args []driver.Value) {
// Do one pass over the parameters to see if we're going to send any of
// them over in binary. If we are, create a paramFormats array at the
// same time.
var paramFormats []int
for i, x := range args {
_, ok := x.([]byte)
if ok {
if paramFormats == nil {
paramFormats = make([]int, len(args))
}
paramFormats[i] = 1
}
}
if paramFormats == nil {
b.int16(0)
} else {
b.int16(len(paramFormats))
for _, x := range paramFormats {
b.int16(x)
}
}
b.int16(len(args))
for _, x := range args {
if x == nil {
b.int32(-1)
} else {
datum := binaryEncode(&cn.parameterStatus, x)
b.int32(len(datum))
b.bytes(datum)
}
}
}
func (cn *conn) sendBinaryModeQuery(query string, args []driver.Value) {
if len(args) >= 65536 {
errorf("got %d parameters but PostgreSQL only supports 65535 parameters", len(args))
}
b := cn.writeBuf('P')
b.byte(0) // unnamed statement
b.string(query)
b.int16(0)
b.next('B')
b.int16(0) // unnamed portal and statement
cn.sendBinaryParameters(b, args)
b.bytes(colFmtDataAllText)
b.next('D')
b.byte('P')
b.byte(0) // unnamed portal
b.next('E')
b.byte(0)
b.int32(0)
b.next('S')
cn.send(b)
}
func (c *conn) processParameterStatus(r *readBuf) { func (c *conn) processParameterStatus(r *readBuf) {
var err error var err error
@@ -1381,15 +1502,167 @@ func (c *conn) processReadyForQuery(r *readBuf) {
c.txnStatus = transactionStatus(r.byte()) c.txnStatus = transactionStatus(r.byte())
} }
func parseMeta(r *readBuf) (cols []string, rowTyps []oid.Oid) { func (cn *conn) readReadyForQuery() {
t, r := cn.recv1()
switch t {
case 'Z':
cn.processReadyForQuery(r)
return
default:
cn.bad = true
errorf("unexpected message %q; expected ReadyForQuery", t)
}
}
func (cn *conn) readParseResponse() {
t, r := cn.recv1()
switch t {
case '1':
return
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Parse response %q", t)
}
}
func (cn *conn) readStatementDescribeResponse() (paramTyps []oid.Oid, colNames []string, colTyps []oid.Oid) {
for {
t, r := cn.recv1()
switch t {
case 't':
nparams := r.int16()
paramTyps = make([]oid.Oid, nparams)
for i := range paramTyps {
paramTyps[i] = r.oid()
}
case 'n':
return paramTyps, nil, nil
case 'T':
colNames, colTyps = parseStatementRowDescribe(r)
return paramTyps, colNames, colTyps
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Describe statement response %q", t)
}
}
}
func (cn *conn) readPortalDescribeResponse() (colNames []string, colFmts []format, colTyps []oid.Oid) {
t, r := cn.recv1()
switch t {
case 'T':
return parsePortalRowDescribe(r)
case 'n':
return nil, nil, nil
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Describe response %q", t)
}
panic("not reached")
}
func (cn *conn) readBindResponse() {
t, r := cn.recv1()
switch t {
case '2':
return
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
default:
cn.bad = true
errorf("unexpected Bind response %q", t)
}
}
func (cn *conn) postExecuteWorkaround() {
// Work around a bug in sql.DB.QueryRow: in Go 1.2 and earlier it ignores
// any errors from rows.Next, which masks errors that happened during the
// execution of the query. To avoid the problem in common cases, we wait
// here for one more message from the database. If it's not an error the
// query will likely succeed (or perhaps has already, if it's a
// CommandComplete), so we push the message into the conn struct; recv1
// will return it as the next message for rows.Next or rows.Close.
// However, if it's an error, we wait until ReadyForQuery and then return
// the error to our caller.
for {
t, r := cn.recv1()
switch t {
case 'E':
err := parseError(r)
cn.readReadyForQuery()
panic(err)
case 'C', 'D', 'I':
// the query didn't fail, but we can't process this message
cn.saveMessage(t, r)
return
default:
cn.bad = true
errorf("unexpected message during extended query execution: %q", t)
}
}
}
// Only for Exec(), since we ignore the returned data
func (cn *conn) readExecuteResponse(protocolState string) (res driver.Result, commandTag string, err error) {
for {
t, r := cn.recv1()
switch t {
case 'C':
res, commandTag = cn.parseComplete(r.string())
case 'Z':
cn.processReadyForQuery(r)
return res, commandTag, err
case 'E':
err = parseError(r)
case 'T', 'D', 'I':
// ignore any results
default:
cn.bad = true
errorf("unknown %s response: %q", protocolState, t)
}
}
}
func parseStatementRowDescribe(r *readBuf) (colNames []string, colTyps []oid.Oid) {
n := r.int16() n := r.int16()
cols = make([]string, n) colNames = make([]string, n)
rowTyps = make([]oid.Oid, n) colTyps = make([]oid.Oid, n)
for i := range cols { for i := range colNames {
cols[i] = r.string() colNames[i] = r.string()
r.next(6) r.next(6)
rowTyps[i] = r.oid() colTyps[i] = r.oid()
r.next(8) r.next(6)
// format code not known when describing a statement; always 0
r.next(2)
}
return
}
func parsePortalRowDescribe(r *readBuf) (colNames []string, colFmts []format, colTyps []oid.Oid) {
n := r.int16()
colNames = make([]string, n)
colFmts = make([]format, n)
colTyps = make([]oid.Oid, n)
for i := range colNames {
colNames[i] = r.string()
r.next(6)
colTyps[i] = r.oid()
r.next(6)
colFmts[i] = format(r.int16())
} }
return return
} }
+47 -27
View File
@@ -7,6 +7,7 @@ import (
"io" "io"
"os" "os"
"reflect" "reflect"
"strings"
"testing" "testing"
"time" "time"
) )
@@ -15,21 +16,31 @@ type Fatalistic interface {
Fatal(args ...interface{}) Fatal(args ...interface{})
} }
func forceBinaryParameters() bool {
bp := os.Getenv("PQTEST_BINARY_PARAMETERS")
if bp == "yes" {
return true
} else if bp == "" || bp == "no" {
return false
} else {
panic("unexpected value for PQTEST_BINARY_PARAMETERS")
}
}
func openTestConnConninfo(conninfo string) (*sql.DB, error) { func openTestConnConninfo(conninfo string) (*sql.DB, error) {
datname := os.Getenv("PGDATABASE") defaultTo := func(envvar string, value string) {
sslmode := os.Getenv("PGSSLMODE") if os.Getenv(envvar) == "" {
timeout := os.Getenv("PGCONNECT_TIMEOUT") os.Setenv(envvar, value)
}
if datname == "" {
os.Setenv("PGDATABASE", "pqgotest")
} }
defaultTo("PGDATABASE", "pqgotest")
defaultTo("PGSSLMODE", "disable")
defaultTo("PGCONNECT_TIMEOUT", "20")
if sslmode == "" { if forceBinaryParameters() &&
os.Setenv("PGSSLMODE", "disable") !strings.HasPrefix(conninfo, "postgres://") &&
} !strings.HasPrefix(conninfo, "postgresql://") {
conninfo = conninfo + " binary_parameters=yes"
if timeout == "" {
os.Setenv("PGCONNECT_TIMEOUT", "20")
} }
return sql.Open("postgres", conninfo) return sql.Open("postgres", conninfo)
@@ -106,18 +117,22 @@ func TestCommitInFailedTransaction(t *testing.T) {
} }
func TestOpenURL(t *testing.T) { func TestOpenURL(t *testing.T) {
db, err := openTestConnConninfo("postgres://") testURL := func(url string) {
if err != nil { db, err := openTestConnConninfo(url)
t.Fatal(err) if err != nil {
t.Fatal(err)
}
defer db.Close()
// database/sql might not call our Open at all unless we do something with
// the connection
txn, err := db.Begin()
if err != nil {
t.Fatal(err)
}
txn.Rollback()
} }
defer db.Close() testURL("postgres://")
// database/sql might not call our Open at all unless we do something with testURL("postgresql://")
// the connection
txn, err := db.Begin()
if err != nil {
t.Fatal(err)
}
txn.Rollback()
} }
func TestExec(t *testing.T) { func TestExec(t *testing.T) {
@@ -342,6 +357,7 @@ func TestEncodeDecode(t *testing.T) {
'2000-1-1 01:02:03.04-7'::timestamptz, '2000-1-1 01:02:03.04-7'::timestamptz,
0::boolean, 0::boolean,
123, 123,
-321,
3.14::float8 3.14::float8
WHERE WHERE
E'\\000\\001\\002'::bytea = $1 E'\\000\\001\\002'::bytea = $1
@@ -370,9 +386,9 @@ func TestEncodeDecode(t *testing.T) {
var got2 string var got2 string
var got3 = sql.NullInt64{Valid: true} var got3 = sql.NullInt64{Valid: true}
var got4 time.Time var got4 time.Time
var got5, got6, got7 interface{} var got5, got6, got7, got8 interface{}
err = r.Scan(&got1, &got2, &got3, &got4, &got5, &got6, &got7) err = r.Scan(&got1, &got2, &got3, &got4, &got5, &got6, &got7, &got8)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -401,8 +417,12 @@ func TestEncodeDecode(t *testing.T) {
t.Fatalf("expected 123, got %d", got6) t.Fatalf("expected 123, got %d", got6)
} }
if got7 != float64(3.14) { if got7 != int64(-321) {
t.Fatalf("expected 3.14, got %f", got7) t.Fatalf("expected -321, got %d", got7)
}
if got8 != float64(3.14) {
t.Fatalf("expected 3.14, got %f", got8)
} }
} }
+2
View File
@@ -150,6 +150,8 @@ func (ci *copyin) resploop() {
switch t { switch t {
case 'C': case 'C':
// complete // complete
case 'N':
// NoticeResponse
case 'Z': case 'Z':
ci.cn.processReadyForQuery(&r) ci.cn.processReadyForQuery(&r)
ci.done <- true ci.done <- true
+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) { func TestCopyInTypes(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()
@@ -307,12 +387,14 @@ func TestCopyRespLoopConnectionError(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// We have to try and send something over, since postgres won't process if getServerVersion(t, db) < 90500 {
// SIGTERMs while it's waiting for CopyData/CopyEnd messages; see // We have to try and send something over, since postgres before
// tcop/postgres.c. // version 9.5 won't process SIGTERMs while it's waiting for
_, err = stmt.Exec(1) // CopyData/CopyEnd messages; see tcop/postgres.c.
if err != nil { _, err = stmt.Exec(1)
t.Fatal(err) if err != nil {
t.Fatal(err)
}
} }
_, err = stmt.Exec() _, err = stmt.Exec()
if err == nil { if err == nil {
+2 -1
View File
@@ -5,8 +5,9 @@ In most cases clients will use the database/sql package instead of
using this package directly. For example: using this package directly. For example:
import ( import (
_ "github.com/lib/pq"
"database/sql" "database/sql"
_ "github.com/lib/pq"
) )
func main() { func main() {
+135 -34
View File
@@ -3,24 +3,34 @@ package pq
import ( import (
"bytes" "bytes"
"database/sql/driver" "database/sql/driver"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/lib/pq/oid"
"math" "math"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/lib/pq/oid"
) )
func binaryEncode(parameterStatus *parameterStatus, x interface{}) []byte {
switch v := x.(type) {
case []byte:
return v
default:
return encode(parameterStatus, x, oid.T_unknown)
}
panic("not reached")
}
func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte { func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) []byte {
switch v := x.(type) { switch v := x.(type) {
case int64: case int64:
return []byte(fmt.Sprintf("%d", v)) return strconv.AppendInt(nil, v, 10)
case float32:
return []byte(fmt.Sprintf("%.9f", v))
case float64: case float64:
return []byte(fmt.Sprintf("%.17f", v)) return strconv.AppendFloat(nil, v, 'f', -1, 64)
case []byte: case []byte:
if pgtypOid == oid.T_bytea { if pgtypOid == oid.T_bytea {
return encodeBytea(parameterStatus.serverVersion, v) return encodeBytea(parameterStatus.serverVersion, v)
@@ -34,7 +44,7 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [
return []byte(v) return []byte(v)
case bool: case bool:
return []byte(fmt.Sprintf("%t", v)) return strconv.AppendBool(nil, v)
case time.Time: case time.Time:
return formatTs(v) return formatTs(v)
@@ -45,7 +55,33 @@ func encode(parameterStatus *parameterStatus, x interface{}, pgtypOid oid.Oid) [
panic("not reached") panic("not reached")
} }
func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} { func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid, f format) interface{} {
if f == formatBinary {
return binaryDecode(parameterStatus, s, typ)
} else {
return textDecode(parameterStatus, s, typ)
}
}
func binaryDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} {
switch typ {
case oid.T_bytea:
return s
case oid.T_int8:
return int64(binary.BigEndian.Uint64(s))
case oid.T_int4:
return int64(int32(binary.BigEndian.Uint32(s)))
case oid.T_int2:
return int64(int16(binary.BigEndian.Uint16(s)))
default:
errorf("don't know how to decode binary parameter of type %u", uint32(typ))
}
panic("not reached")
}
func textDecode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{} {
switch typ { switch typ {
case oid.T_bytea: case oid.T_bytea:
return parseBytea(s) return parseBytea(s)
@@ -59,7 +95,7 @@ func decode(parameterStatus *parameterStatus, s []byte, typ oid.Oid) interface{}
return mustParse("15:04:05-07", typ, s) return mustParse("15:04:05-07", typ, s)
case oid.T_bool: case oid.T_bool:
return s[0] == 't' return s[0] == 't'
case oid.T_int8, oid.T_int2, oid.T_int4: case oid.T_int8, oid.T_int4, oid.T_int2:
i, err := strconv.ParseInt(string(s), 10, 64) i, err := strconv.ParseInt(string(s), 10, 64)
if err != nil { if err != nil {
errorf("%s", err) errorf("%s", err)
@@ -86,8 +122,6 @@ func appendEncodedText(parameterStatus *parameterStatus, buf []byte, x interface
switch v := x.(type) { switch v := x.(type) {
case int64: case int64:
return strconv.AppendInt(buf, v, 10) return strconv.AppendInt(buf, v, 10)
case float32:
return strconv.AppendFloat(buf, float64(v), 'f', -1, 32)
case float64: case float64:
return strconv.AppendFloat(buf, v, 'f', -1, 64) return strconv.AppendFloat(buf, v, 'f', -1, 64)
case []byte: case []byte:
@@ -149,12 +183,6 @@ func appendEscapedText(buf []byte, text string) []byte {
func mustParse(f string, typ oid.Oid, s []byte) time.Time { func mustParse(f string, typ oid.Oid, s []byte) time.Time {
str := string(s) str := string(s)
// Special case until time.Parse bug is fixed:
// http://code.google.com/p/go/issues/detail?id=3487
if str[len(str)-2] == '.' {
str += "0"
}
// check for a 30-minute-offset timezone // check for a 30-minute-offset timezone
if (typ == oid.T_timestamptz || typ == oid.T_timetz) && if (typ == oid.T_timestamptz || typ == oid.T_timetz) &&
str[len(str)-3] == ':' { str[len(str)-3] == ':' {
@@ -212,11 +240,72 @@ func (c *locationCache) getLocation(offset int) *time.Location {
return location return location
} }
var infinityTsEnabled = false
var infinityTsNegative time.Time
var infinityTsPositive time.Time
const (
infinityTsEnabledAlready = "pq: infinity timestamp enabled already"
infinityTsNegativeMustBeSmaller = "pq: infinity timestamp: negative value must be smaller (before) than positive"
)
/*
* If EnableInfinityTs is not called, "-infinity" and "infinity" will return
* []byte("-infinity") and []byte("infinity") respectively, and potentially
* cause error "sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time",
* when scanning into a time.Time value.
*
* Once EnableInfinityTs has been called, all connections created using this
* driver will decode Postgres' "-infinity" and "infinity" for "timestamp",
* "timestamp with time zone" and "date" types to the predefined minimum and
* maximum times, respectively. When encoding time.Time values, any time which
* equals or preceeds the predefined minimum time will be encoded to
* "-infinity". Any values at or past the maximum time will similarly be
* encoded to "infinity".
*
*
* If EnableInfinityTs is called with negative >= positive, it will panic.
* Calling EnableInfinityTs after a connection has been established results in
* undefined behavior. If EnableInfinityTs is called more than once, it will
* panic.
*/
func EnableInfinityTs(negative time.Time, positive time.Time) {
if infinityTsEnabled {
panic(infinityTsEnabledAlready)
}
if !negative.Before(positive) {
panic(infinityTsNegativeMustBeSmaller)
}
infinityTsEnabled = true
infinityTsNegative = negative
infinityTsPositive = positive
}
/*
* Testing might want to toggle infinityTsEnabled
*/
func disableInfinityTs() {
infinityTsEnabled = false
}
// This is a time function specific to the Postgres default DateStyle // This is a time function specific to the Postgres default DateStyle
// setting ("ISO, MDY"), the only one we currently support. This // setting ("ISO, MDY"), the only one we currently support. This
// accounts for the discrepancies between the parsing available with // accounts for the discrepancies between the parsing available with
// time.Parse and the Postgres date formatting quirks. // time.Parse and the Postgres date formatting quirks.
func parseTs(currentLocation *time.Location, str string) (result time.Time) { func parseTs(currentLocation *time.Location, str string) interface{} {
switch str {
case "-infinity":
if infinityTsEnabled {
return infinityTsNegative
}
return []byte(str)
case "infinity":
if infinityTsEnabled {
return infinityTsPositive
}
return []byte(str)
}
monSep := strings.IndexRune(str, '-') monSep := strings.IndexRune(str, '-')
// this is Gregorian year, not ISO Year // this is Gregorian year, not ISO Year
// In Gregorian system, the year 1 BC is followed by AD 1 // In Gregorian system, the year 1 BC is followed by AD 1
@@ -310,10 +399,18 @@ func parseTs(currentLocation *time.Location, str string) (result time.Time) {
return t return t
} }
// formatTs formats t as time.RFC3339Nano and appends time zone seconds if // formatTs formats t into a format postgres understands.
// needed.
func formatTs(t time.Time) (b []byte) { func formatTs(t time.Time) (b []byte) {
b = []byte(t.Format(time.RFC3339Nano)) if infinityTsEnabled {
// t <= -infinity : ! (t > -infinity)
if !t.After(infinityTsNegative) {
return []byte("-infinity")
}
// t >= infinity : ! (!t < infinity)
if !t.Before(infinityTsPositive) {
return []byte("infinity")
}
}
// Need to send dates before 0001 A.D. with " BC" suffix, instead of the // Need to send dates before 0001 A.D. with " BC" suffix, instead of the
// minus sign preferred by Go. // minus sign preferred by Go.
// Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on // Beware, "0000" in ISO is "1 BC", "-0001" is "2 BC" and so on
@@ -324,25 +421,26 @@ func formatTs(t time.Time) (b []byte) {
bc = true bc = true
} }
b = []byte(t.Format(time.RFC3339Nano)) b = []byte(t.Format(time.RFC3339Nano))
if bc {
b = append(b, " BC"...)
}
_, offset := t.Zone() _, offset := t.Zone()
offset = offset % 60 offset = offset % 60
if offset == 0 { if offset != 0 {
return b // RFC3339Nano already printed the minus sign
if offset < 0 {
offset = -offset
}
b = append(b, ':')
if offset < 10 {
b = append(b, '0')
}
b = strconv.AppendInt(b, int64(offset), 10)
} }
if offset < 0 { if bc {
offset = -offset b = append(b, " BC"...)
} }
return b
b = append(b, ':')
if offset < 10 {
b = append(b, '0')
}
return strconv.AppendInt(b, int64(offset), 10)
} }
// Parse a bytea value received from the server. Both "hex" and the legacy // Parse a bytea value received from the server. Both "hex" and the legacy
@@ -397,7 +495,10 @@ func parseBytea(s []byte) (result []byte) {
func encodeBytea(serverVersion int, v []byte) (result []byte) { func encodeBytea(serverVersion int, v []byte) (result []byte) {
if serverVersion >= 90000 { if serverVersion >= 90000 {
// Use the hex format if we know that the server supports it // Use the hex format if we know that the server supports it
result = []byte(fmt.Sprintf("\\x%x", v)) result = make([]byte, 2+hex.EncodedLen(len(v)))
result[0] = '\\'
result[1] = 'x'
hex.Encode(result[2:], v)
} else { } else {
// .. or resort to "escape" // .. or resort to "escape"
for _, b := range v { for _, b := range v {
+300 -14
View File
@@ -1,12 +1,13 @@
package pq package pq
import ( import (
"github.com/lib/pq/oid"
"bytes" "bytes"
"database/sql"
"fmt" "fmt"
"testing" "testing"
"time" "time"
"github.com/lib/pq/oid"
) )
func TestScanTimestamp(t *testing.T) { func TestScanTimestamp(t *testing.T) {
@@ -78,7 +79,11 @@ func tryParse(str string) (t time.Time, err error) {
return return
} }
}() }()
t = parseTs(nil, str) i := parseTs(nil, str)
t, ok := i.(time.Time)
if !ok {
err = fmt.Errorf("Not a time.Time type, got %#v", i)
}
return return
} }
@@ -132,8 +137,18 @@ var formatTimeTests = []struct {
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "2001-02-03T04:05:06.123456789Z"},
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "2001-02-03T04:05:06.123456789+02:00"},
{time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"}, {time.Date(2001, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "2001-02-03T04:05:06.123456789-06:00"},
{time.Date(1, time.January, 1, 0, 0, 0, 0, time.FixedZone("", 19*60+32)), "0001-01-01T00:00:00+00:19:32"},
{time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"}, {time.Date(2001, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "2001-02-03T04:05:06-07:30:09"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00"},
{time.Date(1, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 0)), "0001-02-03T04:05:06.123456789Z BC"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", 2*60*60)), "0001-02-03T04:05:06.123456789+02:00 BC"},
{time.Date(0, time.February, 3, 4, 5, 6, 123456789, time.FixedZone("", -6*60*60)), "0001-02-03T04:05:06.123456789-06:00 BC"},
{time.Date(1, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09"},
{time.Date(0, time.February, 3, 4, 5, 6, 0, time.FixedZone("", -(7*60*60+30*60+9))), "0001-02-03T04:05:06-07:30:09 BC"},
} }
func TestFormatTs(t *testing.T) { func TestFormatTs(t *testing.T) {
@@ -249,6 +264,131 @@ func TestTimestampWithOutTimezone(t *testing.T) {
test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033") test("2013-01-04T20:14:58.80033Z", "2013-01-04 20:14:58.80033")
} }
func TestInfinityTimestamp(t *testing.T) {
db := openTestConn(t)
defer db.Close()
var err error
var resultT time.Time
expectedError := fmt.Errorf(`sql: Scan error on column index 0: unsupported driver -> Scan pair: []uint8 -> *time.Time`)
type testCases []struct {
Query string
Param string
ExpectedErr error
ExpectedVal interface{}
}
tc := testCases{
{"SELECT $1::timestamp", "-infinity", expectedError, "-infinity"},
{"SELECT $1::timestamptz", "-infinity", expectedError, "-infinity"},
{"SELECT $1::timestamp", "infinity", expectedError, "infinity"},
{"SELECT $1::timestamptz", "infinity", expectedError, "infinity"},
}
// try to assert []byte to time.Time
for _, q := range tc {
err = db.QueryRow(q.Query, q.Param).Scan(&resultT)
if err.Error() != q.ExpectedErr.Error() {
t.Errorf("Scanning -/+infinity, expected error, %q, got %q", q.ExpectedErr, err)
}
}
// yield []byte
for _, q := range tc {
var resultI interface{}
err = db.QueryRow(q.Query, q.Param).Scan(&resultI)
if err != nil {
t.Errorf("Scanning -/+infinity, expected no error, got %q", err)
}
result, ok := resultI.([]byte)
if !ok {
t.Errorf("Scanning -/+infinity, expected []byte, got %#v", resultI)
}
if string(result) != q.ExpectedVal {
t.Errorf("Scanning -/+infinity, expected %q, got %q", q.ExpectedVal, result)
}
}
y1500 := time.Date(1500, time.January, 1, 0, 0, 0, 0, time.UTC)
y2500 := time.Date(2500, time.January, 1, 0, 0, 0, 0, time.UTC)
EnableInfinityTs(y1500, y2500)
err = db.QueryRow("SELECT $1::timestamp", "infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning infinity, expected no error, got %q", err)
}
if !resultT.Equal(y2500) {
t.Errorf("Scanning infinity, expected %q, got %q", y2500, resultT)
}
err = db.QueryRow("SELECT $1::timestamptz", "infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning infinity, expected no error, got %q", err)
}
if !resultT.Equal(y2500) {
t.Errorf("Scanning Infinity, expected time %q, got %q", y2500, resultT.String())
}
err = db.QueryRow("SELECT $1::timestamp", "-infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning -infinity, expected no error, got %q", err)
}
if !resultT.Equal(y1500) {
t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String())
}
err = db.QueryRow("SELECT $1::timestamptz", "-infinity").Scan(&resultT)
if err != nil {
t.Errorf("Scanning -infinity, expected no error, got %q", err)
}
if !resultT.Equal(y1500) {
t.Errorf("Scanning -infinity, expected time %q, got %q", y1500, resultT.String())
}
y_1500 := time.Date(-1500, time.January, 1, 0, 0, 0, 0, time.UTC)
y11500 := time.Date(11500, time.January, 1, 0, 0, 0, 0, time.UTC)
var s string
err = db.QueryRow("SELECT $1::timestamp::text", y_1500).Scan(&s)
if err != nil {
t.Errorf("Encoding -infinity, expected no error, got %q", err)
}
if s != "-infinity" {
t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s)
}
err = db.QueryRow("SELECT $1::timestamptz::text", y_1500).Scan(&s)
if err != nil {
t.Errorf("Encoding -infinity, expected no error, got %q", err)
}
if s != "-infinity" {
t.Errorf("Encoding -infinity, expected %q, got %q", "-infinity", s)
}
err = db.QueryRow("SELECT $1::timestamp::text", y11500).Scan(&s)
if err != nil {
t.Errorf("Encoding infinity, expected no error, got %q", err)
}
if s != "infinity" {
t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s)
}
err = db.QueryRow("SELECT $1::timestamptz::text", y11500).Scan(&s)
if err != nil {
t.Errorf("Encoding infinity, expected no error, got %q", err)
}
if s != "infinity" {
t.Errorf("Encoding infinity, expected %q, got %q", "infinity", s)
}
disableInfinityTs()
var panicErrorString string
func() {
defer func() {
panicErrorString, _ = recover().(string)
}()
EnableInfinityTs(y2500, y1500)
}()
if panicErrorString != infinityTsNegativeMustBeSmaller {
t.Errorf("Expected error, %q, got %q", infinityTsNegativeMustBeSmaller, panicErrorString)
}
}
func TestStringWithNul(t *testing.T) { func TestStringWithNul(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()
@@ -261,7 +401,7 @@ func TestStringWithNul(t *testing.T) {
} }
} }
func TestByteaToText(t *testing.T) { func TestByteSliceToText(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()
@@ -279,7 +419,7 @@ func TestByteaToText(t *testing.T) {
} }
} }
func TestTextToBytea(t *testing.T) { func TestStringToBytea(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()
@@ -297,6 +437,136 @@ func TestTextToBytea(t *testing.T) {
} }
} }
func TestTextByteSliceToUUID(t *testing.T) {
db := openTestConn(t)
defer db.Close()
b := []byte("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11")
row := db.QueryRow("SELECT $1::uuid", b)
var result string
err := row.Scan(&result)
if forceBinaryParameters() {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22P03" {
t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code)
}
} else {
if err != nil {
t.Fatal(err)
}
if result != string(b) {
t.Fatalf("expected %v but got %v", b, result)
}
}
}
func TestBinaryByteSlicetoUUID(t *testing.T) {
db := openTestConn(t)
defer db.Close()
b := []byte{'\xa0','\xee','\xbc','\x99',
'\x9c', '\x0b',
'\x4e', '\xf8',
'\xbb', '\x00', '\x6b',
'\xb9', '\xbd', '\x38', '\x0a', '\x11'}
row := db.QueryRow("SELECT $1::uuid", b)
var result string
err := row.Scan(&result)
if forceBinaryParameters() {
if err != nil {
t.Fatal(err)
}
if result != string("a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11") {
t.Fatalf("expected %v but got %v", b, result)
}
} else {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22021" {
t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code)
}
}
}
func TestStringToUUID(t *testing.T) {
db := openTestConn(t)
defer db.Close()
s := "a0eebc99-9c0b-4ef8-bb00-6bb9bd380a11"
row := db.QueryRow("SELECT $1::uuid", s)
var result string
err := row.Scan(&result)
if err != nil {
t.Fatal(err)
}
if result != s {
t.Fatalf("expected %v but got %v", s, result)
}
}
func TestTextByteSliceToInt(t *testing.T) {
db := openTestConn(t)
defer db.Close()
expected := 12345678
b := []byte(fmt.Sprintf("%d", expected))
row := db.QueryRow("SELECT $1::int", b)
var result int
err := row.Scan(&result)
if forceBinaryParameters() {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22P03" {
t.Fatalf("Expected to get invalid binary encoding error (22P03), got %s", pqErr.Code)
}
} else {
if err != nil {
t.Fatal(err)
}
if result != expected {
t.Fatalf("expected %v but got %v", expected, result)
}
}
}
func TestBinaryByteSliceToInt(t *testing.T) {
db := openTestConn(t)
defer db.Close()
expected := 12345678
b := []byte{'\x00', '\xbc', '\x61', '\x4e'}
row := db.QueryRow("SELECT $1::int", b)
var result int
err := row.Scan(&result)
if forceBinaryParameters() {
if err != nil {
t.Fatal(err)
}
if result != expected {
t.Fatalf("expected %v but got %v", expected, result)
}
} else {
pqErr := err.(*Error)
if pqErr == nil {
t.Errorf("Expected to get error")
} else if pqErr.Code != "22021" {
t.Fatalf("Expected to get invalid byte sequence for encoding error (22021), got %s", pqErr.Code)
}
}
}
func TestByteaOutputFormatEncoding(t *testing.T) { func TestByteaOutputFormatEncoding(t *testing.T) {
input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123") input := []byte("\\x\x00\x01\x02\xFF\xFEabcdefg0123")
want := []byte("\\x5c78000102fffe6162636465666730313233") want := []byte("\\x5c78000102fffe6162636465666730313233")
@@ -321,7 +591,7 @@ func TestByteaOutputFormats(t *testing.T) {
return return
} }
testByteaOutputFormat := func(f string) { testByteaOutputFormat := func(f string, usePrepared bool) {
expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08") expectedData := []byte("\x5c\x78\x00\xff\x61\x62\x63\x01\x08")
sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')" sqlQuery := "SELECT decode('5c7800ff6162630108', 'hex')"
@@ -338,8 +608,18 @@ func TestByteaOutputFormats(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// use Query; QueryRow would hide the actual error var rows *sql.Rows
rows, err := txn.Query(sqlQuery) var stmt *sql.Stmt
if usePrepared {
stmt, err = txn.Prepare(sqlQuery)
if err != nil {
t.Fatal(err)
}
rows, err = stmt.Query()
} else {
// use Query; QueryRow would hide the actual error
rows, err = txn.Query(sqlQuery)
}
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -357,13 +637,21 @@ func TestByteaOutputFormats(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if stmt != nil {
err = stmt.Close()
if err != nil {
t.Fatal(err)
}
}
if !bytes.Equal(data, expectedData) { if !bytes.Equal(data, expectedData) {
t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData) t.Errorf("unexpected bytea value %v for format %s; expected %v", data, f, expectedData)
} }
} }
testByteaOutputFormat("hex") testByteaOutputFormat("hex", false)
testByteaOutputFormat("escape") testByteaOutputFormat("escape", false)
testByteaOutputFormat("hex", true)
testByteaOutputFormat("escape", true)
} }
func TestAppendEncodedText(t *testing.T) { func TestAppendEncodedText(t *testing.T) {
@@ -371,15 +659,13 @@ func TestAppendEncodedText(t *testing.T) {
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, int64(10)) buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, int64(10))
buf = append(buf, '\t') buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, float32(42.0000000001))
buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, 42.0000000001) buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, 42.0000000001)
buf = append(buf, '\t') buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, "hello\tworld") buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, "hello\tworld")
buf = append(buf, '\t') buf = append(buf, '\t')
buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255}) buf = appendEncodedText(&parameterStatus{serverVersion: 90000}, buf, []byte{0, 128, 255})
if string(buf) != "10\t42\t42.0000000001\thello\\tworld\t\\\\x0080ff" { if string(buf) != "10\t42.0000000001\thello\\tworld\t\\\\x0080ff" {
t.Fatal(string(buf)) t.Fatal(string(buf))
} }
} }
+13
View File
@@ -459,6 +459,19 @@ func errorf(s string, args ...interface{}) {
panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...)))
} }
func errRecoverNoErrBadConn(err *error) {
e := recover()
if e == nil {
// Do nothing
return
}
var ok bool
*err, ok = e.(error)
if !ok {
*err = fmt.Errorf("pq: unexpected error: %#v", e)
}
}
func (c *conn) errRecover(err *error) { func (c *conn) errRecover(err *error) {
e := recover() e := recover()
switch v := e.(type) { switch v := e.(type) {
+2 -1
View File
@@ -2,9 +2,10 @@ package hstore
import ( import (
"database/sql" "database/sql"
_ "github.com/lib/pq"
"os" "os"
"testing" "testing"
_ "github.com/lib/pq"
) )
type Fatalistic interface { type Fatalistic interface {
+2 -2
View File
@@ -18,11 +18,11 @@ mechanism to avoid polling the database while waiting for more work to arrive.
package main package main
import ( import (
"github.com/lib/pq"
"database/sql" "database/sql"
"fmt" "fmt"
"time" "time"
"github.com/lib/pq"
) )
func doWork(db *sql.DB, work int64) { func doWork(db *sql.DB, work int64) {
+30 -16
View File
@@ -6,7 +6,6 @@ package pq
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -87,12 +86,16 @@ func NewListenerConn(name string, notificationChan chan<- *Notification) (*Liste
// Returns an error if an unrecoverable error has occurred and the ListenerConn // Returns an error if an unrecoverable error has occurred and the ListenerConn
// should be abandoned. // should be abandoned.
func (l *ListenerConn) acquireSenderLock() error { func (l *ListenerConn) acquireSenderLock() error {
l.connectionLock.Lock() // we must acquire senderLock first to avoid deadlocks; see ExecSimpleQuery
defer l.connectionLock.Unlock()
if l.err != nil {
return l.err
}
l.senderLock.Lock() l.senderLock.Lock()
l.connectionLock.Lock()
err := l.err
l.connectionLock.Unlock()
if err != nil {
l.senderLock.Unlock()
return err
}
return nil return nil
} }
@@ -125,7 +128,7 @@ func (l *ListenerConn) setState(newState int32) bool {
// away or should be discarded because we couldn't agree on the state with the // away or should be discarded because we couldn't agree on the state with the
// server backend. // server backend.
func (l *ListenerConn) listenerConnLoop() (err error) { func (l *ListenerConn) listenerConnLoop() (err error) {
defer l.cn.errRecover(&err) defer errRecoverNoErrBadConn(&err)
r := &readBuf{} r := &readBuf{}
for { for {
@@ -140,6 +143,9 @@ func (l *ListenerConn) listenerConnLoop() (err error) {
// about the scratch buffer being overwritten. // about the scratch buffer being overwritten.
l.notificationChan <- recvNotification(r) l.notificationChan <- recvNotification(r)
case 'T', 'D':
// only used by tests; ignore
case 'E': case 'E':
// We might receive an ErrorResponse even when not in a query; it // We might receive an ErrorResponse even when not in a query; it
// is expected that the server will close the connection after // is expected that the server will close the connection after
@@ -238,7 +244,7 @@ func (l *ListenerConn) Ping() error {
// The caller must be holding senderLock (see acquireSenderLock and // The caller must be holding senderLock (see acquireSenderLock and
// releaseSenderLock). // releaseSenderLock).
func (l *ListenerConn) sendSimpleQuery(q string) (err error) { func (l *ListenerConn) sendSimpleQuery(q string) (err error) {
defer l.cn.errRecover(&err) defer errRecoverNoErrBadConn(&err)
// must set connection state before sending the query // must set connection state before sending the query
if !l.setState(connStateExpectResponse) { if !l.setState(connStateExpectResponse) {
@@ -247,8 +253,10 @@ func (l *ListenerConn) sendSimpleQuery(q string) (err error) {
// Can't use l.cn.writeBuf here because it uses the scratch buffer which // Can't use l.cn.writeBuf here because it uses the scratch buffer which
// might get overwritten by listenerConnLoop. // might get overwritten by listenerConnLoop.
data := writeBuf([]byte("Q\x00\x00\x00\x00")) b := &writeBuf{
b := &data buf: []byte("Q\x00\x00\x00\x00"),
pos: 1,
}
b.string(q) b.string(q)
l.cn.send(b) l.cn.send(b)
@@ -277,13 +285,13 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
// We can't know what state the protocol is in, so we need to abandon // We can't know what state the protocol is in, so we need to abandon
// this connection. // this connection.
l.connectionLock.Lock() l.connectionLock.Lock()
defer l.connectionLock.Unlock()
// Set the error pointer if it hasn't been set already; see // Set the error pointer if it hasn't been set already; see
// listenerConnMain. // listenerConnMain.
if l.err == nil { if l.err == nil {
l.err = err l.err = err
} }
l.cn.Close() l.connectionLock.Unlock()
l.cn.c.Close()
return false, err return false, err
} }
@@ -292,8 +300,11 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
m, ok := <-l.replyChan m, ok := <-l.replyChan
if !ok { if !ok {
// We lost the connection to server, don't bother waiting for a // We lost the connection to server, don't bother waiting for a
// a response. // a response. err should have been set already.
return false, io.EOF l.connectionLock.Lock()
err := l.err
l.connectionLock.Unlock()
return false, err
} }
switch m.typ { switch m.typ {
case 'Z': case 'Z':
@@ -320,12 +331,15 @@ func (l *ListenerConn) ExecSimpleQuery(q string) (executed bool, err error) {
func (l *ListenerConn) Close() error { func (l *ListenerConn) Close() error {
l.connectionLock.Lock() l.connectionLock.Lock()
defer l.connectionLock.Unlock()
if l.err != nil { if l.err != nil {
l.connectionLock.Unlock()
return errListenerConnClosed return errListenerConnClosed
} }
l.err = errListenerConnClosed l.err = errListenerConnClosed
return l.cn.Close() l.connectionLock.Unlock()
// We can't send anything on the connection without holding senderLock.
// Simply close the net.Conn to wake up everyone operating on it.
return l.cn.c.Close()
} }
// Err() returns the reason the connection was closed. It is not safe to call // Err() returns the reason the connection was closed. It is not safe to call
+73 -1
View File
@@ -5,6 +5,9 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"runtime"
"sync"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@@ -43,7 +46,7 @@ func expectEvent(t *testing.T, eventch <-chan ListenerEventType, et ListenerEven
} }
return nil return nil
case <-time.After(1500 * time.Millisecond): case <-time.After(1500 * time.Millisecond):
return fmt.Errorf("timeout") panic("expectEvent timeout")
} }
} }
@@ -210,6 +213,75 @@ func TestConnPing(t *testing.T) {
} }
} }
// Test for deadlock where a query fails while another one is queued
func TestConnExecDeadlock(t *testing.T) {
l, _ := newTestListenerConn(t)
defer l.Close()
var wg sync.WaitGroup
wg.Add(2)
go func() {
l.ExecSimpleQuery("SELECT pg_sleep(60)")
wg.Done()
}()
runtime.Gosched()
go func() {
l.ExecSimpleQuery("SELECT 1")
wg.Done()
}()
// give the two goroutines some time to get into position
runtime.Gosched()
// calls Close on the net.Conn; equivalent to a network failure
l.Close()
var done int32 = 0
go func() {
time.Sleep(10 * time.Second)
if atomic.LoadInt32(&done) != 1 {
panic("timed out")
}
}()
wg.Wait()
atomic.StoreInt32(&done, 1)
}
// Test for ListenerConn being closed while a slow query is executing
func TestListenerConnCloseWhileQueryIsExecuting(t *testing.T) {
l, _ := newTestListenerConn(t)
defer l.Close()
var wg sync.WaitGroup
wg.Add(1)
go func() {
sent, err := l.ExecSimpleQuery("SELECT pg_sleep(60)")
if sent {
panic("expected sent=false")
}
// could be any of a number of errors
if err == nil {
panic("expected error")
}
wg.Done()
}()
// give the above goroutine some time to get into position
runtime.Gosched()
err := l.Close()
if err != nil {
t.Fatal(err)
}
var done int32 = 0
go func() {
time.Sleep(10 * time.Second)
if atomic.LoadInt32(&done) != 1 {
panic("timed out")
}
}()
wg.Wait()
atomic.StoreInt32(&done, 1)
}
func TestNotifyExtra(t *testing.T) { func TestNotifyExtra(t *testing.T) {
db := openTestConn(t) db := openTestConn(t)
defer db.Close() defer db.Close()
+1 -1
View File
@@ -5,12 +5,12 @@
package main package main
import ( import (
"database/sql"
"fmt" "fmt"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"database/sql"
_ "github.com/lib/pq" _ "github.com/lib/pq"
) )
+1 -1
View File
@@ -34,7 +34,7 @@ func ParseURL(url string) (string, error) {
return "", err return "", err
} }
if u.Scheme != "postgres" { if u.Scheme != "postgres" && u.Scheme != "postgresql" {
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme)
} }
+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') BINDATA_IGNORE = $(shell git ls-files -io --exclude-standard $< | sed 's/^/-ignore=/;s/[.]/[.]/g')
usage: usage:
@@ -10,17 +14,19 @@ usage:
@echo "make build : Generate production build for current OS" @echo "make build : Generate production build for current OS"
@echo "make bootstrap : Install cross-compilation toolchain" @echo "make bootstrap : Install cross-compilation toolchain"
@echo "make release : Generate binaries for all supported OSes" @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 clean : Remove all build files and reset assets"
@echo "make assets : Generate production assets file" @echo "make assets : Generate production assets file"
@echo "make dev-assets : Generate development assets file" @echo "make dev-assets : Generate development assets file"
@echo "make docker : Build docker image" @echo "make docker : Build docker image"
@echo "make docker-release : Build and tag docker image"
@echo "" @echo ""
test: test:
godep go test godep go test -cover ./...
assets: static/ 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: dev-assets:
@$(MAKE) --no-print-directory assets BINDATA_OPTS="-debug" @$(MAKE) --no-print-directory assets BINDATA_OPTS="-debug"
@@ -34,22 +40,33 @@ build: assets
@echo "You can now execute ./pgweb" @echo "You can now execute ./pgweb"
release: assets 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: bootstrap:
gox -build-toolchain gox -build-toolchain
setup: setup:
go get github.com/tools/godep go get github.com/tools/godep
go get golang.org/x/tools/cmd/cover
godep get github.com/mitchellh/gox godep get github.com/mitchellh/gox
godep get github.com/jteeuwen/go-bindata/... godep get github.com/jteeuwen/go-bindata/...
godep restore godep restore
clean: clean:
rm -f ./pgweb rm -f ./pgweb
rm -f ./bin/* rm -rf ./bin/*
rm -f bindata.go rm -f bindata.go
make assets make assets
docker: 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. 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 ## Overview
@@ -12,8 +14,7 @@ is to utilize ability of the compiler to produce zero-dependency binaries for
multiple platforms. Pgweb was created as an attempt to build very simple and portable multiple platforms. Pgweb was created as an attempt to build very simple and portable
application to work with local or remote PostgreSQL databases. application to work with local or remote PostgreSQL databases.
<img src="screenshots/browse.png" width="345px" /> [See application screenshots](SCREENS.md)
<img src="screenshots/query.png" width="345px" />
## Features ## Features
@@ -21,10 +22,10 @@ application to work with local or remote PostgreSQL databases.
- Zero dependencies - Zero dependencies
- Simple installation (distributes as a single binary) - Simple installation (distributes as a single binary)
- Connect to local or remote servers - Connect to local or remote servers
- Browse tables and table data - Browse tables and table rows
- Get table details: structure, size, indeces, row count - Get table details: structure, size, indeces, row count
- Run / analyze custom queries - Run and analyze custom SQL queries
- Export query results to CSV - Export table rows and query results as CSV
- Query history - Query history
- Server bookmarks - Server bookmarks
@@ -63,23 +64,25 @@ pgweb --url postgres://user:password@host:port/database?sslmode=[mode]
## Testing ## Testing
Run tests: Before running tests, make sure you have PostgreSQL server running on `localhost:5432`
interface. Also, you must have `postgres` user that could create new databases
in your local environment. Pgweb server should not be running at the same time.
Execute test suite:
``` ```
make test make test
``` ```
Continuous integration status:
- Travis (Linux) - [![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 ## Contribute
- Fork repository - Fork this repository
- Create a feature or bugfix branch - Create a new feature branch for a new functionality or bugfix
- Open a new pull request - Commit your changes
- Use [github issues](https://github.com/sosedoff/pgweb/issues) for any questions - Execute test suite
- Push your code and open a new pull request
- Use [issues](https://github.com/sosedoff/pgweb/issues) for any questions
- Check [wiki](https://github.com/sosedoff/pgweb/wiki) for extra documentation
## Contact ## Contact
+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": [ "keywords": [
"postgres", "postgres",
"database", "database",
"golang",
"frontend" "frontend"
], ],
"addons": [ "addons": [
"heroku-postgresql" "heroku-postgresql"
], ],
"env": { "env": {
"BUILDPACK_URL": "https://github.com/kr/heroku-buildpack-go",
"AUTH_USER": { "AUTH_USER": {
"description": "A username to log into pgweb.", "description": "A username to log into pgweb.",
"value": "admin" "value": "admin"
}, },
"AUTH_PASS": { "AUTH_PASS": {
"description": "The login password. Please change this.", "description": "The login password. Please change this.",
"value": "pgweb" "generator": "secret"
} }
} }
} }
+4 -2
View File
@@ -1,5 +1,7 @@
clone_folder: c:\go\src\github.com\sosedoff\pgweb
environment: environment:
GOPATH: c:\projects GOPATH: c:\go
services: services:
- postgresql - postgresql
@@ -18,6 +20,6 @@ build_script:
- godep go build - godep go build
test_script: test_script:
- godep go test - godep go test -cover ./...
deploy: off 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" "os/signal"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/jessevdk/go-flags" "github.com/sosedoff/pgweb/pkg/api"
_ "github.com/lib/pq" "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" var options command.Options
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
func exitWithMessage(message string) { func exitWithMessage(message string) {
fmt.Println("Error:", message) fmt.Println("Error:", message)
@@ -39,49 +22,57 @@ func exitWithMessage(message string) {
} }
func initClient() { func initClient() {
if connectionSettingsBlank(options) { if connection.IsBlank(command.Opts) {
return return
} }
client, err := NewClient() cl, err := client.New()
if err != nil { if err != nil {
exitWithMessage(err.Error()) exitWithMessage(err.Error())
} }
if options.Debug { if command.Opts.Debug {
fmt.Println("Server connection string:", client.connectionString) fmt.Println("Server connection string:", cl.ConnectionString)
} }
fmt.Println("Connecting to server...") fmt.Println("Connecting to server...")
err = client.Test() err = cl.Test()
if err != nil { if err != nil {
exitWithMessage(err.Error()) exitWithMessage(err.Error())
} }
fmt.Println("Checking tables...") fmt.Println("Checking tables...")
_, err = client.Tables() _, err = cl.Tables()
if err != nil { if err != nil {
exitWithMessage(err.Error()) exitWithMessage(err.Error())
} }
dbClient = client api.DbClient = cl
} }
func initOptions() { func initOptions() {
_, err := flags.ParseArgs(&options, os.Args) err := command.ParseOptions()
if err != nil { if err != nil {
os.Exit(1) os.Exit(1)
} }
if options.Url == "" { options = command.Opts
options.Url = os.Getenv("DATABASE_URL")
}
if options.Version { if options.Version {
fmt.Printf("pgweb v%s\n", VERSION) printVersion()
os.Exit(0) 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() { func startServer() {
@@ -93,7 +84,7 @@ func startServer() {
router.Use(gin.BasicAuth(auth)) router.Use(gin.BasicAuth(auth))
} }
setupRoutes(router) api.SetupRoutes(router)
fmt.Println("Starting server...") fmt.Println("Starting server...")
go func() { go func() {
@@ -129,20 +120,19 @@ func openPage() {
func main() { func main() {
initOptions() initOptions()
fmt.Println("Pgweb version", VERSION)
initClient() initClient()
if dbClient != nil { if api.DbClient != nil {
defer dbClient.db.Close() defer api.DbClient.Close()
} }
if !options.Debug { if !options.Debug {
gin.SetMode("release") gin.SetMode("release")
} }
// Print memory usage every 30 seconds with debug flag
if options.Debug { if options.Debug {
startRuntimeProfiler() util.StartProfiler()
} }
startServer() 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 ( import (
"testing" "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 ( import (
"fmt" "fmt"
@@ -22,19 +22,13 @@ type Bookmark struct {
func readServerConfig(path string) (Bookmark, error) { func readServerConfig(path string) (Bookmark, error) {
bookmark := Bookmark{} bookmark := Bookmark{}
buff, err := ioutil.ReadFile(path)
buff, err := ioutil.ReadFile(path)
if err != nil { if err != nil {
fmt.Println(err)
return bookmark, err return bookmark, err
} }
_, err = toml.Decode(string(buff), &bookmark) _, err = toml.Decode(string(buff), &bookmark)
if err != nil {
fmt.Println(err)
}
return bookmark, err return bookmark, err
} }
@@ -43,13 +37,12 @@ func fileBasename(path string) string {
return strings.Replace(filename, filepath.Ext(path), "", 1) return strings.Replace(filename, filepath.Ext(path), "", 1)
} }
func bookmarksPath() string { func Path() string {
path, _ := homedir.Dir() path, _ := homedir.Dir()
return fmt.Sprintf("%s/.pgweb/bookmarks", path) return fmt.Sprintf("%s/.pgweb/bookmarks", path)
} }
func readAllBookmarks() (map[string]Bookmark, error) { func ReadAll(path string) (map[string]Bookmark, error) {
path := bookmarksPath()
results := map[string]Bookmark{} results := map[string]Bookmark{}
files, err := ioutil.ReadDir(path) files, err := ioutil.ReadDir(path)
@@ -67,7 +60,7 @@ func readAllBookmarks() (map[string]Bookmark, error) {
config, err := readServerConfig(fullPath) config, err := readServerConfig(fullPath)
if err != nil { if err != nil {
fmt.Println(err) fmt.Printf("%s parse error: %s\n", fullPath, err)
continue 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 ( import (
"bytes" "bytes"
@@ -6,13 +6,19 @@ import (
"fmt" "fmt"
"reflect" "reflect"
_ "github.com/lib/pq"
"github.com/jmoiron/sqlx" "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 { type Client struct {
db *sqlx.DB db *sqlx.DB
history []HistoryRecord History []history.Record
connectionString string ConnectionString string
} }
type Row []interface{} type Row []interface{}
@@ -29,10 +35,10 @@ type RowsOptions struct {
SortOrder string // Sort direction (ASC, DESC) SortOrder string // Sort direction (ASC, DESC)
} }
func NewClient() (*Client, error) { func New() (*Client, error) {
str, err := buildConnectionString(options) str, err := connection.BuildString(command.Opts)
if options.Debug && str != "" { if command.Opts.Debug && str != "" {
fmt.Println("Creating a new client for:", str) fmt.Println("Creating a new client for:", str)
} }
@@ -48,28 +54,27 @@ func NewClient() (*Client, error) {
client := Client{ client := Client{
db: db, db: db,
connectionString: str, ConnectionString: str,
history: NewHistory(), History: history.New(),
} }
return &client, nil return &client, nil
} }
func NewClientFromUrl(url string) (*Client, error) { func NewFromUrl(url string) (*Client, error) {
if options.Debug { if command.Opts.Debug {
fmt.Println("Creating a new client for:", url) fmt.Println("Creating a new client for:", url)
} }
db, err := sqlx.Open("postgres", url) db, err := sqlx.Open("postgres", url)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client := Client{ client := Client{
db: db, db: db,
connectionString: url, ConnectionString: url,
history: NewHistory(), History: history.New(),
} }
return &client, nil return &client, nil
@@ -80,19 +85,23 @@ func (client *Client) Test() error {
} }
func (client *Client) Info() (*Result, error) { func (client *Client) Info() (*Result, error) {
return client.query(PG_INFO) return client.query(statements.PG_INFO)
} }
func (client *Client) Databases() ([]string, error) { 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) { 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) { 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) { 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) { 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) { 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 { if err != nil {
return nil, err return nil, err
@@ -127,12 +136,17 @@ func (client *Client) TableIndexes(table string) (*Result, error) {
return res, err 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) { func (client *Client) Query(query string) (*Result, error) {
res, err := client.query(query) res, err := client.query(query)
// Save history records only if query did not fail // Save history records only if query did not fail
if err == nil { if err == nil {
client.history = append(client.history, NewHistoryRecord(query)) client.History = append(client.History, history.NewRecord(query))
} }
return res, err return res, err
@@ -222,6 +236,14 @@ func (res *Result) CSV() []byte {
return buff.Bytes() 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 // Fetch all rows as strings for a single column
func (client *Client) fetchRows(q string) ([]string, error) { func (client *Client) fetchRows(q string) ([]string, error) {
res, err := client.query(q) res, err := client.query(q)
+24 -10
View File
@@ -1,4 +1,4 @@
package main package client
import ( import (
"fmt" "fmt"
@@ -10,8 +10,10 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var testClient *Client var (
var testCommands map[string]string testClient *Client
testCommands map[string]string
)
func setupCommands() { func setupCommands() {
testCommands = map[string]string{ testCommands = map[string]string{
@@ -40,7 +42,7 @@ func setup() {
os.Exit(1) 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 { if err != nil {
fmt.Println("Database import failed:", string(out)) fmt.Println("Database import failed:", string(out))
@@ -50,7 +52,7 @@ func setup() {
} }
func setupClient() { func setupClient() {
testClient, _ = NewClientFromUrl("postgres://postgres@localhost/booktown?sslmode=disable") testClient, _ = NewFromUrl("postgres://postgres@localhost/booktown?sslmode=disable")
} }
func teardownClient() { func teardownClient() {
@@ -69,14 +71,26 @@ func teardown() {
func test_NewClientFromUrl(t *testing.T) { func test_NewClientFromUrl(t *testing.T) {
url := "postgres://postgres@localhost/booktown?sslmode=disable" url := "postgres://postgres@localhost/booktown?sslmode=disable"
client, err := NewClientFromUrl(url) client, err := NewFromUrl(url)
if err != nil { if err != nil {
defer client.db.Close() defer client.Close()
} }
assert.Equal(t, nil, err) 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) { func test_Test(t *testing.T) {
@@ -209,7 +223,7 @@ func test_ResultCsv(t *testing.T) {
func test_History(t *testing.T) { func test_History(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books") _, 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, nil, err)
assert.Equal(t, "SELECT * FROM books", query) assert.Equal(t, "SELECT * FROM books", query)
@@ -217,7 +231,7 @@ func test_History(t *testing.T) {
func test_HistoryError(t *testing.T) { func test_HistoryError(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books123") _, 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, nil, err)
assert.NotEqual(t, "SELECT * FROM books123", query) 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 ( import (
"errors" "errors"
@@ -6,6 +6,8 @@ import (
"os" "os"
"os/user" "os/user"
"strings" "strings"
"github.com/sosedoff/pgweb/pkg/command"
) )
func currentUser() (string, error) { func currentUser() (string, error) {
@@ -22,11 +24,11 @@ func currentUser() (string, error) {
return "", errors.New("Unable to detect OS user") return "", errors.New("Unable to detect OS user")
} }
func formatConnectionUrl(opts Options) (string, error) { func FormatUrl(opts command.Options) (string, error) {
url := opts.Url url := opts.Url
// Make sure to only accept urls in a standard format // Make sure to only accept urls in a standard format
if !strings.Contains(url, "postgres://") { if !strings.HasPrefix(url, "postgres://") && !strings.HasPrefix(url, "postgresql://") {
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode") return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
} }
@@ -50,13 +52,13 @@ func formatConnectionUrl(opts Options) (string, error) {
return url, nil return url, nil
} }
func connectionSettingsBlank(opts Options) bool { func IsBlank(opts command.Options) bool {
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == "" 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 != "" { if opts.Url != "" {
return formatConnectionUrl(opts) return FormatUrl(opts)
} }
// Try to detect user from current OS user // Try to detect user from current OS user
@@ -1,23 +1,24 @@
package main package connection
import ( import (
"fmt" "fmt"
"os/user" "os/user"
"testing" "testing"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_Invalid_Url(t *testing.T) { func Test_Invalid_Url(t *testing.T) {
opts := Options{} opts := command.Options{}
examples := []string{ examples := []string{
"postgresql://foobar", "postgre://foobar",
"foobar", "foobar",
} }
for _, val := range examples { for _, val := range examples {
opts.Url = val opts.Url = val
str, err := buildConnectionString(opts) str, err := BuildString(opts)
assert.Equal(t, "", str) assert.Equal(t, "", str)
assert.Error(t, err) assert.Error(t, err)
@@ -27,14 +28,14 @@ func Test_Invalid_Url(t *testing.T) {
func Test_Valid_Url(t *testing.T) { func Test_Valid_Url(t *testing.T) {
url := "postgres://myhost/database" 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, nil, err)
assert.Equal(t, url, str) assert.Equal(t, url, str)
} }
func Test_Url_And_Ssl_Flag(t *testing.T) { func Test_Url_And_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{ str, err := BuildString(command.Options{
Url: "postgres://myhost/database", Url: "postgres://myhost/database",
Ssl: "disable", 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) { func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{ str, err := BuildString(command.Options{
Url: "postgres://localhost/database", Url: "postgres://localhost/database",
}) })
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str) 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", 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) { func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{ str, err := BuildString(command.Options{
Url: "postgres://localhost/database", Url: "postgres://localhost/database",
Ssl: "require", Ssl: "require",
}) })
@@ -68,7 +69,7 @@ func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str) 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", Url: "postgres://127.0.0.1/database",
Ssl: "require", 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) { 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", Url: "postgres://localhost/database?sslmode=require",
}) })
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str) 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", 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) { func Test_Flag_Args(t *testing.T) {
str, err := buildConnectionString(Options{ str, err := BuildString(command.Options{
Host: "host", Host: "host",
Port: 5432, Port: 5432,
User: "user", User: "user",
@@ -107,7 +108,7 @@ func Test_Flag_Args(t *testing.T) {
} }
func Test_Localhost(t *testing.T) { func Test_Localhost(t *testing.T) {
opts := Options{ opts := command.Options{
Host: "localhost", Host: "localhost",
Port: 5432, Port: 5432,
User: "user", User: "user",
@@ -115,18 +116,18 @@ func Test_Localhost(t *testing.T) {
DbName: "db", DbName: "db",
} }
str, err := buildConnectionString(opts) str, err := BuildString(opts)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str) assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
opts.Host = "127.0.0.1" opts.Host = "127.0.0.1"
str, err = buildConnectionString(opts) str, err = BuildString(opts)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str) assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
} }
func Test_Localhost_And_Ssl(t *testing.T) { func Test_Localhost_And_Ssl(t *testing.T) {
opts := Options{ opts := command.Options{
Host: "localhost", Host: "localhost",
Port: 5432, Port: 5432,
User: "user", User: "user",
@@ -135,31 +136,31 @@ func Test_Localhost_And_Ssl(t *testing.T) {
Ssl: "require", Ssl: "require",
} }
str, err := buildConnectionString(opts) str, err := BuildString(opts)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str) assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
} }
func Test_No_User(t *testing.T) { 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() u, _ := user.Current()
str, err := buildConnectionString(opts) str, err := BuildString(opts)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str) assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str)
} }
func Test_Port(t *testing.T) { func Test_Port(t *testing.T) {
opts := Options{Host: "host", User: "user", Port: 5000, DbName: "db"} opts := command.Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
str, err := buildConnectionString(opts) str, err := BuildString(opts)
assert.Equal(t, nil, err) assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user@host:5000/db", str) assert.Equal(t, "postgres://user@host:5000/db", str)
} }
func Test_Blank(t *testing.T) { func Test_Blank(t *testing.T) {
assert.Equal(t, true, connectionSettingsBlank(Options{})) assert.Equal(t, true, IsBlank(command.Options{}))
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user"})) assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user"}))
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user", DbName: "db"})) assert.Equal(t, false, IsBlank(command.Options{Host: "host", User: "user", DbName: "db"}))
assert.Equal(t, false, connectionSettingsBlank(Options{Url: "url"})) 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 ( const (
PG_DATABASES = `SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY datname ASC` 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 PG_INFO = `SELECT
session_user session_user
, current_user , current_user
@@ -28,4 +30,18 @@ FROM information_schema.columns
WHERE table_name = $1` WHERE table_name = $1`
PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name` PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name`
PG_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; padding: 0px;
height: 50px; height: 50px;
display: block; display: block;
width: 550px;
} }
#nav ul li { #nav ul li {
@@ -74,6 +73,8 @@
left: 0px; left: 0px;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
border-right: 1px solid #d4d4d4;
box-sizing: border-box;
} }
#sidebar .title { #sidebar .title {
@@ -84,8 +85,9 @@
height: 33px; height: 33px;
padding: 0px 8px; padding: 0px 8px;
padding-left: 12px; padding-left: 12px;
color: #fff; color: #000;
background: #31373d; font-weight: bold;
background: #f6f6f6;
} }
#sidebar div.tables-list { #sidebar div.tables-list {
@@ -95,7 +97,7 @@
left: 0px; left: 0px;
bottom: 130px; bottom: 130px;
overflow: auto; overflow: auto;
background: #31373d; background: #f6f6f6;
} }
#sidebar div.tables-list .wrap { #sidebar div.tables-list .wrap {
@@ -115,17 +117,17 @@
float: right; float: right;
margin-right: 4px; margin-right: 4px;
cursor: pointer; cursor: pointer;
color: #95a7b7; color: #aaa;
display: none; display: none;
} }
#sidebar div.tables-list .title span.refresh:hover { #sidebar div.tables-list .title span.refresh:hover {
color: #fff; color: #555;
} }
#sidebar div.tables-list #tables { #sidebar div.tables-list #tables {
padding: 33px 0 0; padding: 33px 0 0;
font-size: 13px; font-size: 12px;
} }
#sidebar div.table-information { #sidebar div.table-information {
@@ -134,7 +136,10 @@
left: 0px; left: 0px;
height: 130px; height: 130px;
bottom: 0px; 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 { #sidebar div.table-information .wrap {
@@ -143,7 +148,7 @@
} }
#sidebar div.table-information .title { #sidebar div.table-information .title {
background: #272c30; background: #f6f6f6;
} }
#sidebar div.table-information ul { #sidebar div.table-information ul {
@@ -157,11 +162,11 @@
height: 24px; height: 24px;
padding: 0 8px 0 12px; padding: 0 8px 0 12px;
cursor: default; cursor: default;
color: #637D94 !important; color: #7F7E7F !important;
} }
#sidebar div.table-information ul li span { #sidebar div.table-information ul li span {
color: #95a7b7; color: #4F4F4F;
min-width: 0; min-width: 0;
padding-left: 0px; padding-left: 0px;
} }
@@ -178,19 +183,18 @@
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
cursor: pointer; cursor: pointer;
color: #95a7b7 !important; color: #4F4E4F !important;
white-space: nowrap; white-space: nowrap;
} }
#sidebar li.selected { #sidebar li.selected {
color: #fff !important;
font-weight: bold; font-weight: bold;
background: #272c30; background: #e6e6e6;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
#sidebar ul li:hover span { #sidebar ul li:hover span {
background: #272c30; background: #f1f1f1;
} }
#sidebar ul li i { #sidebar ul li i {
@@ -296,7 +300,7 @@
#results tr:nth-child(even) > td { #results tr:nth-child(even) > td {
border: none; border: none;
background: #f8f8f8; background: #fafafa;
} }
#results tr:nth-child(odd) > td { #results tr:nth-child(odd) > td {
@@ -315,10 +319,23 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
#results th.active {
background: #e3e3e3;
}
#results[data-mode="browse"] th:hover {
cursor: pointer;
background: #fafafa;
}
#results tbody tr:hover td { #results tbody tr:hover td {
background: #ffe; background: #ffe;
} }
#results tbody tr {
border-bottom: 1px solid #f2f2f2;
}
#results tr.selected td { #results tr.selected td {
background: #3874d7 !important; background: #3874d7 !important;
color: #fff !important; color: #fff !important;
@@ -370,7 +387,7 @@
#connection_window { #connection_window {
z-index: 3; z-index: 3;
position: fixed; position: fixed;
background: #ebeef0; background: #fff;
top: 0px; top: 0px;
left: 0px; left: 0px;
bottom: 0px; bottom: 0px;
@@ -410,12 +427,12 @@
text-align: center; text-align: center;
text-shadow: 0px 1px 0px #fff; text-shadow: 0px 1px 0px #fff;
margin-bottom: 25px; margin-bottom: 25px;
color: #95A7B7; color: #999;
font-weight: normal; font-weight: normal;
} }
.connection-settings form { .connection-settings form {
background: #fff; background: #f6f6f6;
padding: 25px; padding: 25px;
border-radius: 4px; border-radius: 4px;
-moz-border-radius: 4px; -moz-border-radius: 4px;
@@ -432,4 +449,5 @@
.connection-ssh-group { .connection-ssh-group {
display: none; 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/jquery.js"></script>
<script type="text/javascript" src="/static/js/ace.js"></script> <script type="text/javascript" src="/static/js/ace.js"></script>
<script type="text/javascript" src="/static/js/ace-pgsql.js"></script> <script type="text/javascript" src="/static/js/ace-pgsql.js"></script>
<script type="text/javascript" src="/static/js/bootstrap-contextmenu.js"></script>
<script type="text/javascript" src="/static/js/app.js"></script> <script type="text/javascript" src="/static/js/app.js"></script>
</head> </head>
<body> <body>
<div id="main"> <div id="main">
<div id="nav"> <div id="nav">
<ul> <ul>
<li id="table_content">Content</li> <li id="table_content">Rows</li>
<li id="table_structure">Structure</li> <li id="table_structure">Structure</li>
<li id="table_indexes">Indexes</li> <li id="table_indexes">Indexes</li>
<li id="table_query" class="selected">SQL Query</li> <li id="table_query" class="selected">SQL Query</li>
<li id="table_history">History</li> <li id="table_history">History</li>
<li id="table_activity">Activity</li>
<li id="table_connection">Connection</li> <li id="table_connection">Connection</li>
</ul> </ul>
@@ -80,7 +82,6 @@
<div class="btn-group btn-group-sm connection-group-switch"> <div class="btn-group btn-group-sm connection-group-switch">
<button type="button" data="scheme" class="btn btn-default" id="connection_scheme">Scheme</button> <button type="button" data="scheme" class="btn btn-default" id="connection_scheme">Scheme</button>
<button type="button" data="standard" class="btn btn-default active" id="connection_standard">Standard</button> <button type="button" data="standard" class="btn btn-default active" id="connection_standard">Standard</button>
<!--<button type="button" data="ssh" class="btn btn-default">SSH</button>-->
</div> </div>
</div> </div>
@@ -121,7 +122,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-sm-3 control-label">Password</label> <label class="col-sm-3 control-label">Password</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" id="pg_password" class="form-control" /> <input type="password" id="pg_password" class="form-control" />
</div> </div>
</div> </div>
@@ -192,5 +193,13 @@
</form> </form>
</div> </div>
</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> </body>
</html> </html>
+102 -13
View File
@@ -4,7 +4,7 @@ var bookmarks = {};
function apiCall(method, path, params, cb) { function apiCall(method, path, params, cb) {
$.ajax({ $.ajax({
url: "/api" + path, url: "/api" + path,
method: method, method: method,
cache: false, cache: false,
data: params, data: params,
@@ -37,7 +37,7 @@ function loadTables() {
getTables(function(data) { getTables(function(data) {
data.forEach(function(item) { data.forEach(function(item) {
$("<li><span><i class='fa fa-table'></i> " + item + "</span></li>").appendTo("#tables"); $("<li><span><i class='fa fa-table'></i> " + item + " </span></li>").appendTo("#tables");
}); });
}); });
} }
@@ -68,7 +68,48 @@ function resetTable() {
removeClass("no-crop"); 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(); resetTable();
if (results.error) { if (results.error) {
@@ -87,7 +128,12 @@ function buildTable(results) {
var rows = ""; var rows = "";
results.columns.forEach(function(col) { 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) { results.rows.forEach(function(row) {
@@ -113,8 +159,8 @@ function showQueryHistory() {
} }
buildTable({ columns: ["id", "query", "timestamp"], rows: rows }); buildTable({ columns: ["id", "query", "timestamp"], rows: rows });
setCurrentTab("table_history"); setCurrentTab("table_history");
$("#input").hide(); $("#input").hide();
$("#output").addClass("full"); $("#output").addClass("full");
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
@@ -157,7 +203,7 @@ function showTableInfo() {
}); });
} }
function showTableContent() { function showTableContent(sortColumn, sortOrder) {
var name = getCurrentTable(); var name = getCurrentTable();
if (name.length == 0) { if (name.length == 0) {
@@ -165,8 +211,8 @@ function showTableContent() {
return; return;
} }
getTableRows(name, { limit: 100 }, function(data) { getTableRows(name, { limit: 100, sort_column: sortColumn, sort_order: sortOrder }, function(data) {
buildTable(data); buildTable(data, sortColumn, sortOrder);
setCurrentTab("table_content"); setCurrentTab("table_content");
$("#results").attr("data-mode", "browse"); $("#results").attr("data-mode", "browse");
@@ -186,6 +232,10 @@ function showTableStructure() {
getTableStructure(name, function(data) { getTableStructure(name, function(data) {
setCurrentTab("table_structure"); setCurrentTab("table_structure");
buildTable(data); 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() { function runQuery() {
setCurrentTab("table_query"); setCurrentTab("table_query");
$("#run, #explain, #csv").prop("disabled", true); $("#run, #explain, #csv").prop("disabled", true);
$("#query_progress").show(); $("#query_progress").show();
var query = $.trim(editor.getValue()); var query = $.trim(editor.getSelectedText() || editor.getValue());
if (query.length == 0) { if (query.length == 0) {
$("#run, #explain, #csv").prop("disabled", false); $("#run, #explain, #csv").prop("disabled", false);
@@ -400,6 +460,7 @@ $(document).ready(function() {
$("#table_history").on("click", function() { showQueryHistory(); }); $("#table_history").on("click", function() { showQueryHistory(); });
$("#table_query").on("click", function() { showQueryPanel(); }); $("#table_query").on("click", function() { showQueryPanel(); });
$("#table_connection").on("click", function() { showConnectionPanel(); }); $("#table_connection").on("click", function() { showConnectionPanel(); });
$("#table_activity").on("click", function() { showActivityPanel(); });
$("#run").on("click", function() { $("#run").on("click", function() {
runQuery(); runQuery();
@@ -418,6 +479,24 @@ $(document).ready(function() {
$(this).addClass("selected"); $(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() { $("#results").on("dblclick", "td > div", function() {
if ($(this).has("textarea").length > 0) { if ($(this).has("textarea").length > 0) {
return; return;
@@ -447,6 +526,16 @@ $(document).ready(function() {
showTableInfo(); 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() { $("#refresh_tables").on("click", function() {
loadTables(); loadTables();
}); });
@@ -529,7 +618,7 @@ $(document).ready(function() {
var button = $(this).children("button"); var button = $(this).children("button");
var url = getConnectionString(); var url = getConnectionString();
if (url.length == 0) { if (url.length == 0) {
return; return;
} }
@@ -557,7 +646,7 @@ $(document).ready(function() {
initEditor(); initEditor();
addShortcutTooltips(); addShortcutTooltips();
apiCall("get", "/connection", {}, function(resp) { apiCall("get", "/connection", {}, function(resp) {
if (resp.error) { if (resp.error) {
connected = false; connected = false;
@@ -571,4 +660,4 @@ $(document).ready(function() {
$("#main").show(); $("#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));