Compare commits

...
78 Commits
Author SHA1 Message Date
Dan Sosedoff 7766c3bb66 Version bump: 0.7.0 2016-01-05 13:14:36 -06:00
Dan Sosedoff 35aa91de27 Readme, license 2016-01-05 00:41:50 -06:00
Dan Sosedoff 3a793ed87d More design tweaks 2016-01-05 00:31:14 -06:00
Dan Sosedoff 9cfdf4a194 Fresh design 2016-01-04 20:25:54 -06:00
Dan Sosedoff aa0ffedc8c Update sql editor theme to twilight, make font bigger 2016-01-04 19:17:51 -06:00
Dan Sosedoff 82c162d399 Fix appveyor builds 2016-01-04 18:51:25 -06:00
Dan Sosedoff 995d52db1b Merge pull request #107 from sosedoff/export
Export to JSON / XML
2016-01-04 18:40:43 -06:00
Dan Sosedoff c7e58f0828 Remove duplicate code 2016-01-04 18:35:05 -06:00
Dan Sosedoff 1f99576a37 Add test cases for result struct 2016-01-04 18:33:54 -06:00
Dan Sosedoff 91d8d3ee83 Move result struct into its own file 2016-01-04 18:19:16 -06:00
Dan Sosedoff 5a92c5508c Add ability to export query results to JSON 2016-01-04 18:15:05 -06:00
Dan Sosedoff 73a97893e9 Add ability to export table to JSON and XML 2016-01-04 18:03:53 -06:00
Dan Sosedoff bc1f876fb5 Merge pull request #104 from brianlow/constraints
Show table constraints
2015-12-08 18:33:51 -06:00
Brian Low eeb0fdf3a4 Show table constraints 2015-12-04 17:15:03 -07:00
Dan Sosedoff 4fb24cb229 Merge pull request #100 from niiyz/dev-sequence
Add sequence.
2015-11-16 21:19:57 -06:00
niiyz d35b3661a9 Make Assets. 2015-11-14 08:45:55 +09:00
niiyz 3e6e223ed0 Indent modification. 2015-11-13 22:43:19 +09:00
niiyz 8579edf1ca Indent modification. 2015-11-13 22:41:53 +09:00
niiyz 420d85e811 Indent modification. 2015-11-13 22:34:53 +09:00
niiyz 924a46894b Add Sequence to Front. app.js 2015-11-13 22:26:05 +09:00
niiyz 71aec1f07a Add Sequences to Front. 2015-11-13 21:38:33 +09:00
niiyz 4688e62b90 Add Sequences to API Test. 2015-11-13 21:36:51 +09:00
niiyz 232790b452 Add Sequences to API. 2015-11-13 21:36:13 +09:00
Dan Sosedoff 4c1bb758ce Update change log 2015-10-19 21:20:03 -05:00
Dan Sosedoff 6749b2ace8 Make sure to close fake server connection in tests 2015-10-19 21:09:22 -05:00
Dan Sosedoff 8f7fdcbdca Update ci config 2015-09-14 22:57:53 -05:00
Dan Sosedoff 1e50471a87 CI on 1.5 2015-08-20 01:25:01 -05:00
Dan Sosedoff 20a5728152 Version bump: 0.6.3 2015-08-16 10:24:21 -05:00
Dan Sosedoff 3170fb22cb Escape postgres password, GH-96 2015-08-15 21:11:09 -05:00
Dan Sosedoff c02d15ec34 Encode sql query to base64 for CSV export, GH-95 2015-08-15 20:53:51 -05:00
Dan Sosedoff 2a4edaf08f Add request middleware to log incoming form params 2015-08-04 10:38:23 -05:00
Dan Sosedoff 639ca96caa Write current sql query after input query change 2015-08-02 19:21:42 -05:00
Dan Sosedoff fb13ce8b07 Automatically save last executed query to localStorage 2015-07-30 22:29:08 -05:00
Dan Sosedoff 2aacab44ec Revert "Test on tip"
This reverts commit 279acc8af8.
2015-07-30 22:21:01 -05:00
Dan Sosedoff 279acc8af8 Test on tip 2015-07-29 21:58:16 -05:00
Dan Sosedoff 3765728342 Update changelog 2015-07-17 19:37:07 -05:00
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
51 changed files with 2401 additions and 723 deletions
+2 -1
View File
@@ -1,11 +1,12 @@
language: go language: go
sudo: false
addons: addons:
postgresql: "9.3" postgresql: "9.3"
go: go:
- 1.4.1
- 1.4.2 - 1.4.2
- 1.5.1
install: install:
- make setup - make setup
+33
View File
@@ -1,3 +1,36 @@
## 0.6.3 - 2015-08-16
- Adds PostgreSQL password escaping in web ui, GH-96
- Adds base64 query encoding for CSV export, GH-95
- Adds automatic saving of last executed query to localStorage
- Adds request middleware to log incoming form params in debug mode
## 0.6.2 - 2015-07-15
- Adds ability to specify connection strings prefixed by `postgresql://`, [GH-92]
- Updates configuration for Heroku, [GH-89], [GH-90]
- Updates postgresql library dependency to latest, [GH-91]
- Fixes password field to not display plaintext passwords, [GH-87]
## 0.6.1 - 2015-06-18
- This release is repackage-release targeted to fix binary downloads
## 0.6.0 - 2015-05-31
- Adds ability to execute only selected SQL query in run command view, [GH-85]
- Adds ability to delete/truncate table via context meny on sidebar view
- Adds ability to export table contents to CSV via context menu on sidebar view
- Changes sidebar color scheme to a lighter and better looking one
## 0.5.3 - 2015-05-06
- Changes default server port from 8080 to 8081 to avoil conflict with RethinkDB
- Changes styles for table rows and connection settings window
- Adds highlighting styles for columns with sort order
- Adds git sha into program version output
- Add new endpoint /api/info to get build details
## 0.5.2 - 2015-04-13 ## 0.5.2 - 2015-04-13
- Adds a new endpoint /activity that retuns active queries - Adds a new endpoint /activity that retuns active queries
+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
+1
View File
@@ -8,4 +8,5 @@ RUN go get github.com/tools/godep
RUN godep restore RUN godep restore
RUN godep go build && godep go install RUN godep go build && godep go install
EXPOSE 8081
CMD ["pgweb", "--bind", "0.0.0.0"] CMD ["pgweb", "--bind", "0.0.0.0"]
+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)
} }
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014-2015 Dan Sosedoff <dan.sosedoff@gmail.com> Copyright (c) 2014-2016 Dan Sosedoff <dan.sosedoff@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in this software and associated documentation files (the "Software"), to deal in
+6 -2
View File
@@ -40,11 +40,15 @@ build: assets
@echo "You can now execute ./pgweb" @echo "You can now execute ./pgweb"
release: assets release: assets
gox \ @echo "Building binaries..."
@gox \
-osarch="$(TARGETS)" \ -osarch="$(TARGETS)" \
-ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \ -ldflags "-X github.com/sosedoff/pgweb/pkg/command.GitCommit $(GIT_COMMIT) -X github.com/sosedoff/pgweb/pkg/command.BuildTime $(BUILD_TIME)" \
-output="./bin/pgweb_{{.OS}}_{{.Arch}}" -output="./bin/pgweb_{{.OS}}_{{.Arch}}"
@echo "\nPackaging binaries...\n"
@./script/package.sh
bootstrap: bootstrap:
gox -build-toolchain gox -build-toolchain
@@ -57,7 +61,7 @@ setup:
clean: clean:
rm -f ./pgweb rm -f ./pgweb
rm -f ./bin/* rm -rf ./bin/*
rm -f bindata.go rm -f bindata.go
make assets make assets
+20 -17
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 to CSV/JSON/XML
- Query history - Query history
- Server bookmarks - Server bookmarks
@@ -63,23 +64,25 @@ pgweb --url postgres://user:password@host:port/database?sslmode=[mode]
## Testing ## Testing
Run tests: Before running tests, make sure you have PostgreSQL server running on `localhost:5432`
interface. Also, you must have `postgres` user that could create new databases
in your local environment. Pgweb server should not be running at the same time.
Execute test suite:
``` ```
make test make test
``` ```
Continuous integration status:
- Travis (Linux) - [![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
@@ -91,4 +94,4 @@ Continuous integration status:
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014-2015 Dan Sosedoff, <dan.sosedoff@gmail.com> Copyright (c) 2014-2016 Dan Sosedoff, <dan.sosedoff@gmail.com>
+7
View File
@@ -0,0 +1,7 @@
# Screenshots
### Browse table rows
<img src="screenshots/browse.png" />
### Write SQL queries
<img src="screenshots/query.png" />
+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,13 +1,15 @@
clone_folder: c:\go\src\github.com\sosedoff\pgweb clone_folder: c:\go\src\github.com\sosedoff\pgweb
environment: environment:
GOPATH: c:\go GOPATH: c:\gopath
services: services:
- postgresql - postgresql
install: install:
- set PATH=%GOPATH%\bin;%PATH% - set PATH=%GOPATH%\bin;%PATH%
- echo %PATH%
- echo %GOPATH%
- cd %APPVEYOR_BUILD_FOLDER% - cd %APPVEYOR_BUILD_FOLDER%
- go env - go env
- go version - go version
@@ -20,6 +22,6 @@ build_script:
- godep go build - godep go build
test_script: test_script:
- godep go test - godep go test -cover ./...
deploy: off deploy: off
+35 -9
View File
@@ -1,6 +1,7 @@
package api package api
import ( import (
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
@@ -163,6 +164,11 @@ func GetConnectionInfo(c *gin.Context) {
c.JSON(200, res.Format()[0]) c.JSON(200, res.Format()[0])
} }
func GetSequences(c *gin.Context) {
res, err := DbClient.Sequences()
serveResult(res, err, c)
}
func GetActivity(c *gin.Context) { func GetActivity(c *gin.Context) {
res, err := DbClient.Activity() res, err := DbClient.Activity()
serveResult(res, err, c) serveResult(res, err, c)
@@ -173,24 +179,44 @@ func GetTableIndexes(c *gin.Context) {
serveResult(res, err, c) serveResult(res, err, c)
} }
func HandleQuery(query string, c *gin.Context) { func GetTableConstraints(c *gin.Context) {
result, err := DbClient.Query(query) res, err := DbClient.TableConstraints(c.Params.ByName("table"))
serveResult(res, err, c)
}
func HandleQuery(query string, c *gin.Context) {
rawQuery, err := base64.StdEncoding.DecodeString(query)
if err == nil {
query = string(rawQuery)
}
result, err := DbClient.Query(query)
if err != nil { if err != nil {
c.JSON(400, NewError(err)) c.JSON(400, NewError(err))
return return
} }
q := c.Request.URL.Query() format := getQueryParam(c, "format")
filename := getQueryParam(c, "filename")
if len(q["format"]) > 0 && q["format"][0] == "csv" { if filename == "" {
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix()) filename = fmt.Sprintf("pgweb-%v.%v", time.Now().Unix(), format)
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
c.Data(200, "text/csv", result.CSV())
return
} }
c.JSON(200, result) if format != "" {
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
}
switch format {
case "csv":
c.Data(200, "text/csv", result.CSV())
case "json":
c.Data(200, "applicaiton/json", result.JSON())
case "xml":
c.XML(200, result)
default:
c.JSON(200, result)
}
} }
func GetBookmarks(c *gin.Context) { func GetBookmarks(c *gin.Context) {
+21
View File
@@ -1,6 +1,7 @@
package api package api
import ( import (
"log"
"mime" "mime"
"path/filepath" "path/filepath"
@@ -21,6 +22,17 @@ type Error struct {
Message string `json:"error"` Message string `json:"error"`
} }
func getQueryParam(c *gin.Context, name string) string {
result := ""
q := c.Request.URL.Query()
if len(q[name]) > 0 {
result = q[name][0]
}
return result
}
func assetContentType(name string) string { func assetContentType(name string) string {
ext := filepath.Ext(name) ext := filepath.Ext(name)
result := mime.TypeByExtension(ext) result := mime.TypeByExtension(ext)
@@ -76,6 +88,15 @@ func dbCheckMiddleware() gin.HandlerFunc {
} }
} }
// Middleware function to print out request parameters and body for debugging
func requestInspectMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := c.Request.ParseForm()
log.Println("Request params:", err, c.Request.Form)
}
}
func serveStaticAsset(path string, c *gin.Context) { func serveStaticAsset(path string, c *gin.Context) {
data, err := data.Asset("static" + path) data, err := data.Asset("static" + path)
if err != nil { if err != nil {
+12 -1
View File
@@ -2,20 +2,30 @@ package api
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/command"
) )
func SetupMiddlewares(group *gin.RouterGroup) {
if command.Opts.Debug {
group.Use(requestInspectMiddleware())
}
group.Use(dbCheckMiddleware())
}
func SetupRoutes(router *gin.Engine) { func SetupRoutes(router *gin.Engine) {
router.GET("/", GetHome) router.GET("/", GetHome)
router.GET("/static/*path", GetAsset) router.GET("/static/*path", GetAsset)
api := router.Group("/api") api := router.Group("/api")
{ {
api.Use(dbCheckMiddleware()) SetupMiddlewares(api)
api.GET("/info", GetInfo) api.GET("/info", GetInfo)
api.POST("/connect", Connect) api.POST("/connect", Connect)
api.GET("/databases", GetDatabases) api.GET("/databases", GetDatabases)
api.GET("/connection", GetConnectionInfo) api.GET("/connection", GetConnectionInfo)
api.GET("/sequences", GetSequences)
api.GET("/activity", GetActivity) api.GET("/activity", GetActivity)
api.GET("/schemas", GetSchemas) api.GET("/schemas", GetSchemas)
api.GET("/tables", GetTables) api.GET("/tables", GetTables)
@@ -23,6 +33,7 @@ func SetupRoutes(router *gin.Engine) {
api.GET("/tables/:table/rows", GetTableRows) api.GET("/tables/:table/rows", GetTableRows)
api.GET("/tables/:table/info", GetTableInfo) api.GET("/tables/:table/info", GetTableInfo)
api.GET("/tables/:table/indexes", GetTableIndexes) api.GET("/tables/:table/indexes", GetTableIndexes)
api.GET("/tables/:table/constraints", GetTableConstraints)
api.GET("/query", RunQuery) api.GET("/query", RunQuery)
api.POST("/query", RunQuery) api.POST("/query", RunQuery)
api.GET("/explain", ExplainQuery) api.GET("/explain", ExplainQuery)
+14 -54
View File
@@ -1,8 +1,6 @@
package client package client
import ( import (
"bytes"
"encoding/csv"
"fmt" "fmt"
"reflect" "reflect"
@@ -21,13 +19,6 @@ type Client struct {
ConnectionString string ConnectionString string
} }
type Row []interface{}
type Result struct {
Columns []string `json:"columns"`
Rows []Row `json:"rows"`
}
// Struct to hold table rows browsing options // Struct to hold table rows browsing options
type RowsOptions struct { type RowsOptions struct {
Limit int // Number of rows to fetch Limit int // Number of rows to fetch
@@ -136,6 +127,20 @@ func (client *Client) TableIndexes(table string) (*Result, error) {
return res, err return res, err
} }
func (client *Client) TableConstraints(table string) (*Result, error) {
res, err := client.query(statements.PG_TABLE_CONSTRAINTS, table)
if err != nil {
return nil, err
}
return res, err
}
func (client *Client) Sequences() ([]string, error) {
return client.fetchRows(statements.PG_SEQUENCES)
}
// Returns all active queriers on the server // Returns all active queriers on the server
func (client *Client) Activity() (*Result, error) { func (client *Client) Activity() (*Result, error) {
return client.query(statements.PG_ACTIVITY) return client.query(statements.PG_ACTIVITY)
@@ -191,51 +196,6 @@ func (client *Client) query(query string, args ...interface{}) (*Result, error)
return &result, nil return &result, nil
} }
func (res *Result) Format() []map[string]interface{} {
var items []map[string]interface{}
for _, row := range res.Rows {
item := make(map[string]interface{})
for i, c := range res.Columns {
item[c] = row[i]
}
items = append(items, item)
}
return items
}
func (res *Result) CSV() []byte {
buff := &bytes.Buffer{}
writer := csv.NewWriter(buff)
writer.Write(res.Columns)
for _, row := range res.Rows {
record := make([]string, len(res.Columns))
for i, item := range row {
if item != nil {
record[i] = fmt.Sprintf("%v", item)
} else {
record[i] = ""
}
}
err := writer.Write(record)
if err != nil {
fmt.Println(err)
break
}
}
writer.Flush()
return buff.Bytes()
}
// Close database connection // Close database connection
func (client *Client) Close() error { func (client *Client) Close() error {
if client.db != nil { if client.db != nil {
+36
View File
@@ -81,6 +81,18 @@ func test_NewClientFromUrl(t *testing.T) {
assert.Equal(t, url, client.ConnectionString) assert.Equal(t, url, client.ConnectionString)
} }
func test_NewClientFromUrl2(t *testing.T) {
url := "postgresql://postgres@localhost/booktown?sslmode=disable"
client, err := NewFromUrl(url)
if err != nil {
defer client.Close()
}
assert.Equal(t, nil, err)
assert.Equal(t, url, client.ConnectionString)
}
func test_Test(t *testing.T) { func test_Test(t *testing.T) {
assert.Equal(t, nil, testClient.Test()) assert.Equal(t, nil, testClient.Test())
} }
@@ -176,6 +188,28 @@ func test_TableIndexes(t *testing.T) {
assert.Equal(t, 2, len(res.Rows)) assert.Equal(t, 2, len(res.Rows))
} }
func test_TableConstraints(t *testing.T) {
res, err := testClient.TableConstraints("editions")
assert.Equal(t, nil, err)
assert.Equal(t, 1, len(res.Columns))
assert.Equal(t, 2, len(res.Rows))
}
func test_Sequences(t *testing.T) {
res, err := testClient.Sequences()
expected := []string{
"author_ids",
"book_ids",
"shipments_ship_id_seq",
"subject_ids",
}
assert.Equal(t, nil, err)
assert.Equal(t, expected, res)
}
func test_Query(t *testing.T) { func test_Query(t *testing.T) {
res, err := testClient.Query("SELECT * FROM books") res, err := testClient.Query("SELECT * FROM books")
@@ -245,6 +279,8 @@ func TestAll(t *testing.T) {
test_TableRows(t) test_TableRows(t)
test_TableInfo(t) test_TableInfo(t)
test_TableIndexes(t) test_TableIndexes(t)
test_TableConstraints(t)
test_Sequences(t)
test_Query(t) test_Query(t)
test_QueryError(t) test_QueryError(t)
test_QueryInvalidTable(t) test_QueryInvalidTable(t)
+65
View File
@@ -0,0 +1,65 @@
package client
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
)
type Row []interface{}
type Result struct {
Columns []string `json:"columns"`
Rows []Row `json:"rows"`
}
func (res *Result) Format() []map[string]interface{} {
var items []map[string]interface{}
for _, row := range res.Rows {
item := make(map[string]interface{})
for i, c := range res.Columns {
item[c] = row[i]
}
items = append(items, item)
}
return items
}
func (res *Result) CSV() []byte {
buff := &bytes.Buffer{}
writer := csv.NewWriter(buff)
writer.Write(res.Columns)
for _, row := range res.Rows {
record := make([]string, len(res.Columns))
for i, item := range row {
if item != nil {
record[i] = fmt.Sprintf("%v", item)
} else {
record[i] = ""
}
}
err := writer.Write(record)
if err != nil {
fmt.Println(err)
break
}
}
writer.Flush()
return buff.Bytes()
}
func (res *Result) JSON() []byte {
data, _ := json.Marshal(res.Format())
return data
}
+46
View File
@@ -0,0 +1,46 @@
package client
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_CSV(t *testing.T) {
result := Result{
Columns: []string{"id", "name", "email"},
Rows: []Row{
Row{1, "John", "john@example.com"},
Row{2, "Bob", "bob@example.com"},
},
}
expected := "id,name,email\n1,John,john@example.com\n2,Bob,bob@example.com\n"
output := string(result.CSV())
assert.Equal(t, expected, output)
}
func Test_JSON(t *testing.T) {
result := Result{
Columns: []string{"id", "name", "email"},
Rows: []Row{
Row{1, "John", "john@example.com"},
Row{2, "Bob", "bob@example.com"},
},
}
output := result.JSON()
obj := []map[string]interface{}{}
err := json.Unmarshal(output, &obj)
assert.NoError(t, err)
assert.Equal(t, 2, len(obj))
for i, row := range obj {
for j, col := range result.Columns {
assert.Equal(t, result.Rows[i][j], row[col])
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
package command package command
const VERSION = "0.5.2" const VERSION = "0.7.0"
var ( var (
GitCommit string GitCommit string
+3 -2
View File
@@ -3,6 +3,7 @@ package connection
import ( import (
"errors" "errors"
"fmt" "fmt"
neturl "net/url"
"os" "os"
"os/user" "os/user"
"strings" "strings"
@@ -28,7 +29,7 @@ func FormatUrl(opts command.Options) (string, error) {
url := opts.Url url := opts.Url
// Make sure to only accept urls in a standard format // Make sure to only accept urls in a standard format
if !strings.Contains(url, "postgres://") { if !strings.HasPrefix(url, "postgres://") && !strings.HasPrefix(url, "postgresql://") {
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode") return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
} }
@@ -83,7 +84,7 @@ func BuildString(opts command.Options) (string, error) {
} }
if opts.Pass != "" { if opts.Pass != "" {
url += fmt.Sprintf(":%s", opts.Pass) url += fmt.Sprintf(":%s", neturl.QueryEscape(opts.Pass))
} }
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port) url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func Test_Invalid_Url(t *testing.T) { func Test_Invalid_Url(t *testing.T) {
opts := command.Options{} opts := command.Options{}
examples := []string{ examples := []string{
"postgresql://foobar", "postgre://foobar",
"foobar", "foobar",
} }
+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")
}
+75
View File
@@ -0,0 +1,75 @@
package connection
import (
"fmt"
"net"
"os"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_portAvailable(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("FIXME")
}
assert.Equal(t, true, portAvailable(8081))
serv, err := net.Listen("tcp", "127.0.0.1:8081")
if err != nil {
fmt.Println(os.Stderr, "Unable to start test tcp listener:", err)
t.Fail()
return
}
defer serv.Close()
go func() {
for {
conn, err := serv.Accept()
if err == nil {
conn.Close()
}
serv.Close()
}
}()
assert.Equal(t, false, portAvailable(8081))
assert.Equal(t, true, portAvailable(8082))
}
func Test_getAvailablePort(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("FIXME")
}
port, err := getAvailablePort(8081, 1)
assert.Equal(t, nil, err)
assert.Equal(t, 8081, port)
serv, err := net.Listen("tcp", "127.0.0.1:8081")
if err != nil {
fmt.Println(os.Stderr, "Unable to start test tcp listener:", err)
t.Fail()
return
}
defer serv.Close()
go func() {
for {
conn, err := serv.Accept()
if err == nil {
conn.Close()
}
}
}()
port, err = getAvailablePort(8081, 0)
assert.EqualError(t, err, "No available port")
assert.Equal(t, -1, port)
port, err = getAvailablePort(8081, 1)
assert.Equal(t, nil, err)
assert.Equal(t, 8082, port)
}
+283 -230
View File
File diff suppressed because one or more lines are too long
+12
View File
@@ -18,6 +18,16 @@ const (
PG_TABLE_INDEXES = `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = $1` PG_TABLE_INDEXES = `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = $1`
PG_TABLE_CONSTRAINTS = `SELECT
pg_get_constraintdef(c.oid, true) as condef
FROM pg_constraint c
JOIN pg_namespace n ON n.oid = c.connamespace
JOIN pg_class cl ON cl.oid = c.conrelid
WHERE n.nspname = 'public'
AND relname = $1
ORDER BY contype desc`
PG_TABLE_INFO = `SELECT PG_TABLE_INFO = `SELECT
pg_size_pretty(pg_table_size($1)) AS data_size pg_size_pretty(pg_table_size($1)) AS data_size
, pg_size_pretty(pg_indexes_size($1)) AS index_size , pg_size_pretty(pg_indexes_size($1)) AS index_size
@@ -31,6 +41,8 @@ WHERE table_name = $1`
PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name` PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name`
PG_SEQUENCES = `SELECT sequence_name FROM information_schema.sequences WHERE sequence_schema = 'public' ORDER BY sequence_name`
PG_ACTIVITY = `SELECT PG_ACTIVITY = `SELECT
datname, datname,
query, query,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 367 KiB

+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
+73 -36
View File
@@ -8,8 +8,7 @@
left: 250px; left: 250px;
right: 0; right: 0;
height: 50px; height: 50px;
box-shadow: inset 0 -1px 0 0 #b8b7b5; background: #79589f;
background: #d8d7d6;
} }
#nav ul { #nav ul {
@@ -27,7 +26,7 @@
line-height: 49px; line-height: 49px;
font-size: 13px; font-size: 13px;
padding: 0px 14px; padding: 0px 14px;
color: #6c6b6b; color: #d6cce2;
font-weight: 500; font-weight: 500;
margin: 0 1px; margin: 0 1px;
cursor: pointer; cursor: pointer;
@@ -41,15 +40,13 @@
} }
#nav ul li:hover { #nav ul li:hover {
color: rgba(0,0,0,0.9); color: #fff;
} }
#nav ul li.selected { #nav ul li.selected {
position: relative; position: relative;
background: #fff; background: #fff;
color: #000; color: #000;
border-right: 1px solid #b8b7b5;
border-left: 1px solid #b8b7b5;
margin: 0; margin: 0;
} }
@@ -73,6 +70,8 @@
left: 0px; left: 0px;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
box-sizing: border-box;
background: #f9f9f9;
} }
#sidebar .title { #sidebar .title {
@@ -83,8 +82,17 @@
height: 33px; height: 33px;
padding: 0px 8px; padding: 0px 8px;
padding-left: 12px; padding-left: 12px;
color: #555;
font-weight: bold;
font-size: 13px;
}
#sidebar .title.main {
line-height: 50px;
height: 50px;
background: #79589f;
color: #fff; color: #fff;
background: #31373d; font-size: 14px;
} }
#sidebar div.tables-list { #sidebar div.tables-list {
@@ -94,7 +102,6 @@
left: 0px; left: 0px;
bottom: 130px; bottom: 130px;
overflow: auto; overflow: auto;
background: #31373d;
} }
#sidebar div.tables-list .wrap { #sidebar div.tables-list .wrap {
@@ -114,17 +121,13 @@
float: right; float: right;
margin-right: 4px; margin-right: 4px;
cursor: pointer; cursor: pointer;
color: #95a7b7; color: #fff;
display: none; display: none;
} }
#sidebar div.tables-list .title span.refresh:hover { #sidebar div.tables-list #tables, #sequences {
color: #fff; padding: 50px 0 0;
} font-size: 12px;
#sidebar div.tables-list #tables {
padding: 33px 0 0;
font-size: 13px;
} }
#sidebar div.table-information { #sidebar div.table-information {
@@ -133,7 +136,9 @@
left: 0px; left: 0px;
height: 130px; height: 130px;
bottom: 0px; bottom: 0px;
background: #272c30; /*border-top: 1px solid #d4d4d4;*/
/*border-right: 1px solid #d4d4d4;*/
box-sizing: border-box;
} }
#sidebar div.table-information .wrap { #sidebar div.table-information .wrap {
@@ -142,7 +147,7 @@
} }
#sidebar div.table-information .title { #sidebar div.table-information .title {
background: #272c30; /*background: #f6f6f6;*/
} }
#sidebar div.table-information ul { #sidebar div.table-information ul {
@@ -156,11 +161,11 @@
height: 24px; height: 24px;
padding: 0 8px 0 12px; padding: 0 8px 0 12px;
cursor: default; cursor: default;
color: #637D94 !important; color: #7F7E7F !important;
} }
#sidebar div.table-information ul li span { #sidebar div.table-information ul li span {
color: #95a7b7; color: #4F4F4F;
min-width: 0; min-width: 0;
padding-left: 0px; padding-left: 0px;
} }
@@ -177,19 +182,18 @@
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
cursor: pointer; cursor: pointer;
color: #95a7b7 !important; color: #4F4E4F !important;
white-space: nowrap; white-space: nowrap;
} }
#sidebar li.selected { #sidebar li.selected {
color: #fff !important;
font-weight: bold; font-weight: bold;
background: #272c30; background: #e6e6e6;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
#sidebar ul li:hover span { #sidebar ul li:hover span {
background: #272c30; background: #f1f1f1;
} }
#sidebar ul li i { #sidebar ul li i {
@@ -225,8 +229,7 @@
background: #fff; background: #fff;
padding: 10px; padding: 10px;
height: 50px; height: 50px;
border-top: solid 1px #e1e2e3; border-bottom: solid 1px #ddd;
border-bottom: solid 1px #b8b7b5;
} }
#input .actions .btn { #input .actions .btn {
@@ -235,18 +238,32 @@
padding: 0px 13px; padding: 0px 13px;
margin: 0px; margin: 0px;
font-size: 13px; font-size: 13px;
color: #fff; color: #555;
border: none; border: none;
box-shadow: none; box-shadow: none;
background: #7eb54e; background: transparent;
border: 1px solid #999;
float: left; float: left;
margin-right: 10px; margin-right: 10px;
} }
#input .actions .btn-default { #input .actions .btn-primary {
background: #aaa; border-color: #7eb54e;
color: #7eb54e;
} }
#input .actions .btn:hover {
background: transparent !important;
border-color: #444;
color: #000;
}
#input .actions .btn-primary:hover {
color: #64903e;
border-color: #64903e;
}
#input .actions #query_progress { #input .actions #query_progress {
display: none; display: none;
float: left; float: left;
@@ -295,7 +312,6 @@
#results tr:nth-child(even) > td { #results tr:nth-child(even) > td {
border: none; border: none;
background: #fafafa;
} }
#results tr:nth-child(odd) > td { #results tr:nth-child(odd) > td {
@@ -304,18 +320,17 @@
} }
#results th { #results th {
background: #f3f3f3;
border-top: none; border-top: none;
border-bottom: 1px solid #eae9e9; border-bottom: 1px solid #eae9e9;
padding: 3px 9px; padding: 3px 9px;
line-height: 24px; line-height: 24px;
color: #6a6a6a; color: #333;
font-weight: bold; font-weight: bold;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
#results th.active { #results th.active {
background: #e3e3e3; background: #f3f3f3;
} }
#results[data-mode="browse"] th:hover { #results[data-mode="browse"] th:hover {
@@ -376,7 +391,8 @@
} }
#custom_query { #custom_query {
height: 205px; height: 193px;
margin-top: 12px;
} }
#connection_window { #connection_window {
@@ -394,6 +410,9 @@
position: fixed; position: fixed;
right: 8px; right: 8px;
top: 10px; top: 10px;
background: #79589f;
color: #fff;
border-color: #fff;
} }
#edit_connection i { #edit_connection i {
@@ -444,4 +463,22 @@
.connection-ssh-group { .connection-ssh-group {
display: none; display: none;
} z-index: 1000;
}
/* -------------------------------------------------------------------------- */
/* Ace Customizations */
/* -------------------------------------------------------------------------- */
.ace_gutter, .ace_gutter-cell {
background: #fff !important;
color: #bbb;
}
.ace_active-line {
background: #f9f9f9 !important;
}
.ace_active-line {
background: none !important;
}
+19 -6
View File
@@ -12,31 +12,34 @@
<script type="text/javascript" src="/static/js/jquery.js"></script> <script type="text/javascript" src="/static/js/jquery.js"></script>
<script type="text/javascript" src="/static/js/ace.js"></script> <script type="text/javascript" src="/static/js/ace.js"></script>
<script type="text/javascript" src="/static/js/ace-pgsql.js"></script> <script type="text/javascript" src="/static/js/ace-pgsql.js"></script>
<script type="text/javascript" src="/static/js/bootstrap-contextmenu.js"></script>
<script type="text/javascript" src="/static/js/app.js"></script> <script type="text/javascript" src="/static/js/app.js"></script>
</head> </head>
<body> <body>
<div id="main"> <div id="main">
<div id="nav"> <div id="nav">
<ul> <ul>
<li id="table_content">Content</li> <li id="table_content">Rows</li>
<li id="table_structure">Structure</li> <li id="table_structure">Structure</li>
<li id="table_indexes">Indexes</li> <li id="table_indexes">Indexes</li>
<li id="table_constraints">Constraints</li>
<li id="table_query" class="selected">SQL Query</li> <li id="table_query" class="selected">SQL Query</li>
<li id="table_history">History</li> <li id="table_history">History</li>
<li id="table_activity">Activity</li> <li id="table_activity">Activity</li>
<li id="table_connection">Connection</li> <li id="table_connection">Connection</li>
</ul> </ul>
<a href="#" id="edit_connection" class="btn btn-primary btn-sm"><i class="fa fa-gear"></i> Edit Connection</a> <a href="#" id="edit_connection" class="btn btn-default btn-sm"><i class="fa fa-globe"></i> Edit Connection</a>
</div> </div>
<div id="sidebar"> <div id="sidebar">
<div class="tables-list"> <div class="tables-list">
<div class="wrap"> <div class="wrap">
<div class="title"> <div class="title main">
<i class="fa fa-database"></i> <span class="current-database" id="current_database"></span> <i class="fa fa-database"></i> <span class="current-database" id="current_database"></span>
<span class="refresh" id="refresh_tables" title="Refresh tables list"><i class="fa fa-refresh"></i></span> <span class="refresh" id="refresh_tables" title="Refresh tables list"><i class="fa fa-refresh"></i></span>
</div> </div>
<ul id="tables"></ul> <ul id="tables"></ul>
<ul id="sequences"></ul>
</div> </div>
</div> </div>
<div class="table-information"> <div class="table-information">
@@ -58,7 +61,8 @@
<div class="actions"> <div class="actions">
<input type="button" id="run" value="Run Query" class="btn btn-sm btn-primary" /> <input type="button" id="run" value="Run Query" class="btn btn-sm btn-primary" />
<input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" /> <input type="button" id="explain" value="Explain Query" class="btn btn-sm btn-default" />
<input type="button" id="csv" value="Download CSV" class="btn btn-sm btn-default" /> <input type="button" id="csv" value="CSV" class="btn btn-sm btn-default" />
<input type="button" id="json" value="JSON" class="btn btn-sm btn-default" />
<div id="query_progress">Please wait, query is executing...</div> <div id="query_progress">Please wait, query is executing...</div>
</div> </div>
@@ -81,7 +85,6 @@
<div class="btn-group btn-group-sm connection-group-switch"> <div class="btn-group btn-group-sm connection-group-switch">
<button type="button" data="scheme" class="btn btn-default" id="connection_scheme">Scheme</button> <button type="button" data="scheme" class="btn btn-default" id="connection_scheme">Scheme</button>
<button type="button" data="standard" class="btn btn-default active" id="connection_standard">Standard</button> <button type="button" data="standard" class="btn btn-default active" id="connection_standard">Standard</button>
<!--<button type="button" data="ssh" class="btn btn-default">SSH</button>-->
</div> </div>
</div> </div>
@@ -122,7 +125,7 @@
<div class="form-group"> <div class="form-group">
<label class="col-sm-3 control-label">Password</label> <label class="col-sm-3 control-label">Password</label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" id="pg_password" class="form-control" /> <input type="password" id="pg_password" class="form-control" />
</div> </div>
</div> </div>
@@ -193,5 +196,15 @@
</form> </form>
</div> </div>
</div> </div>
<div id="tables_context_menu">
<ul class="dropdown-menu" role="menu">
<li><a href="#" data-action="export" data-format="json">Export to JSON</a></li>
<li><a href="#" data-action="export" data-format="csv">Export to CSV</a></li>
<li><a href="#" data-action="export" data-format="xml">Export to XML</a></li>
<li class="divider"></li>
<li><a href="#" data-action="truncate">Truncate table</a></li>
<li><a href="#" data-action="delete">Delete table</a></li>
</ul>
</div>
</body> </body>
</html> </html>
+152 -36
View File
@@ -17,12 +17,18 @@ function apiCall(method, path, params, cb) {
}); });
} }
function getTables(cb) { apiCall("get", "/tables", {}, cb); } function getTables(cb) { apiCall("get", "/tables", {}, cb); }
function getTableRows(table, opts, cb) { apiCall("get", "/tables/" + table + "/rows", opts, cb); } function getTableRows(table, opts, cb) { apiCall("get", "/tables/" + table + "/rows", opts, cb); }
function getTableStructure(table, cb) { apiCall("get", "/tables/" + table, {}, cb); } function getTableStructure(table, cb) { apiCall("get", "/tables/" + table, {}, cb); }
function getTableIndexes(table, cb) { apiCall("get", "/tables/" + table + "/indexes", {}, cb); } function getTableIndexes(table, cb) { apiCall("get", "/tables/" + table + "/indexes", {}, cb); }
function getHistory(cb) { apiCall("get", "/history", {}, cb); } function getTableConstraints(table, cb) { apiCall("get", "/tables/" + table + "/constraints", {}, cb); }
function getBookmarks(cb) { apiCall("get", "/bookmarks", {}, cb); } function getHistory(cb) { apiCall("get", "/history", {}, cb); }
function getBookmarks(cb) { apiCall("get", "/bookmarks", {}, cb); }
function getSequences(cb) { apiCall("get", "/sequences", {}, cb); }
function encodeQuery(query) {
return window.btoa(query);
}
function executeQuery(query, cb) { function executeQuery(query, cb) {
apiCall("post", "/query", { query: query }, cb); apiCall("post", "/query", { query: query }, cb);
@@ -37,7 +43,17 @@ function loadTables() {
getTables(function(data) { getTables(function(data) {
data.forEach(function(item) { data.forEach(function(item) {
$("<li><span><i class='fa fa-table'></i> " + item + "</span></li>").appendTo("#tables"); $("<li><span><i class='fa fa-table'></i> " + item + " </span></li>").appendTo("#tables");
});
});
}
function loadSequences() {
$("#sequences li").remove();
getSequences(function(data) {
data.forEach(function(item) {
$("<li><span><i class='fa fa-chevron-right'></i> " + item + " </span></li>").appendTo("#sequences");
}); });
}); });
} }
@@ -68,15 +84,47 @@ function resetTable() {
removeClass("no-crop"); removeClass("no-crop");
} }
function performTableAction(table, action, el) {
if (action == "truncate" || action == "delete") {
var message = "Are you sure you want to " + action + " table " + table + " ?";
if (!confirm(message)) return;
}
switch(action) {
case "truncate":
executeQuery("TRUNCATE TABLE " + table, function(data) {
if (data.error) alert(data.error);
resetTable();
});
break;
case "delete":
executeQuery("DROP TABLE " + table, function(data) {
if (data.error) alert(data.error);
loadTables();
loadSequences();
resetTable();
});
break;
case "export":
var format = el.data("format");
var filename = table + "." + format;
var query = window.encodeURI("SELECT * FROM " + table);
var url = "http://" + window.location.host + "/api/query?format=" + format + "&filename=" + filename + "&query=" + query;
var win = window.open(url, "_blank");
win.focus();
break;
}
}
function sortArrow(direction) { function sortArrow(direction) {
switch (direction) { switch (direction) {
case "ASC": case "ASC":
return "&#x25B2;"; return "&#x25B2;";
case "DESC": case "DESC":
return "&#x25BC;"; return "&#x25BC;";
default: default:
return ""; return "";
} }
} }
function buildTable(results, sortColumn, sortOrder) { function buildTable(results, sortColumn, sortOrder) {
@@ -155,6 +203,24 @@ function showTableIndexes() {
}); });
} }
function showTableConstraints() {
var name = getCurrentTable();
if (name.length == 0) {
alert("Please select a table!");
return;
}
getTableConstraints(name, function(data) {
setCurrentTab("table_constraints");
buildTable(data);
$("#input").hide();
$("#output").addClass("full");
$("#results").addClass("no-crop");
});
}
function showTableInfo() { function showTableInfo() {
var name = getCurrentTable(); var name = getCurrentTable();
@@ -250,13 +316,13 @@ function showActivityPanel() {
function runQuery() { function runQuery() {
setCurrentTab("table_query"); setCurrentTab("table_query");
$("#run, #explain, #csv").prop("disabled", true); $("#run, #explain, #csv, #json").prop("disabled", true);
$("#query_progress").show(); $("#query_progress").show();
var query = $.trim(editor.getValue()); var query = $.trim(editor.getSelectedText() || editor.getValue());
if (query.length == 0) { if (query.length == 0) {
$("#run, #explain, #csv").prop("disabled", false); $("#run, #explain, #csv, #json").prop("disabled", false);
$("#query_progress").hide(); $("#query_progress").hide();
return; return;
} }
@@ -264,7 +330,7 @@ function runQuery() {
executeQuery(query, function(data) { executeQuery(query, function(data) {
buildTable(data); buildTable(data);
$("#run, #explain, #csv").prop("disabled", false); $("#run, #explain, #csv, #json").prop("disabled", false);
$("#query_progress").hide(); $("#query_progress").hide();
$("#input").show(); $("#input").show();
$("#output").removeClass("full"); $("#output").removeClass("full");
@@ -278,6 +344,7 @@ function runQuery() {
// Refresh tables list if table was added or removed // Refresh tables list if table was added or removed
if (query.match(re)) { if (query.match(re)) {
loadTables(); loadTables();
loadSequences();
} }
}); });
} }
@@ -285,13 +352,13 @@ function runQuery() {
function runExplain() { function runExplain() {
setCurrentTab("table_query"); setCurrentTab("table_query");
$("#run, #explain, #csv").prop("disabled", true); $("#run, #explain, #csv, #json").prop("disabled", true);
$("#query_progress").show(); $("#query_progress").show();
var query = $.trim(editor.getValue()); var query = $.trim(editor.getValue());
if (query.length == 0) { if (query.length == 0) {
$("#run, #explain, #csv").prop("disabled", false); $("#run, #explain, #csv, #json").prop("disabled", false);
$("#query_progress").hide(); $("#query_progress").hide();
return; return;
} }
@@ -299,7 +366,7 @@ function runExplain() {
explainQuery(query, function(data) { explainQuery(query, function(data) {
buildTable(data); buildTable(data);
$("#run, #explain, #csv").prop("disabled", false); $("#run, #explain, #csv, #json").prop("disabled", false);
$("#query_progress").hide(); $("#query_progress").hide();
$("#input").show(); $("#input").show();
$("#output").removeClass("full"); $("#output").removeClass("full");
@@ -307,17 +374,14 @@ function runExplain() {
}); });
} }
function exportToCSV() { function exportTo(format) {
var query = $.trim(editor.getValue()); var query = $.trim(editor.getValue());
if (query.length == 0) { if (query.length == 0) {
return; return;
} }
// Replace line breaks with spaces and properly encode query var url = "http://" + window.location.host + "/api/query?format=" + format + "&query=" + encodeQuery(query);
query = window.encodeURI(query.replace(/\n/g, " "));
var url = "http://" + window.location.host + "/api/query?format=csv&query=" + query;
var win = window.open(url, '_blank'); var win = window.open(url, '_blank');
setCurrentTab("table_query"); setCurrentTab("table_query");
@@ -325,11 +389,16 @@ function exportToCSV() {
} }
function initEditor() { function initEditor() {
var writeQueryTimeout = null;
editor = ace.edit("custom_query"); editor = ace.edit("custom_query");
editor.setFontSize(13);
editor.setTheme("ace/theme/tomorrow");
editor.setShowPrintMargin(false);
editor.getSession().setMode("ace/mode/pgsql"); editor.getSession().setMode("ace/mode/pgsql");
editor.getSession().setTabSize(2); editor.getSession().setTabSize(2);
editor.getSession().setUseSoftTabs(true); editor.getSession().setUseSoftTabs(true);
editor.commands.addCommands([{ editor.commands.addCommands([{
name: "run_query", name: "run_query",
bindKey: { bindKey: {
@@ -349,6 +418,22 @@ function initEditor() {
runExplain(); runExplain();
} }
}]); }]);
editor.on("change", function() {
if (writeQueryTimeout) {
clearTimeout(writeQueryTimeout);
}
writeQueryTimeout = setTimeout(function() {
localStorage.setItem("pgweb_query", editor.getValue());
}, 1000);
});
var query = localStorage.getItem("pgweb_query");
if (query && query.length > 0) {
editor.setValue(query);
editor.clearSelection();
}
} }
function addShortcutTooltips() { function addShortcutTooltips() {
@@ -403,7 +488,7 @@ function getConnectionString() {
var host = $("#pg_host").val(); var host = $("#pg_host").val();
var port = $("#pg_port").val(); var port = $("#pg_port").val();
var user = $("#pg_user").val(); var user = $("#pg_user").val();
var pass = $("#pg_password").val(); var pass = encodeURIComponent($("#pg_password").val());
var db = $("#pg_db").val(); var db = $("#pg_db").val();
if (port.length == 0) { if (port.length == 0) {
@@ -424,13 +509,14 @@ function getConnectionString() {
} }
$(document).ready(function() { $(document).ready(function() {
$("#table_content").on("click", function() { showTableContent(); }); $("#table_content").on("click", function() { showTableContent(); });
$("#table_structure").on("click", function() { showTableStructure(); }); $("#table_structure").on("click", function() { showTableStructure(); });
$("#table_indexes").on("click", function() { showTableIndexes(); }); $("#table_indexes").on("click", function() { showTableIndexes(); });
$("#table_history").on("click", function() { showQueryHistory(); }); $("#table_constraints").on("click", function() { showTableConstraints(); });
$("#table_query").on("click", function() { showQueryPanel(); }); $("#table_history").on("click", function() { showQueryHistory(); });
$("#table_connection").on("click", function() { showConnectionPanel(); }); $("#table_query").on("click", function() { showQueryPanel(); });
$("#table_activity").on("click", function() { showActivityPanel(); }); $("#table_connection").on("click", function() { showConnectionPanel(); });
$("#table_activity").on("click", function() { showActivityPanel(); });
$("#run").on("click", function() { $("#run").on("click", function() {
runQuery(); runQuery();
@@ -441,9 +527,13 @@ $(document).ready(function() {
}); });
$("#csv").on("click", function() { $("#csv").on("click", function() {
exportToCSV(); exportTo("csv");
}); });
$("#json").on("click", function() {
exportTo("json");
})
$("#results").on("click", "tr", function() { $("#results").on("click", "tr", function() {
$("#results tr.selected").removeClass(); $("#results tr.selected").removeClass();
$(this).addClass("selected"); $(this).addClass("selected");
@@ -489,6 +579,7 @@ $(document).ready(function() {
$("#tables").on("click", "li", function() { $("#tables").on("click", "li", function() {
$("#tables li.selected").removeClass("selected"); $("#tables li.selected").removeClass("selected");
$("#sequences li.selected").removeClass("selected");
$(this).addClass("selected"); $(this).addClass("selected");
$("#tables").attr("data-current", $.trim($(this).text())); $("#tables").attr("data-current", $.trim($(this).text()));
@@ -496,8 +587,31 @@ $(document).ready(function() {
showTableInfo(); showTableInfo();
}); });
$("#tables").contextmenu({
target: "#tables_context_menu",
scopes: "li",
onItem: function(context, e) {
var el = $(e.target);
var table = $.trim($(context[0]).text());
var action = el.data("action");
performTableAction(table, action, el);
}
});
$("#sequences").on("click", "li", function() {
$("#tables li.selected").removeClass("selected");
$("#sequences li.selected").removeClass("selected");
$(this).addClass("selected");
$("#tables").attr("data-current", $.trim($(this).text()));
showTableContent();
$(".table-information ul").hide();
});
$("#refresh_tables").on("click", function() { $("#refresh_tables").on("click", function() {
loadTables(); loadTables();
loadSequences();
}); });
$("#edit_connection").on("click", function() { $("#edit_connection").on("click", function() {
@@ -596,6 +710,7 @@ $(document).ready(function() {
else { else {
connected = true; connected = true;
loadTables(); loadTables();
loadSequences();
$("#connection_window").hide(); $("#connection_window").hide();
$("#current_database").text(resp.current_database); $("#current_database").text(resp.current_database);
@@ -615,6 +730,7 @@ $(document).ready(function() {
else { else {
connected = true; connected = true;
loadTables(); loadTables();
loadSequences();
$("#current_database").text(resp.current_database); $("#current_database").text(resp.current_database);
$("#main").show(); $("#main").show();
+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));
+1
View File
@@ -0,0 +1 @@
ace.define("ace/theme/tomorrow",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-tomorrow",t.cssText=".ace-tomorrow .ace_gutter {background: #f6f6f6;color: #4D4D4C}.ace-tomorrow .ace_print-margin {width: 1px;background: #f6f6f6}.ace-tomorrow {background-color: #FFFFFF;color: #4D4D4C}.ace-tomorrow .ace_cursor {color: #AEAFAD}.ace-tomorrow .ace_marker-layer .ace_selection {background: #D6D6D6}.ace-tomorrow.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px #FFFFFF;}.ace-tomorrow .ace_marker-layer .ace_step {background: rgb(255, 255, 0)}.ace-tomorrow .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid #D1D1D1}.ace-tomorrow .ace_marker-layer .ace_active-line {background: #EFEFEF}.ace-tomorrow .ace_gutter-active-line {background-color : #dcdcdc}.ace-tomorrow .ace_marker-layer .ace_selected-word {border: 1px solid #D6D6D6}.ace-tomorrow .ace_invisible {color: #D1D1D1}.ace-tomorrow .ace_keyword,.ace-tomorrow .ace_meta,.ace-tomorrow .ace_storage,.ace-tomorrow .ace_storage.ace_type,.ace-tomorrow .ace_support.ace_type {color: #8959A8}.ace-tomorrow .ace_keyword.ace_operator {color: #3E999F}.ace-tomorrow .ace_constant.ace_character,.ace-tomorrow .ace_constant.ace_language,.ace-tomorrow .ace_constant.ace_numeric,.ace-tomorrow .ace_keyword.ace_other.ace_unit,.ace-tomorrow .ace_support.ace_constant,.ace-tomorrow .ace_variable.ace_parameter {color: #F5871F}.ace-tomorrow .ace_constant.ace_other {color: #666969}.ace-tomorrow .ace_invalid {color: #FFFFFF;background-color: #C82829}.ace-tomorrow .ace_invalid.ace_deprecated {color: #FFFFFF;background-color: #8959A8}.ace-tomorrow .ace_fold {background-color: #4271AE;border-color: #4D4D4C}.ace-tomorrow .ace_entity.ace_name.ace_function,.ace-tomorrow .ace_support.ace_function,.ace-tomorrow .ace_variable {color: #4271AE}.ace-tomorrow .ace_support.ace_class,.ace-tomorrow .ace_support.ace_type {color: #C99E00}.ace-tomorrow .ace_heading,.ace-tomorrow .ace_markup.ace_heading,.ace-tomorrow .ace_string {color: #718C00}.ace-tomorrow .ace_entity.ace_name.ace_tag,.ace-tomorrow .ace_entity.ace_other.ace_attribute-name,.ace-tomorrow .ace_meta.ace_tag,.ace-tomorrow .ace_string.ace_regexp,.ace-tomorrow .ace_variable {color: #C82829}.ace-tomorrow .ace_comment {color: #8E908C}.ace-tomorrow .ace_indent-guide {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y}";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})