mirror of
https://github.com/TECHNOFAB11/dbmate.git
synced 2025-12-12 08:00:04 +01:00
Add dump command (#23)
Adds `dbmate dump` command to write the database schema to a file. The intent is for this file to be checked in to the codebase, similar to Rails' `schema.rb` (or `structure.sql`) file. This allows developers to share a single file documenting the database schema, and makes it considerably easier to review PRs which add (or change) migrations. The existing `up`, `migrate`, and `rollback` commands will automatically trigger a schema dump, unless `--no-dump-schema` is passed. Closes https://github.com/amacneil/dbmate/issues/5
This commit is contained in:
parent
54a9fbc859
commit
d855ee1ada
15 changed files with 578 additions and 34 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
/dist
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/dist
|
||||||
|
/testdata/db/schema.sql
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,14 @@ FROM golang:1.9 as build
|
||||||
# required to force cgo (for sqlite driver) with cross compile
|
# required to force cgo (for sqlite driver) with cross compile
|
||||||
ENV CGO_ENABLED 1
|
ENV CGO_ENABLED 1
|
||||||
|
|
||||||
|
# install database clients
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
mysql-client \
|
||||||
|
postgresql-client \
|
||||||
|
sqlite3 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# development dependencies
|
# development dependencies
|
||||||
RUN go get \
|
RUN go get \
|
||||||
github.com/golang/lint/golint \
|
github.com/golang/lint/golint \
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,24 @@ func NewApp() *cli.App {
|
||||||
app.Version = dbmate.Version
|
app.Version = dbmate.Version
|
||||||
|
|
||||||
app.Flags = []cli.Flag{
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "env, e",
|
||||||
|
Value: "DATABASE_URL",
|
||||||
|
Usage: "specify an environment variable containing the database URL",
|
||||||
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "migrations-dir, d",
|
Name: "migrations-dir, d",
|
||||||
Value: dbmate.DefaultMigrationsDir,
|
Value: dbmate.DefaultMigrationsDir,
|
||||||
Usage: "specify the directory containing migration files",
|
Usage: "specify the directory containing migration files",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "env, e",
|
Name: "schema-file, s",
|
||||||
Value: "DATABASE_URL",
|
Value: dbmate.DefaultSchemaFile,
|
||||||
Usage: "specify an environment variable containing the database URL",
|
Usage: "specify the schema file location",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "no-dump-schema",
|
||||||
|
Usage: "don't update the schema file on migrate/rollback",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,14 +58,14 @@ func NewApp() *cli.App {
|
||||||
Usage: "Generate a new migration file",
|
Usage: "Generate a new migration file",
|
||||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||||
name := c.Args().First()
|
name := c.Args().First()
|
||||||
return db.New(name)
|
return db.NewMigration(name)
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "up",
|
Name: "up",
|
||||||
Usage: "Create database (if necessary) and migrate to the latest version",
|
Usage: "Create database (if necessary) and migrate to the latest version",
|
||||||
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||||
return db.Up()
|
return db.CreateAndMigrate()
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -88,6 +97,13 @@ func NewApp() *cli.App {
|
||||||
return db.Rollback()
|
return db.Rollback()
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "dump",
|
||||||
|
Usage: "Write the database schema to disk",
|
||||||
|
Action: action(func(db *dbmate.DB, c *cli.Context) error {
|
||||||
|
return db.DumpSchema()
|
||||||
|
}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
@ -111,8 +127,10 @@ func action(f func(*dbmate.DB, *cli.Context) error) cli.ActionFunc {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
db := dbmate.NewDB(u)
|
db := dbmate.New(u)
|
||||||
|
db.AutoDumpSchema = !c.GlobalBool("no-dump-schema")
|
||||||
db.MigrationsDir = c.GlobalString("migrations-dir")
|
db.MigrationsDir = c.GlobalString("migrations-dir")
|
||||||
|
db.SchemaFile = c.GlobalString("schema-file")
|
||||||
|
|
||||||
return f(db, c)
|
return f(db, c)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ services:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql
|
image: mysql:5.7
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: root
|
MYSQL_ROOT_PASSWORD: root
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "status", "-proot"]
|
test: ["CMD", "mysqladmin", "status", "-proot"]
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres
|
image: postgres:9.6
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "pg_isready"]
|
test: ["CMD", "pg_isready"]
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,24 @@ import (
|
||||||
// DefaultMigrationsDir specifies default directory to find migration files
|
// DefaultMigrationsDir specifies default directory to find migration files
|
||||||
var DefaultMigrationsDir = "./db/migrations"
|
var DefaultMigrationsDir = "./db/migrations"
|
||||||
|
|
||||||
|
// DefaultSchemaFile specifies default location for schema.sql
|
||||||
|
var DefaultSchemaFile = "./db/schema.sql"
|
||||||
|
|
||||||
// DB allows dbmate actions to be performed on a specified database
|
// DB allows dbmate actions to be performed on a specified database
|
||||||
type DB struct {
|
type DB struct {
|
||||||
|
AutoDumpSchema bool
|
||||||
DatabaseURL *url.URL
|
DatabaseURL *url.URL
|
||||||
MigrationsDir string
|
MigrationsDir string
|
||||||
|
SchemaFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDB initializes a new dbmate database
|
// New initializes a new dbmate database
|
||||||
func NewDB(databaseURL *url.URL) *DB {
|
func New(databaseURL *url.URL) *DB {
|
||||||
return &DB{
|
return &DB{
|
||||||
|
AutoDumpSchema: true,
|
||||||
DatabaseURL: databaseURL,
|
DatabaseURL: databaseURL,
|
||||||
MigrationsDir: DefaultMigrationsDir,
|
MigrationsDir: DefaultMigrationsDir,
|
||||||
|
SchemaFile: DefaultSchemaFile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,8 +41,8 @@ func (db *DB) GetDriver() (Driver, error) {
|
||||||
return GetDriver(db.DatabaseURL.Scheme)
|
return GetDriver(db.DatabaseURL.Scheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Up creates the database (if necessary) and runs migrations
|
// CreateAndMigrate creates the database (if necessary) and runs migrations
|
||||||
func (db *DB) Up() error {
|
func (db *DB) CreateAndMigrate() error {
|
||||||
drv, err := db.GetDriver()
|
drv, err := db.GetDriver()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -75,10 +82,34 @@ func (db *DB) Drop() error {
|
||||||
return drv.DropDatabase(db.DatabaseURL)
|
return drv.DropDatabase(db.DatabaseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DumpSchema writes the current database schema to a file
|
||||||
|
func (db *DB) DumpSchema() error {
|
||||||
|
drv, sqlDB, err := db.openDatabaseForMigration()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer mustClose(sqlDB)
|
||||||
|
|
||||||
|
schema, err := drv.DumpSchema(db.DatabaseURL, sqlDB)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Writing: %s\n", db.SchemaFile)
|
||||||
|
|
||||||
|
// ensure schema directory exists
|
||||||
|
if err = ensureDir(filepath.Dir(db.SchemaFile)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write schema to file
|
||||||
|
return ioutil.WriteFile(db.SchemaFile, schema, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
||||||
|
|
||||||
// New creates a new migration file
|
// NewMigration creates a new migration file
|
||||||
func (db *DB) New(name string) error {
|
func (db *DB) NewMigration(name string) error {
|
||||||
// new migration name
|
// new migration name
|
||||||
timestamp := time.Now().UTC().Format("20060102150405")
|
timestamp := time.Now().UTC().Format("20060102150405")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
|
|
@ -87,8 +118,8 @@ func (db *DB) New(name string) error {
|
||||||
name = fmt.Sprintf("%s_%s.sql", timestamp, name)
|
name = fmt.Sprintf("%s_%s.sql", timestamp, name)
|
||||||
|
|
||||||
// create migrations dir if missing
|
// create migrations dir if missing
|
||||||
if err := os.MkdirAll(db.MigrationsDir, 0755); err != nil {
|
if err := ensureDir(db.MigrationsDir); err != nil {
|
||||||
return fmt.Errorf("unable to create directory `%s`", db.MigrationsDir)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check file does not already exist
|
// check file does not already exist
|
||||||
|
|
@ -203,6 +234,11 @@ func (db *DB) Migrate() error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automatically update schema file, silence errors
|
||||||
|
if db.AutoDumpSchema {
|
||||||
|
_ = db.DumpSchema()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,5 +376,10 @@ func (db *DB) Rollback() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automatically update schema file, silence errors
|
||||||
|
if db.AutoDumpSchema {
|
||||||
|
_ = db.DumpSchema()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package dbmate
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -23,7 +24,103 @@ func newTestDB(t *testing.T, u *url.URL) *DB {
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewDB(u)
|
db := New(u)
|
||||||
|
db.AutoDumpSchema = false
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
u := postgresTestURL(t)
|
||||||
|
db := New(u)
|
||||||
|
require.True(t, db.AutoDumpSchema)
|
||||||
|
require.Equal(t, u.String(), db.DatabaseURL.String())
|
||||||
|
require.Equal(t, "./db/migrations", db.MigrationsDir)
|
||||||
|
require.Equal(t, "./db/schema.sql", db.SchemaFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDumpSchema(t *testing.T) {
|
||||||
|
u := postgresTestURL(t)
|
||||||
|
db := newTestDB(t, u)
|
||||||
|
|
||||||
|
// create custom schema file directory
|
||||||
|
dir, err := ioutil.TempDir("", "dbmate")
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(dir)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// create schema.sql in subdirectory to test creating directory
|
||||||
|
db.SchemaFile = filepath.Join(dir, "/schema/schema.sql")
|
||||||
|
|
||||||
|
// drop database
|
||||||
|
err = db.Drop()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// create and migrate
|
||||||
|
err = db.CreateAndMigrate()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// schema.sql should not exist
|
||||||
|
_, err = os.Stat(db.SchemaFile)
|
||||||
|
require.True(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
// dump schema
|
||||||
|
err = db.DumpSchema()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// verify schema
|
||||||
|
schema, err := ioutil.ReadFile(db.SchemaFile)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Contains(t, string(schema), "-- PostgreSQL database dump")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoDumpSchema(t *testing.T) {
|
||||||
|
u := postgresTestURL(t)
|
||||||
|
db := newTestDB(t, u)
|
||||||
|
db.AutoDumpSchema = true
|
||||||
|
|
||||||
|
// create custom schema file directory
|
||||||
|
dir, err := ioutil.TempDir("", "dbmate")
|
||||||
|
require.Nil(t, err)
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(dir)
|
||||||
|
require.Nil(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// create schema.sql in subdirectory to test creating directory
|
||||||
|
db.SchemaFile = filepath.Join(dir, "/schema/schema.sql")
|
||||||
|
|
||||||
|
// drop database
|
||||||
|
err = db.Drop()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// schema.sql should not exist
|
||||||
|
_, err = os.Stat(db.SchemaFile)
|
||||||
|
require.True(t, os.IsNotExist(err))
|
||||||
|
|
||||||
|
// create and migrate
|
||||||
|
err = db.CreateAndMigrate()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// verify schema
|
||||||
|
schema, err := ioutil.ReadFile(db.SchemaFile)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Contains(t, string(schema), "-- PostgreSQL database dump")
|
||||||
|
|
||||||
|
// remove schema
|
||||||
|
err = os.Remove(db.SchemaFile)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// rollback
|
||||||
|
err = db.Rollback()
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// schema should be recreated
|
||||||
|
schema, err = ioutil.ReadFile(db.SchemaFile)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Contains(t, string(schema), "-- PostgreSQL database dump")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testURLs(t *testing.T) []*url.URL {
|
func testURLs(t *testing.T) []*url.URL {
|
||||||
|
|
@ -77,7 +174,7 @@ func testUpURL(t *testing.T, u *url.URL) {
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
// create and migrate
|
// create and migrate
|
||||||
err = db.Up()
|
err = db.CreateAndMigrate()
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
// verify results
|
// verify results
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ type Driver interface {
|
||||||
DatabaseExists(*url.URL) (bool, error)
|
DatabaseExists(*url.URL) (bool, error)
|
||||||
CreateDatabase(*url.URL) error
|
CreateDatabase(*url.URL) error
|
||||||
DropDatabase(*url.URL) error
|
DropDatabase(*url.URL) error
|
||||||
|
DumpSchema(*url.URL, *sql.DB) ([]byte, error)
|
||||||
CreateMigrationsTable(*sql.DB) error
|
CreateMigrationsTable(*sql.DB) error
|
||||||
SelectMigrations(*sql.DB, int) (map[string]bool, error)
|
SelectMigrations(*sql.DB, int) (map[string]bool, error)
|
||||||
InsertMigration(Transaction, string) error
|
InsertMigration(Transaction, string) error
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package dbmate
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -46,7 +47,7 @@ func (drv MySQLDriver) openRootDB(u *url.URL) (*sql.DB, error) {
|
||||||
return drv.Open(&rootURL)
|
return drv.Open(&rootURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func quoteIdentifier(str string) string {
|
func mysqlQuoteIdentifier(str string) string {
|
||||||
str = strings.Replace(str, "`", "\\`", -1)
|
str = strings.Replace(str, "`", "\\`", -1)
|
||||||
|
|
||||||
return fmt.Sprintf("`%s`", str)
|
return fmt.Sprintf("`%s`", str)
|
||||||
|
|
@ -64,7 +65,7 @@ func (drv MySQLDriver) CreateDatabase(u *url.URL) error {
|
||||||
defer mustClose(db)
|
defer mustClose(db)
|
||||||
|
|
||||||
_, err = db.Exec(fmt.Sprintf("create database %s",
|
_, err = db.Exec(fmt.Sprintf("create database %s",
|
||||||
quoteIdentifier(name)))
|
mysqlQuoteIdentifier(name)))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -81,11 +82,77 @@ func (drv MySQLDriver) DropDatabase(u *url.URL) error {
|
||||||
defer mustClose(db)
|
defer mustClose(db)
|
||||||
|
|
||||||
_, err = db.Exec(fmt.Sprintf("drop database if exists %s",
|
_, err = db.Exec(fmt.Sprintf("drop database if exists %s",
|
||||||
quoteIdentifier(name)))
|
mysqlQuoteIdentifier(name)))
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mysqldumpArgs(u *url.URL) []string {
|
||||||
|
// generate CLI arguments
|
||||||
|
args := []string{"--opt", "--routines", "--no-data",
|
||||||
|
"--skip-dump-date", "--skip-add-drop-table"}
|
||||||
|
|
||||||
|
if hostname := u.Hostname(); hostname != "" {
|
||||||
|
args = append(args, "--host="+hostname)
|
||||||
|
}
|
||||||
|
if port := u.Port(); port != "" {
|
||||||
|
args = append(args, "--port="+port)
|
||||||
|
}
|
||||||
|
if username := u.User.Username(); username != "" {
|
||||||
|
args = append(args, "--user="+username)
|
||||||
|
}
|
||||||
|
// mysql recommands against using environment variables to supply password
|
||||||
|
// https://dev.mysql.com/doc/refman/5.7/en/password-security-user.html
|
||||||
|
if password, set := u.User.Password(); set {
|
||||||
|
args = append(args, "--password="+password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add database name
|
||||||
|
args = append(args, strings.TrimLeft(u.Path, "/"))
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func mysqlSchemaMigrationsDump(db *sql.DB) ([]byte, error) {
|
||||||
|
// load applied migrations
|
||||||
|
migrations, err := queryColumn(db,
|
||||||
|
"select quote(version) from schema_migrations order by version asc")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build schema_migrations table data
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("\n--\n-- Dbmate schema migrations\n--\n\n" +
|
||||||
|
"LOCK TABLES `schema_migrations` WRITE;\n")
|
||||||
|
|
||||||
|
if len(migrations) > 0 {
|
||||||
|
buf.WriteString("INSERT INTO `schema_migrations` (version) VALUES\n (" +
|
||||||
|
strings.Join(migrations, "),\n (") +
|
||||||
|
");\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString("UNLOCK TABLES;\n")
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpSchema returns the current database schema
|
||||||
|
func (drv MySQLDriver) DumpSchema(u *url.URL, db *sql.DB) ([]byte, error) {
|
||||||
|
schema, err := runCommand("mysqldump", mysqldumpArgs(u)...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations, err := mysqlSchemaMigrationsDump(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = append(schema, migrations...)
|
||||||
|
return trimLeadingSQLComments(schema)
|
||||||
|
}
|
||||||
|
|
||||||
// DatabaseExists determines whether the database exists
|
// DatabaseExists determines whether the database exists
|
||||||
func (drv MySQLDriver) DatabaseExists(u *url.URL) (bool, error) {
|
func (drv MySQLDriver) DatabaseExists(u *url.URL) (bool, error) {
|
||||||
name := databaseName(u)
|
name := databaseName(u)
|
||||||
|
|
@ -97,8 +164,8 @@ func (drv MySQLDriver) DatabaseExists(u *url.URL) (bool, error) {
|
||||||
defer mustClose(db)
|
defer mustClose(db)
|
||||||
|
|
||||||
exists := false
|
exists := false
|
||||||
err = db.QueryRow(`select true from information_schema.schemata
|
err = db.QueryRow("select true from information_schema.schemata "+
|
||||||
where schema_name = ?`, name).Scan(&exists)
|
"where schema_name = ?", name).Scan(&exists)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
@ -108,8 +175,8 @@ func (drv MySQLDriver) DatabaseExists(u *url.URL) (bool, error) {
|
||||||
|
|
||||||
// CreateMigrationsTable creates the schema_migrations table
|
// CreateMigrationsTable creates the schema_migrations table
|
||||||
func (drv MySQLDriver) CreateMigrationsTable(db *sql.DB) error {
|
func (drv MySQLDriver) CreateMigrationsTable(db *sql.DB) error {
|
||||||
_, err := db.Exec(`create table if not exists schema_migrations (
|
_, err := db.Exec("create table if not exists schema_migrations " +
|
||||||
version varchar(255) primary key)`)
|
"(version varchar(255) primary key)")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,45 @@ func TestMySQLCreateDropDatabase(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMySQLDumpSchema(t *testing.T) {
|
||||||
|
drv := MySQLDriver{}
|
||||||
|
u := mySQLTestURL(t)
|
||||||
|
|
||||||
|
// prepare database
|
||||||
|
db := prepTestMySQLDB(t)
|
||||||
|
defer mustClose(db)
|
||||||
|
err := drv.CreateMigrationsTable(db)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// insert migration
|
||||||
|
err = drv.InsertMigration(db, "abc1")
|
||||||
|
require.Nil(t, err)
|
||||||
|
err = drv.InsertMigration(db, "abc2")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// DumpSchema should return schema
|
||||||
|
schema, err := drv.DumpSchema(u, db)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Contains(t, string(schema), "CREATE TABLE `schema_migrations`")
|
||||||
|
require.Contains(t, string(schema), "\n-- Dump completed\n\n"+
|
||||||
|
"--\n"+
|
||||||
|
"-- Dbmate schema migrations\n"+
|
||||||
|
"--\n\n"+
|
||||||
|
"LOCK TABLES `schema_migrations` WRITE;\n"+
|
||||||
|
"INSERT INTO `schema_migrations` (version) VALUES\n"+
|
||||||
|
" ('abc1'),\n"+
|
||||||
|
" ('abc2');\n"+
|
||||||
|
"UNLOCK TABLES;\n")
|
||||||
|
|
||||||
|
// DumpSchema should return error if command fails
|
||||||
|
u.Path = "/fakedb"
|
||||||
|
schema, err = drv.DumpSchema(u, db)
|
||||||
|
require.Nil(t, schema)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.Equal(t, "mysqldump: Got error: 1049: \"Unknown database 'fakedb'\" "+
|
||||||
|
"when selecting the database", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestMySQLDatabaseExists(t *testing.T) {
|
func TestMySQLDatabaseExists(t *testing.T) {
|
||||||
drv := MySQLDriver{}
|
drv := MySQLDriver{}
|
||||||
u := mySQLTestURL(t)
|
u := mySQLTestURL(t)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package dbmate
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
@ -59,6 +61,45 @@ func (drv PostgresDriver) DropDatabase(u *url.URL) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func postgresSchemaMigrationsDump(db *sql.DB) ([]byte, error) {
|
||||||
|
// load applied migrations
|
||||||
|
migrations, err := queryColumn(db,
|
||||||
|
"select quote_literal(version) from schema_migrations order by version asc")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build schema_migrations table data
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("\n--\n-- Dbmate schema migrations\n--\n\n")
|
||||||
|
|
||||||
|
if len(migrations) > 0 {
|
||||||
|
buf.WriteString("INSERT INTO schema_migrations (version) VALUES\n (" +
|
||||||
|
strings.Join(migrations, "),\n (") +
|
||||||
|
");\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpSchema returns the current database schema
|
||||||
|
func (drv PostgresDriver) DumpSchema(u *url.URL, db *sql.DB) ([]byte, error) {
|
||||||
|
// load schema
|
||||||
|
schema, err := runCommand("pg_dump", "--format=plain", "--encoding=UTF8",
|
||||||
|
"--schema-only", "--no-privileges", "--no-owner", u.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations, err := postgresSchemaMigrationsDump(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = append(schema, migrations...)
|
||||||
|
return trimLeadingSQLComments(schema)
|
||||||
|
}
|
||||||
|
|
||||||
// DatabaseExists determines whether the database exists
|
// DatabaseExists determines whether the database exists
|
||||||
func (drv PostgresDriver) DatabaseExists(u *url.URL) (bool, error) {
|
func (drv PostgresDriver) DatabaseExists(u *url.URL) (bool, error) {
|
||||||
name := databaseName(u)
|
name := databaseName(u)
|
||||||
|
|
@ -81,8 +122,8 @@ func (drv PostgresDriver) DatabaseExists(u *url.URL) (bool, error) {
|
||||||
|
|
||||||
// CreateMigrationsTable creates the schema_migrations table
|
// CreateMigrationsTable creates the schema_migrations table
|
||||||
func (drv PostgresDriver) CreateMigrationsTable(db *sql.DB) error {
|
func (drv PostgresDriver) CreateMigrationsTable(db *sql.DB) error {
|
||||||
_, err := db.Exec(`create table if not exists schema_migrations (
|
_, err := db.Exec("create table if not exists schema_migrations " +
|
||||||
version varchar(255) primary key)`)
|
"(version varchar(255) primary key)")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,45 @@ func TestPostgresCreateDropDatabase(t *testing.T) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPostgresDumpSchema(t *testing.T) {
|
||||||
|
drv := PostgresDriver{}
|
||||||
|
u := postgresTestURL(t)
|
||||||
|
|
||||||
|
// prepare database
|
||||||
|
db := prepTestPostgresDB(t)
|
||||||
|
defer mustClose(db)
|
||||||
|
err := drv.CreateMigrationsTable(db)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// insert migration
|
||||||
|
err = drv.InsertMigration(db, "abc1")
|
||||||
|
require.Nil(t, err)
|
||||||
|
err = drv.InsertMigration(db, "abc2")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// DumpSchema should return schema
|
||||||
|
schema, err := drv.DumpSchema(u, db)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Contains(t, string(schema), "CREATE TABLE schema_migrations")
|
||||||
|
require.Contains(t, string(schema), "\n--\n"+
|
||||||
|
"-- PostgreSQL database dump complete\n"+
|
||||||
|
"--\n\n\n"+
|
||||||
|
"--\n"+
|
||||||
|
"-- Dbmate schema migrations\n"+
|
||||||
|
"--\n\n"+
|
||||||
|
"INSERT INTO schema_migrations (version) VALUES\n"+
|
||||||
|
" ('abc1'),\n"+
|
||||||
|
" ('abc2');\n")
|
||||||
|
|
||||||
|
// DumpSchema should return error if command fails
|
||||||
|
u.Path = "/fakedb"
|
||||||
|
schema, err = drv.DumpSchema(u, db)
|
||||||
|
require.Nil(t, schema)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.Equal(t, "pg_dump: [archiver (db)] connection to database \"fakedb\" failed: "+
|
||||||
|
"FATAL: database \"fakedb\" does not exist", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestPostgresDatabaseExists(t *testing.T) {
|
func TestPostgresDatabaseExists(t *testing.T) {
|
||||||
drv := PostgresDriver{}
|
drv := PostgresDriver{}
|
||||||
u := postgresTestURL(t)
|
u := postgresTestURL(t)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package dbmate
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3" // sqlite driver for database/sql
|
_ "github.com/mattn/go-sqlite3" // sqlite driver for database/sql
|
||||||
)
|
)
|
||||||
|
|
@ -56,6 +58,44 @@ func (drv SQLiteDriver) DropDatabase(u *url.URL) error {
|
||||||
return os.Remove(path)
|
return os.Remove(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sqliteSchemaMigrationsDump(db *sql.DB) ([]byte, error) {
|
||||||
|
// load applied migrations
|
||||||
|
migrations, err := queryColumn(db,
|
||||||
|
"select quote(version) from schema_migrations order by version asc")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build schema_migrations table data
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString("-- Dbmate schema migrations\n")
|
||||||
|
|
||||||
|
if len(migrations) > 0 {
|
||||||
|
buf.WriteString("INSERT INTO schema_migrations (version) VALUES\n (" +
|
||||||
|
strings.Join(migrations, "),\n (") +
|
||||||
|
");\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpSchema returns the current database schema
|
||||||
|
func (drv SQLiteDriver) DumpSchema(u *url.URL, db *sql.DB) ([]byte, error) {
|
||||||
|
path := sqlitePath(u)
|
||||||
|
schema, err := runCommand("sqlite3", path, ".schema")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations, err := sqliteSchemaMigrationsDump(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
schema = append(schema, migrations...)
|
||||||
|
return trimLeadingSQLComments(schema)
|
||||||
|
}
|
||||||
|
|
||||||
// DatabaseExists determines whether the database exists
|
// DatabaseExists determines whether the database exists
|
||||||
func (drv SQLiteDriver) DatabaseExists(u *url.URL) (bool, error) {
|
func (drv SQLiteDriver) DatabaseExists(u *url.URL) (bool, error) {
|
||||||
_, err := os.Stat(sqlitePath(u))
|
_, err := os.Stat(sqlitePath(u))
|
||||||
|
|
@ -71,8 +111,8 @@ func (drv SQLiteDriver) DatabaseExists(u *url.URL) (bool, error) {
|
||||||
|
|
||||||
// CreateMigrationsTable creates the schema_migrations table
|
// CreateMigrationsTable creates the schema_migrations table
|
||||||
func (drv SQLiteDriver) CreateMigrationsTable(db *sql.DB) error {
|
func (drv SQLiteDriver) CreateMigrationsTable(db *sql.DB) error {
|
||||||
_, err := db.Exec(`create table if not exists schema_migrations (
|
_, err := db.Exec("create table if not exists schema_migrations " +
|
||||||
version varchar(255) primary key)`)
|
"(version varchar(255) primary key)")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,40 @@ func TestSQLiteCreateDropDatabase(t *testing.T) {
|
||||||
require.Equal(t, true, os.IsNotExist(err))
|
require.Equal(t, true, os.IsNotExist(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSQLiteDumpSchema(t *testing.T) {
|
||||||
|
drv := SQLiteDriver{}
|
||||||
|
u := sqliteTestURL(t)
|
||||||
|
|
||||||
|
// prepare database
|
||||||
|
db := prepTestSQLiteDB(t)
|
||||||
|
defer mustClose(db)
|
||||||
|
err := drv.CreateMigrationsTable(db)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// insert migration
|
||||||
|
err = drv.InsertMigration(db, "abc1")
|
||||||
|
require.Nil(t, err)
|
||||||
|
err = drv.InsertMigration(db, "abc2")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// DumpSchema should return schema
|
||||||
|
schema, err := drv.DumpSchema(u, db)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Contains(t, string(schema), "CREATE TABLE schema_migrations")
|
||||||
|
require.Contains(t, string(schema), ");\n-- Dbmate schema migrations\n"+
|
||||||
|
"INSERT INTO schema_migrations (version) VALUES\n"+
|
||||||
|
" ('abc1'),\n"+
|
||||||
|
" ('abc2');\n")
|
||||||
|
|
||||||
|
// DumpSchema should return error if command fails
|
||||||
|
u.Path = "/."
|
||||||
|
schema, err = drv.DumpSchema(u, db)
|
||||||
|
require.Nil(t, schema)
|
||||||
|
require.NotNil(t, err)
|
||||||
|
require.Equal(t, "Error: unable to open database \".\": unable to open database file",
|
||||||
|
err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestSQLiteDatabaseExists(t *testing.T) {
|
func TestSQLiteDatabaseExists(t *testing.T) {
|
||||||
drv := SQLiteDriver{}
|
drv := SQLiteDriver{}
|
||||||
u := sqliteTestURL(t)
|
u := sqliteTestURL(t)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,17 @@
|
||||||
package dbmate
|
package dbmate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// databaseName returns the database name from a URL
|
// databaseName returns the database name from a URL
|
||||||
|
|
@ -15,8 +24,106 @@ func databaseName(u *url.URL) string {
|
||||||
return name
|
return name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mustClose ensures a stream is closed
|
||||||
func mustClose(c io.Closer) {
|
func mustClose(c io.Closer) {
|
||||||
if err := c.Close(); err != nil {
|
if err := c.Close(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureDir creates a directory if it does not already exist
|
||||||
|
func ensureDir(dir string) error {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("unable to create directory `%s`", dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCommand runs a command and returns the stdout if successful
|
||||||
|
func runCommand(name string, args ...string) ([]byte, error) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// trimLeadingSQLComments removes sql comments and blank lines from the beginning of text
|
||||||
|
// generally when performing sql dumps these contain host-specific information such as
|
||||||
|
// client/server version numbers
|
||||||
|
func trimLeadingSQLComments(data []byte) ([]byte, error) {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// queryColumn runs a SQL statement and returns a slice of strings
|
||||||
|
// it is assumed that the statement returns only one column
|
||||||
|
// e.g. schema_migrations table
|
||||||
|
func queryColumn(db *sql.DB, query string) ([]string, error) {
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer mustClose(rows)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,14 @@ func TestDatabaseName_Empty(t *testing.T) {
|
||||||
name := databaseName(u)
|
name := databaseName(u)
|
||||||
require.Equal(t, "", name)
|
require.Equal(t, "", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrimLeadingSQLComments(t *testing.T) {
|
||||||
|
in := "--\n" +
|
||||||
|
"-- foo\n\n" +
|
||||||
|
"-- bar\n\n" +
|
||||||
|
"real stuff\n" +
|
||||||
|
"-- end\n"
|
||||||
|
out, err := trimLeadingSQLComments([]byte(in))
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "real stuff\n-- end\n", string(out))
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue