mirror of
https://github.com/TECHNOFAB11/dbmate.git
synced 2025-12-11 23:50:04 +01:00
Initial commit
This commit is contained in:
commit
9cfc758ca1
14 changed files with 697 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
20
Dockerfile
Normal file
20
Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
FROM alpine:3.2
|
||||||
|
|
||||||
|
ENV GOPATH /go
|
||||||
|
ENV PATH /go/bin:$PATH
|
||||||
|
|
||||||
|
# install build dependencies
|
||||||
|
RUN apk add -U --no-progress go git ca-certificates
|
||||||
|
RUN go get \
|
||||||
|
github.com/golang/lint/golint \
|
||||||
|
golang.org/x/tools/cmd/vet
|
||||||
|
|
||||||
|
# copy source files
|
||||||
|
COPY . /go/src/github.com/adrianmacneil/dbmate
|
||||||
|
WORKDIR /go/src/github.com/adrianmacneil/dbmate
|
||||||
|
|
||||||
|
# build
|
||||||
|
RUN go get -d -t
|
||||||
|
RUN go install -v
|
||||||
|
|
||||||
|
CMD dbmate
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Adrian Macneil
|
||||||
|
|
||||||
|
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 the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
13
Makefile
Normal file
13
Makefile
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
DOCKER := docker-compose run dbmate
|
||||||
|
|
||||||
|
all: build lint test
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
lint:
|
||||||
|
$(DOCKER) golint ./...
|
||||||
|
$(DOCKER) go vet ./...
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(DOCKER) go test ./...
|
||||||
25
README.md
Normal file
25
README.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Dbmate
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Dbmate is currently under development. To install the latest build, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go get -u github.com/adrianmacneil/dbmate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Tests are run with docker-compose. First, install the [Docker Toolbox](https://www.docker.com/docker-toolbox).
|
||||||
|
|
||||||
|
Make sure you have docker running:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ docker-machine start default && eval "$(docker-machine env default)"
|
||||||
|
```
|
||||||
|
|
||||||
|
To build a docker image and run the tests:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ make
|
||||||
|
```
|
||||||
242
commands.go
Normal file
242
commands.go
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/adrianmacneil/dbmate/driver"
|
||||||
|
"github.com/adrianmacneil/dbmate/driver/shared"
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateCommand creates the current database
|
||||||
|
func CreateCommand(ctx *cli.Context) error {
|
||||||
|
u, err := GetDatabaseURL()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
drv, err := driver.Get(u.Scheme)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return drv.CreateDatabase(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropCommand drops the current database (if it exists)
|
||||||
|
func DropCommand(ctx *cli.Context) error {
|
||||||
|
u, err := GetDatabaseURL()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
drv, err := driver.Get(u.Scheme)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return drv.DropDatabase(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
||||||
|
|
||||||
|
// NewCommand creates a new migration file
|
||||||
|
func NewCommand(ctx *cli.Context) error {
|
||||||
|
// new migration name
|
||||||
|
timestamp := time.Now().UTC().Format("20060102150405")
|
||||||
|
name := ctx.Args().First()
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("Please specify a name for the new migration.")
|
||||||
|
}
|
||||||
|
name = fmt.Sprintf("%s_%s.sql", timestamp, name)
|
||||||
|
|
||||||
|
// create migrations dir if missing
|
||||||
|
migrationsDir := ctx.GlobalString("migrations-dir")
|
||||||
|
if err := os.MkdirAll(migrationsDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("Unable to create directory `%s`.", migrationsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check file does not already exist
|
||||||
|
path := filepath.Join(migrationsDir, name)
|
||||||
|
fmt.Printf("Creating migration: %s\n", path)
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("File already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// write new migration
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
_, err = file.WriteString(migrationTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatabaseURL returns the current environment database url
|
||||||
|
func GetDatabaseURL() (u *url.URL, err error) {
|
||||||
|
return url.Parse(os.Getenv("DATABASE_URL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTransaction(db *sql.DB, txFunc func(shared.Transaction) error) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := txFunc(tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpCommand migrates database to the latest version
|
||||||
|
func UpCommand(ctx *cli.Context) error {
|
||||||
|
migrationsDir := ctx.GlobalString("migrations-dir")
|
||||||
|
available, err := findAvailableMigrations(migrationsDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(available) == 0 {
|
||||||
|
return fmt.Errorf("No migration files found.")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := GetDatabaseURL()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
drv, err := driver.Get(u.Scheme)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := drv.Open(u)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err := drv.CreateMigrationsTable(db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
applied, err := drv.SelectMigrations(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for filename := range available {
|
||||||
|
ver := migrationVersion(filename)
|
||||||
|
if _, ok := applied[ver]; ok {
|
||||||
|
// migration already applied
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Applying: %s\n", filename)
|
||||||
|
|
||||||
|
migration, err := parseMigration(filepath.Join(migrationsDir, filename))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// begin transaction
|
||||||
|
doTransaction(db, func(tx shared.Transaction) error {
|
||||||
|
// run actual migration
|
||||||
|
if _, err := tx.Exec(migration["up"]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// record migration
|
||||||
|
if err := drv.InsertMigration(tx, ver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAvailableMigrations(dir string) (map[string]struct{}, error) {
|
||||||
|
files, err := ioutil.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Could not find migrations directory `%s`.", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
nameRegexp := regexp.MustCompile(`^\d.*\.sql$`)
|
||||||
|
migrations := map[string]struct{}{}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := file.Name()
|
||||||
|
if !nameRegexp.MatchString(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations[name] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrationVersion(filename string) string {
|
||||||
|
return regexp.MustCompile(`^\d+`).FindString(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMigration reads a migration file into a map with up/down keys
|
||||||
|
// implementation is similar to regexp.Split()
|
||||||
|
func parseMigration(path string) (map[string]string, error) {
|
||||||
|
// read migration file into string
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
contents := string(data)
|
||||||
|
|
||||||
|
// split string on our trigger comment
|
||||||
|
separatorRegexp := regexp.MustCompile(`(?m)^-- migrate:(.*)$`)
|
||||||
|
matches := separatorRegexp.FindAllStringSubmatchIndex(contents, -1)
|
||||||
|
|
||||||
|
migrations := map[string]string{}
|
||||||
|
direction := ""
|
||||||
|
beg := 0
|
||||||
|
end := 0
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
end = match[0]
|
||||||
|
if direction != "" {
|
||||||
|
// write previous direction to output map
|
||||||
|
migrations[direction] = contents[beg:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// each match records the start of a new direction
|
||||||
|
direction = contents[match[2]:match[3]]
|
||||||
|
beg = match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// write final direction to output map
|
||||||
|
migrations[direction] = contents[beg:]
|
||||||
|
|
||||||
|
return migrations, nil
|
||||||
|
}
|
||||||
19
commands_test.go
Normal file
19
commands_test.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/adrianmacneil/dbmate"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetDatabaseUrl(t *testing.T) {
|
||||||
|
os.Setenv("DATABASE_URL", "postgres://example.org/db")
|
||||||
|
|
||||||
|
u, err := main.GetDatabaseURL()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, "postgres", u.Scheme)
|
||||||
|
require.Equal(t, "example.org", u.Host)
|
||||||
|
require.Equal(t, "/db", u.Path)
|
||||||
|
}
|
||||||
8
docker-compose.yml
Normal file
8
docker-compose.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
postgres:
|
||||||
|
image: postgres:9.4
|
||||||
|
dbmate:
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- .:/go/src/github.com/adrianmacneil/dbmate
|
||||||
|
links:
|
||||||
|
- postgres
|
||||||
30
driver/driver.go
Normal file
30
driver/driver.go
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
package driver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/adrianmacneil/dbmate/driver/postgres"
|
||||||
|
"github.com/adrianmacneil/dbmate/driver/shared"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver provides top level database functions
|
||||||
|
type Driver interface {
|
||||||
|
Open(*url.URL) (*sql.DB, error)
|
||||||
|
CreateDatabase(*url.URL) error
|
||||||
|
DropDatabase(*url.URL) error
|
||||||
|
CreateMigrationsTable(*sql.DB) error
|
||||||
|
SelectMigrations(*sql.DB) (map[string]struct{}, error)
|
||||||
|
InsertMigration(shared.Transaction, string) error
|
||||||
|
DeleteMigration(shared.Transaction, string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get loads a database driver by name
|
||||||
|
func Get(name string) (Driver, error) {
|
||||||
|
switch name {
|
||||||
|
case "postgres":
|
||||||
|
return postgres.Driver{}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unknown driver: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
103
driver/postgres/postgres.go
Normal file
103
driver/postgres/postgres.go
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
package postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/adrianmacneil/dbmate/driver/shared"
|
||||||
|
pq "github.com/lib/pq"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver provides top level database functions
|
||||||
|
type Driver struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open creates a new database connection
|
||||||
|
func (postgres Driver) Open(u *url.URL) (*sql.DB, error) {
|
||||||
|
return sql.Open("postgres", u.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// postgresExec runs a sql statement on the "postgres" database
|
||||||
|
func (postgres Driver) postgresExec(u *url.URL, statement string) error {
|
||||||
|
// connect to postgres database
|
||||||
|
postgresURL := *u
|
||||||
|
postgresURL.Path = "postgres"
|
||||||
|
|
||||||
|
db, err := postgres.Open(&postgresURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// run statement
|
||||||
|
_, err = db.Exec(statement)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateDatabase creates the specified database
|
||||||
|
func (postgres Driver) CreateDatabase(u *url.URL) error {
|
||||||
|
database := shared.DatabaseName(u)
|
||||||
|
fmt.Printf("Creating: %s\n", database)
|
||||||
|
|
||||||
|
return postgres.postgresExec(u, fmt.Sprintf("CREATE DATABASE %s",
|
||||||
|
pq.QuoteIdentifier(database)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropDatabase drops the specified database (if it exists)
|
||||||
|
func (postgres Driver) DropDatabase(u *url.URL) error {
|
||||||
|
database := shared.DatabaseName(u)
|
||||||
|
fmt.Printf("Dropping: %s\n", database)
|
||||||
|
|
||||||
|
return postgres.postgresExec(u, fmt.Sprintf("DROP DATABASE IF EXISTS %s",
|
||||||
|
pq.QuoteIdentifier(database)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasMigrationsTable returns true if the schema_migrations table exists
|
||||||
|
func (postgres Driver) HasMigrationsTable(db *sql.DB) (bool, error) {
|
||||||
|
return false, fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMigrationsTable creates the schema_migrations table
|
||||||
|
func (postgres Driver) CreateMigrationsTable(db *sql.DB) error {
|
||||||
|
_, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version varchar(255) PRIMARY KEY)`)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectMigrations returns a list of applied migrations
|
||||||
|
func (postgres Driver) SelectMigrations(db *sql.DB) (map[string]struct{}, error) {
|
||||||
|
rows, err := db.Query("SELECT version FROM schema_migrations")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
migrations := map[string]struct{}{}
|
||||||
|
for rows.Next() {
|
||||||
|
var version string
|
||||||
|
if err := rows.Scan(&version); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations[version] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertMigration adds a new migration record
|
||||||
|
func (postgres Driver) InsertMigration(db shared.Transaction, version string) error {
|
||||||
|
_, err := db.Exec("INSERT INTO schema_migrations (version) VALUES ($1)", version)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMigration removes a migration record
|
||||||
|
func (postgres Driver) DeleteMigration(db shared.Transaction, version string) error {
|
||||||
|
_, err := db.Exec("DELETE FROM schema_migrations WHERE version = $1", version)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
63
driver/postgres/postgres_test.go
Normal file
63
driver/postgres/postgres_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package postgres_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"github.com/adrianmacneil/dbmate/driver/postgres"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testURL(t *testing.T) *url.URL {
|
||||||
|
str := os.Getenv("POSTGRES_PORT")
|
||||||
|
require.NotEmpty(t, str)
|
||||||
|
|
||||||
|
u, err := url.Parse(str)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
u.Scheme = "postgres"
|
||||||
|
u.User = url.User("postgres")
|
||||||
|
u.Path = "/dbmate"
|
||||||
|
u.RawQuery = "sslmode=disable"
|
||||||
|
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateDropDatabase(t *testing.T) {
|
||||||
|
d := postgres.Driver{}
|
||||||
|
u := testURL(t)
|
||||||
|
|
||||||
|
// drop any existing database
|
||||||
|
err := d.DropDatabase(u)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// create database
|
||||||
|
err = d.CreateDatabase(u)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// check that database exists and we can connect to it
|
||||||
|
func() {
|
||||||
|
db, err := sql.Open("postgres", u.String())
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
require.Nil(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// drop the database
|
||||||
|
err = d.DropDatabase(u)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// check that database no longer exists
|
||||||
|
func() {
|
||||||
|
db, err := sql.Open("postgres", u.String())
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.Equal(t, "pq: database \"dbmate\" does not exist", err.Error())
|
||||||
|
}()
|
||||||
|
}
|
||||||
21
driver/shared/shared.go
Normal file
21
driver/shared/shared.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatabaseName returns the database name from a URL
|
||||||
|
func DatabaseName(u *url.URL) string {
|
||||||
|
name := u.Path
|
||||||
|
if len(name) > 0 && name[:1] == "/" {
|
||||||
|
name = name[1:len(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction can represent a database or open transaction
|
||||||
|
type Transaction interface {
|
||||||
|
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||||
|
}
|
||||||
24
driver/shared/shared_test.go
Normal file
24
driver/shared/shared_test.go
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
package shared_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/adrianmacneil/dbmate/driver/shared"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDatabaseName(t *testing.T) {
|
||||||
|
u, err := url.Parse("ignore://localhost/foo?query")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
name := shared.DatabaseName(u)
|
||||||
|
require.Equal(t, "foo", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDatabaseName_Empty(t *testing.T) {
|
||||||
|
u, err := url.Parse("ignore://localhost")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
name := shared.DatabaseName(u)
|
||||||
|
require.Equal(t, "", name)
|
||||||
|
}
|
||||||
84
main.go
Normal file
84
main.go
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/codegangsta/cli"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
loadDotEnv()
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "dbmate"
|
||||||
|
app.Usage = "A lightweight, framework-independent database migration tool."
|
||||||
|
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "migrations-dir, d",
|
||||||
|
Value: "./db/migrations",
|
||||||
|
Usage: "specify the directory containing migration files",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "up",
|
||||||
|
Usage: "Migrate to the latest version",
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
runCommand(UpCommand, ctx)
|
||||||
|
},
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "pretend, p",
|
||||||
|
Usage: "don't do anything; print migrations to apply",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "new",
|
||||||
|
Usage: "Generate a new migration file",
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
runCommand(NewCommand, ctx)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "create",
|
||||||
|
Usage: "Create database",
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
runCommand(CreateCommand, ctx)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "drop",
|
||||||
|
Usage: "Drop database (if it exists)",
|
||||||
|
Action: func(ctx *cli.Context) {
|
||||||
|
runCommand(DropCommand, ctx)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
type command func(*cli.Context) error
|
||||||
|
|
||||||
|
func runCommand(cmd command, ctx *cli.Context) {
|
||||||
|
err := cmd(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDotEnv() {
|
||||||
|
if _, err := os.Stat(".env"); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := godotenv.Load(); err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue