2020-11-19 15:04:42 +13:00
|
|
|
package dbutil
|
2015-12-01 09:43:35 -08:00
|
|
|
|
|
|
|
|
import (
|
2018-01-22 20:38:40 -08:00
|
|
|
"bufio"
|
|
|
|
|
"bytes"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"errors"
|
2015-12-01 09:43:35 -08:00
|
|
|
"io"
|
|
|
|
|
"net/url"
|
2018-01-22 20:38:40 -08:00
|
|
|
"os/exec"
|
|
|
|
|
"strings"
|
|
|
|
|
"unicode"
|
2015-12-01 09:43:35 -08:00
|
|
|
)
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// Transaction can represent a database or open transaction
|
|
|
|
|
type Transaction interface {
|
|
|
|
|
Exec(query string, args ...interface{}) (sql.Result, error)
|
|
|
|
|
Query(query string, args ...interface{}) (*sql.Rows, error)
|
|
|
|
|
QueryRow(query string, args ...interface{}) *sql.Row
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DatabaseName returns the database name from a URL
|
|
|
|
|
func DatabaseName(u *url.URL) string {
|
2015-12-01 09:43:35 -08:00
|
|
|
name := u.Path
|
|
|
|
|
if len(name) > 0 && name[:1] == "/" {
|
2017-05-14 12:10:45 -07:00
|
|
|
name = name[1:]
|
2015-12-01 09:43:35 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// MustClose ensures a stream is closed
|
|
|
|
|
func MustClose(c io.Closer) {
|
2015-12-01 09:43:35 -08:00
|
|
|
if err := c.Close(); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-22 20:38:40 -08:00
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// RunCommand runs a command and returns the stdout if successful
|
|
|
|
|
func RunCommand(name string, args ...string) ([]byte, error) {
|
2018-01-22 20:38:40 -08:00
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
|
cmd := exec.Command(name, args...)
|
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
|
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
// return stderr if available
|
|
|
|
|
if s := strings.TrimSpace(stderr.String()); s != "" {
|
|
|
|
|
return nil, errors.New(s)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// otherwise return error
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// return stdout
|
|
|
|
|
return stdout.Bytes(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// TrimLeadingSQLComments removes sql comments and blank lines from the beginning of text
|
2018-01-22 20:38:40 -08:00
|
|
|
// generally when performing sql dumps these contain host-specific information such as
|
|
|
|
|
// client/server version numbers
|
2020-11-19 15:04:42 +13:00
|
|
|
func TrimLeadingSQLComments(data []byte) ([]byte, error) {
|
2018-01-22 20:38:40 -08:00
|
|
|
// create decent size buffer
|
|
|
|
|
out := bytes.NewBuffer(make([]byte, 0, len(data)))
|
|
|
|
|
|
|
|
|
|
// iterate over sql lines
|
|
|
|
|
preamble := true
|
|
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(data))
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
|
// we read bytes directly for premature performance optimization
|
|
|
|
|
line := scanner.Bytes()
|
|
|
|
|
|
|
|
|
|
if preamble && (len(line) == 0 || bytes.Equal(line[0:2], []byte("--"))) {
|
|
|
|
|
// header section, skip this line in output buffer
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// header section is over
|
|
|
|
|
preamble = false
|
|
|
|
|
|
|
|
|
|
// trim trailing whitespace
|
|
|
|
|
line = bytes.TrimRightFunc(line, unicode.IsSpace)
|
|
|
|
|
|
|
|
|
|
// copy bytes to output buffer
|
|
|
|
|
if _, err := out.Write(line); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if _, err := out.WriteString("\n"); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return out.Bytes(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// QueryColumn runs a SQL statement and returns a slice of strings
|
2018-01-22 20:38:40 -08:00
|
|
|
// it is assumed that the statement returns only one column
|
|
|
|
|
// e.g. schema_migrations table
|
2020-11-19 15:04:42 +13:00
|
|
|
func QueryColumn(db Transaction, query string, args ...interface{}) ([]string, error) {
|
2020-11-17 18:11:24 +13:00
|
|
|
rows, err := db.Query(query, args...)
|
2018-01-22 20:38:40 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2020-11-19 15:04:42 +13:00
|
|
|
defer MustClose(rows)
|
2018-01-22 20:38:40 -08:00
|
|
|
|
|
|
|
|
// read into slice
|
|
|
|
|
var result []string
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var v string
|
|
|
|
|
if err := rows.Scan(&v); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result = append(result, v)
|
|
|
|
|
}
|
|
|
|
|
if err = rows.Err(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
2020-06-25 12:26:09 -07:00
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// QueryValue runs a SQL statement and returns a single string
|
2020-11-01 13:30:35 +13:00
|
|
|
// it is assumed that the statement returns only one row and one column
|
|
|
|
|
// sql NULL is returned as empty string
|
2020-11-19 15:04:42 +13:00
|
|
|
func QueryValue(db Transaction, query string, args ...interface{}) (string, error) {
|
2020-11-01 13:30:35 +13:00
|
|
|
var result sql.NullString
|
2020-11-17 18:11:24 +13:00
|
|
|
err := db.QueryRow(query, args...).Scan(&result)
|
2020-11-01 13:30:35 +13:00
|
|
|
if err != nil || !result.Valid {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.String, nil
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// MustParseURL parses a URL from string, and panics if it fails.
|
|
|
|
|
// It is used during testing and in cases where we are parsing a generated URL.
|
|
|
|
|
func MustParseURL(s string) *url.URL {
|
|
|
|
|
if s == "" {
|
|
|
|
|
panic("missing url")
|
2020-06-25 12:26:09 -07:00
|
|
|
}
|
2020-11-19 15:04:42 +13:00
|
|
|
|
|
|
|
|
u, err := url.Parse(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
2020-06-25 12:26:09 -07:00
|
|
|
}
|
2020-11-19 15:04:42 +13:00
|
|
|
|
|
|
|
|
return u
|
2020-06-25 12:26:09 -07:00
|
|
|
}
|