Merge with master

This commit is contained in:
Dan Sosedoff
2016-01-17 15:22:33 -06:00
10 changed files with 187 additions and 58 deletions

View File

@@ -1,12 +1,16 @@
sudo: required
language: go language: go
sudo: false
services:
- docker
addons: addons:
postgresql: "9.4" postgresql: "9.4"
go: go:
- 1.4.2 - 1.4.2
- 1.5.2 - 1.5.2
install: install:
- make setup - make setup
@@ -14,3 +18,4 @@ install:
script: script:
- make build - make build
- make test - make test
- PGHOST=127.0.0.1 ./script/test_all.sh

View File

@@ -907,6 +907,8 @@ CREATE FUNCTION "check_book_addition" () RETURNS opaque AS '
CREATE VIEW "stock_view" as SELECT stock.isbn, stock.retail, stock.stock FROM stock; CREATE VIEW "stock_view" as SELECT stock.isbn, stock.retail, stock.stock FROM stock;
CREATE MATERIALIZED VIEW "m_stock_view" as SELECT stock.isbn, stock.retail, stock.stock FROM stock;
-- --
-- TOC Entry ID 30 (OID 3628247) -- TOC Entry ID 30 (OID 3628247)
-- --

View File

@@ -158,7 +158,15 @@ func GetSchemas(c *gin.Context) {
} }
func GetTable(c *gin.Context) { func GetTable(c *gin.Context) {
res, err := DB(c).Table(c.Params.ByName("table")) var res *client.Result
var err error
if c.Request.FormValue("type") == "materialized_view" {
res, err = DB(c).MaterializedView(c.Params.ByName("table"))
} else {
res, err = DB(c).Table(c.Params.ByName("table"))
}
serveResult(res, err, c) serveResult(res, err, c)
} }

View File

@@ -141,6 +141,10 @@ func (client *Client) Table(table string) (*Result, error) {
return client.query(statements.PG_TABLE_SCHEMA, schema, table) return client.query(statements.PG_TABLE_SCHEMA, schema, table)
} }
func (client *Client) MaterializedView(name string) (*Result, error) {
return client.query(statements.PG_MATERIALIZED_VIEW_SCHEMA, name)
}
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) { func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
schema, table := getSchemaAndTable(table) schema, table := getSchemaAndTable(table)
sql := fmt.Sprintf(`SELECT * FROM "%s"."%s"`, schema, table) sql := fmt.Sprintf(`SELECT * FROM "%s"."%s"`, schema, table)
@@ -235,7 +239,10 @@ func (client *Client) query(query string, args ...interface{}) (*Result, error)
return nil, err return nil, err
} }
result := Result{Columns: cols} result := Result{
Columns: cols,
Rows: []Row{},
}
for rows.Next() { for rows.Next() {
obj, err := rows.SliceScan() obj, err := rows.SliceScan()

View File

@@ -13,6 +13,11 @@ import (
var ( var (
testClient *Client testClient *Client
testCommands map[string]string testCommands map[string]string
serverHost string
serverPort string
serverUser string
serverPassword string
serverDatabase string
) )
func mapKeys(data map[string]*Objects) []string { func mapKeys(data map[string]*Objects) []string {
@@ -23,6 +28,28 @@ func mapKeys(data map[string]*Objects) []string {
return result return result
} }
func pgVersion() (int, int) {
var major, minor int
fmt.Sscanf(os.Getenv("PGVERSION"), "%d.%d", &major, &minor)
return major, minor
}
func getVar(name, def string) string {
val := os.Getenv(name)
if val == "" {
return def
}
return val
}
func initVars() {
serverHost = getVar("PGHOST", "localhost")
serverPort = getVar("PGPORT", "5432")
serverUser = getVar("PGUSER", "postgres")
serverPassword = getVar("PGPASSWORD", "postgres")
serverDatabase = getVar("PGDATABASE", "booktown")
}
func setupCommands() { func setupCommands() {
testCommands = map[string]string{ testCommands = map[string]string{
"createdb": "createdb", "createdb": "createdb",
@@ -42,7 +69,13 @@ func onWindows() bool {
} }
func setup() { func setup() {
out, err := exec.Command(testCommands["createdb"], "-U", "postgres", "-h", "localhost", "booktown").CombinedOutput() out, err := exec.Command(
testCommands["createdb"],
"-U", serverUser,
"-h", serverHost,
"-p", serverPort,
serverDatabase,
).CombinedOutput()
if err != nil { if err != nil {
fmt.Println("Database creation failed:", string(out)) fmt.Println("Database creation failed:", string(out))
@@ -50,7 +83,14 @@ func setup() {
os.Exit(1) os.Exit(1)
} }
out, err = exec.Command(testCommands["psql"], "-U", "postgres", "-h", "localhost", "-f", "../../data/booktown.sql", "booktown").CombinedOutput() out, err = exec.Command(
testCommands["psql"],
"-U", serverUser,
"-h", serverHost,
"-p", serverPort,
"-f", "../../data/booktown.sql",
serverDatabase,
).CombinedOutput()
if err != nil { if err != nil {
fmt.Println("Database import failed:", string(out)) fmt.Println("Database import failed:", string(out))
@@ -60,7 +100,8 @@ func setup() {
} }
func setupClient() { func setupClient() {
testClient, _ = NewFromUrl("postgres://postgres@localhost/booktown?sslmode=disable", nil) url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
testClient, _ = NewFromUrl(url, nil)
} }
func teardownClient() { func teardownClient() {
@@ -70,7 +111,13 @@ func teardownClient() {
} }
func teardown() { func teardown() {
_, err := exec.Command(testCommands["dropdb"], "-U", "postgres", "-h", "localhost", "booktown").CombinedOutput() _, err := exec.Command(
testCommands["dropdb"],
"-U", serverUser,
"-h", serverHost,
"-p", serverPort,
serverDatabase,
).CombinedOutput()
if err != nil { if err != nil {
fmt.Println("Teardown error:", err) fmt.Println("Teardown error:", err)
@@ -78,7 +125,7 @@ func teardown() {
} }
func test_NewClientFromUrl(t *testing.T) { func test_NewClientFromUrl(t *testing.T) {
url := "postgres://postgres@localhost/booktown?sslmode=disable" url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
client, err := NewFromUrl(url, nil) client, err := NewFromUrl(url, nil)
if err != nil { if err != nil {
@@ -90,7 +137,7 @@ func test_NewClientFromUrl(t *testing.T) {
} }
func test_NewClientFromUrl2(t *testing.T) { func test_NewClientFromUrl2(t *testing.T) {
url := "postgresql://postgres@localhost/booktown?sslmode=disable" url := fmt.Sprintf("postgresql://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
client, err := NewFromUrl(url, nil) client, err := NewFromUrl(url, nil)
if err != nil { if err != nil {
@@ -156,6 +203,13 @@ func test_Objects(t *testing.T) {
assert.Equal(t, tables, objects["public"].Tables) assert.Equal(t, tables, objects["public"].Tables)
assert.Equal(t, []string{"recent_shipments", "stock_view"}, objects["public"].Views) assert.Equal(t, []string{"recent_shipments", "stock_view"}, objects["public"].Views)
assert.Equal(t, []string{"author_ids", "book_ids", "shipments_ship_id_seq", "subject_ids"}, objects["public"].Sequences) assert.Equal(t, []string{"author_ids", "book_ids", "shipments_ship_id_seq", "subject_ids"}, objects["public"].Sequences)
major, minor := pgVersion()
if minor == 0 || minor >= 3 {
assert.Equal(t, []string{"m_stock_view"}, objects["public"].MaterializedViews)
} else {
t.Logf("Skipping materialized view on %d.%d\n", major, minor)
}
} }
func test_Table(t *testing.T) { func test_Table(t *testing.T) {
@@ -257,7 +311,8 @@ func test_HistoryError(t *testing.T) {
} }
func test_HistoryUniqueness(t *testing.T) { func test_HistoryUniqueness(t *testing.T) {
client, _ := NewFromUrl("postgres://postgres@localhost/booktown?sslmode=disable", nil) url := fmt.Sprintf("postgres://%s@%s:%s/%s?sslmode=disable", serverUser, serverHost, serverPort, serverDatabase)
client, _ := NewFromUrl(url, nil)
client.Query("SELECT * FROM books WHERE id = 1") client.Query("SELECT * FROM books WHERE id = 1")
client.Query("SELECT * FROM books WHERE id = 1") client.Query("SELECT * FROM books WHERE id = 1")
@@ -272,6 +327,7 @@ func TestAll(t *testing.T) {
return return
} }
initVars()
setupCommands() setupCommands()
teardown() teardown()
setup() setup()

View File

@@ -25,9 +25,10 @@ type Result struct {
} }
type Objects struct { type Objects struct {
Tables []string `json:"tables"` Tables []string `json:"table"`
Views []string `json:"views"` Views []string `json:"view"`
Sequences []string `json:"sequences"` MaterializedViews []string `json:"materialized_view"`
Sequences []string `json:"sequence"`
} }
// Due to big int number limitations in javascript, numbers should be encoded // Due to big int number limitations in javascript, numbers should be encoded
@@ -117,6 +118,7 @@ func ObjectsFromResult(res *Result) map[string]*Objects {
objects[schema] = &Objects{ objects[schema] = &Objects{
Tables: []string{}, Tables: []string{},
Views: []string{}, Views: []string{},
MaterializedViews: []string{},
Sequences: []string{}, Sequences: []string{},
} }
} }
@@ -126,6 +128,8 @@ func ObjectsFromResult(res *Result) map[string]*Objects {
objects[schema].Tables = append(objects[schema].Tables, name) objects[schema].Tables = append(objects[schema].Tables, name)
case "view": case "view":
objects[schema].Views = append(objects[schema].Views, name) objects[schema].Views = append(objects[schema].Views, name)
case "materialized_view":
objects[schema].MaterializedViews = append(objects[schema].MaterializedViews, name)
case "sequence": case "sequence":
objects[schema].Sequences = append(objects[schema].Sequences, name) objects[schema].Sequences = append(objects[schema].Sequences, name)
} }

File diff suppressed because one or more lines are too long

View File

@@ -92,6 +92,23 @@ WHERE
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
PG_MATERIALIZED_VIEW_SCHEMA = `
SELECT
attname as column_name,
atttypid::regtype AS data_type,
(case when attnotnull IS TRUE then 'NO' else 'YES' end) as is_nullable,
null as character_maximum_length,
null as character_set_catalog,
null as column_default
FROM
pg_attribute
WHERE
attrelid = $1::regclass AND
attnum > 0 AND
NOT attisdropped`
// ---------------------------------------------------------------------------
PG_ACTIVITY = ` PG_ACTIVITY = `
SELECT SELECT
datname, datname,
@@ -130,7 +147,7 @@ FROM
LEFT JOIN LEFT JOIN
pg_catalog.pg_namespace n ON n.oid = c.relnamespace pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE WHERE
c.relkind IN ('r','v','S','s','') AND c.relkind IN ('r','v','m','S','s','') AND
n.nspname !~ '^pg_toast' AND n.nspname !~ '^pg_toast' AND
n.nspname NOT IN ('information_schema', 'pg_catalog') n.nspname NOT IN ('information_schema', 'pg_catalog')
ORDER BY 1, 2` ORDER BY 1, 2`

20
script/test_all.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -e
export PGHOST=${PGHOST:-192.168.99.100}
export PGUSER="postgres"
export PGPASSWORD=""
export PGDATABASE="booktown"
export PGPORT="15432"
for i in {1..5}
do
export PGVERSION="9.$i"
echo "Running tests against PostgreSQL v$PGVERSION"
docker rm -f postgres || true
docker run -p $PGPORT:5432 --name postgres -e POSTGRES_PASSWORD=$PGPASSWORD -d postgres:$PGVERSION
sleep 5
make test
echo "----------"
done

View File

@@ -2,7 +2,7 @@ var editor = null;
var connected = false; var connected = false;
var bookmarks = {}; var bookmarks = {};
var default_rows_limit = 100; var default_rows_limit = 100;
var currentTable = null; var currentObject = null;
var filterOptions = { var filterOptions = {
"equal": "= 'DATA'", "equal": "= 'DATA'",
@@ -23,11 +23,11 @@ function guid() {
} }
function getSessionId() { function getSessionId() {
var id = localStorage.getItem("session_id"); var id = sessionStorage.getItem("session_id");
if (!id) { if (!id) {
id = guid(); id = guid();
localStorage.setItem("session_id", id); sessionStorage.setItem("session_id", id);
} }
return id; return id;
@@ -79,7 +79,7 @@ function apiCall(method, path, params, cb) {
function getObjects(cb) { apiCall("get", "/objects", {}, cb); } function getObjects(cb) { apiCall("get", "/objects", {}, 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, opts, cb) { apiCall("get", "/tables/" + table, opts, cb); }
function getTableIndexes(table, cb) { apiCall("get", "/tables/" + table + "/indexes", {}, cb); } function getTableIndexes(table, cb) { apiCall("get", "/tables/" + table + "/indexes", {}, cb); }
function getTableConstraints(table, cb) { apiCall("get", "/tables/" + table + "/constraints", {}, cb); } function getTableConstraints(table, cb) { apiCall("get", "/tables/" + table + "/constraints", {}, cb); }
function getHistory(cb) { apiCall("get", "/history", {}, cb); } function getHistory(cb) { apiCall("get", "/history", {}, cb); }
@@ -95,15 +95,17 @@ function buildSchemaSection(name, objects) {
var section = ""; var section = "";
var titles = { var titles = {
"tables": "Tables", "table": "Tables",
"views": "Views", "view": "Views",
"sequences": "Sequences" "materialized_view": "Materialized Views",
"sequence": "Sequences"
}; };
var icons = { var icons = {
"tables": '<i class="fa fa-table"></i>', "table": '<i class="fa fa-table"></i>',
"views": '<i class="fa fa-table"></i>', "view": '<i class="fa fa-table"></i>',
"sequences": '<i class="fa fa-circle-o"></i>' "materialized_view": '<i class="fa fa-table"></i>',
"sequence": '<i class="fa fa-circle-o"></i>'
}; };
var klass = ""; var klass = "";
@@ -113,11 +115,11 @@ function buildSchemaSection(name, objects) {
section += "<div class='schema-name'><i class='fa fa-folder-o'></i><i class='fa fa-folder-open-o'></i> " + name + "</div>"; section += "<div class='schema-name'><i class='fa fa-folder-o'></i><i class='fa fa-folder-open-o'></i> " + name + "</div>";
section += "<div class='schema-container'>"; section += "<div class='schema-container'>";
for (group of ["tables", "views", "sequences"]) { for (group of ["table", "view", "materialized_view", "sequence"]) {
if (objects[group].length == 0) continue; if (objects[group].length == 0) continue;
group_klass = ""; group_klass = "";
if (name == "public" && group == "tables") group_klass = "expanded"; if (name == "public" && group == "table") group_klass = "expanded";
section += "<div class='schema-group " + group_klass + "'>"; section += "<div class='schema-group " + group_klass + "'>";
section += "<div class='schema-group-title'><i class='fa fa-chevron-right'></i><i class='fa fa-chevron-down'></i> " + titles[group] + " (" + objects[group].length + ")</div>"; section += "<div class='schema-group-title'><i class='fa fa-chevron-right'></i><i class='fa fa-chevron-down'></i> " + titles[group] + " (" + objects[group].length + ")</div>";
@@ -165,8 +167,8 @@ function unescapeHtml(str){
return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue; return e.childNodes.length === 0 ? "" : e.childNodes[0].nodeValue;
} }
function getCurrentTable() { function getCurrentObject() {
return currentTable; return currentObject || { name: "", type: "" };
} }
function resetTable() { function resetTable() {
@@ -278,7 +280,7 @@ function showQueryHistory() {
} }
function showTableIndexes() { function showTableIndexes() {
var name = getCurrentTable(); var name = getCurrentObject().name;
if (name.length == 0) { if (name.length == 0) {
alert("Please select a table!"); alert("Please select a table!");
@@ -296,7 +298,7 @@ function showTableIndexes() {
} }
function showTableConstraints() { function showTableConstraints() {
var name = getCurrentTable(); var name = getCurrentObject().name;
if (name.length == 0) { if (name.length == 0) {
alert("Please select a table!"); alert("Please select a table!");
@@ -314,7 +316,7 @@ function showTableConstraints() {
} }
function showTableInfo() { function showTableInfo() {
var name = getCurrentTable(); var name = getCurrentObject().name;
if (name.length == 0) { if (name.length == 0) {
alert("Please select a table!"); alert("Please select a table!");
@@ -330,7 +332,7 @@ function showTableInfo() {
$("#table_encoding").text("Unknown"); $("#table_encoding").text("Unknown");
}); });
buildTableFilters(name); buildTableFilters(name, getCurrentObject().type);
} }
function updatePaginator(pagination) { function updatePaginator(pagination) {
@@ -365,7 +367,7 @@ function updatePaginator(pagination) {
} }
function showTableContent(sortColumn, sortOrder) { function showTableContent(sortColumn, sortOrder) {
var name = getCurrentTable(); var name = getCurrentObject().name;
if (name.length == 0) { if (name.length == 0) {
alert("Please select a table!"); alert("Please select a table!");
@@ -407,7 +409,7 @@ function showTableContent(sortColumn, sortOrder) {
} }
function showTableStructure() { function showTableStructure() {
var name = getCurrentTable(); var name = getCurrentObject().name;
if (name.length == 0) { if (name.length == 0) {
alert("Please select a table!"); alert("Please select a table!");
@@ -419,7 +421,9 @@ function showTableStructure() {
$("#input").hide(); $("#input").hide();
$("#body").prop("class", "full"); $("#body").prop("class", "full");
getTableStructure(name, function(data) { console.log(getCurrentObject());
getTableStructure(name, { type: getCurrentObject().type }, function(data) {
buildTable(data); buildTable(data);
$("#results").addClass("no-crop"); $("#results").addClass("no-crop");
}); });
@@ -537,11 +541,14 @@ function exportTo(format) {
win.focus(); win.focus();
} }
function buildTableFilters(name) { function buildTableFilters(name, type) {
getTableStructure(name, function(data) { getTableStructure(name, { type: type }, function(data) {
if (data.rows.length == 0) { if (data.rows.length == 0) {
$("#pagination .filters").hide(); $("#pagination .filters").hide();
} }
else {
$("#pagination .filters").show();
}
$("#pagination select.column").html("<option value='' selected>Select column</option>"); $("#pagination select.column").html("<option value='' selected>Select column</option>");
@@ -731,7 +738,10 @@ $(document).ready(function() {
}); });
$("#objects").on("click", "li", function(e) { $("#objects").on("click", "li", function(e) {
currentTable = $(this).data("id"); currentObject = {
name: $(this).data("id"),
type: $(this).data("type")
};
$("#objects li").removeClass("active"); $("#objects li").removeClass("active");
$(this).addClass("active"); $(this).addClass("active");