diff --git a/commands.go b/commands.go index cb66323..8b8b80e 100644 --- a/commands.go +++ b/commands.go @@ -14,6 +14,32 @@ import ( "time" ) +// UpCommand creates the database (if necessary) and runs migrations +func UpCommand(ctx *cli.Context) error { + u, err := GetDatabaseURL(ctx) + if err != nil { + return err + } + + drv, err := driver.Get(u.Scheme) + if err != nil { + return err + } + + // create database if it does not already exist + // skip this step if we cannot determine status + // (e.g. user does not have list database permission) + exists, err := drv.DatabaseExists(u) + if err == nil && !exists { + if err := drv.CreateDatabase(u); err != nil { + return err + } + } + + // migrate + return MigrateCommand(ctx) +} + // CreateCommand creates the current database func CreateCommand(ctx *cli.Context) error { u, err := GetDatabaseURL(ctx) diff --git a/driver/driver.go b/driver/driver.go index e4f7fad..6c88ffb 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -11,6 +11,7 @@ import ( // Driver provides top level database functions type Driver interface { Open(*url.URL) (*sql.DB, error) + DatabaseExists(*url.URL) (bool, error) CreateDatabase(*url.URL) error DropDatabase(*url.URL) error CreateMigrationsTable(*sql.DB) error diff --git a/driver/postgres/postgres.go b/driver/postgres/postgres.go index 01cbf28..947b2dc 100644 --- a/driver/postgres/postgres.go +++ b/driver/postgres/postgres.go @@ -18,39 +18,66 @@ func (postgres Driver) Open(u *url.URL) (*sql.DB, error) { } // postgresExec runs a sql statement on the "postgres" database -func (postgres Driver) postgresExec(u *url.URL, statement string) error { +func (postgres Driver) openPostgresDB(u *url.URL) (*sql.DB, error) { // connect to postgres database postgresURL := *u postgresURL.Path = "postgres" - db, err := postgres.Open(&postgresURL) + return postgres.Open(&postgresURL) +} + +// CreateDatabase creates the specified database +func (postgres Driver) CreateDatabase(u *url.URL) error { + name := shared.DatabaseName(u) + fmt.Printf("Creating: %s\n", name) + + db, err := postgres.openPostgresDB(u) if err != nil { return err } defer db.Close() - // run statement - _, err = db.Exec(statement) + _, err = db.Exec(fmt.Sprintf("CREATE DATABASE %s", + pq.QuoteIdentifier(name))) 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) + name := shared.DatabaseName(u) + fmt.Printf("Dropping: %s\n", name) - return postgres.postgresExec(u, fmt.Sprintf("DROP DATABASE IF EXISTS %s", - pq.QuoteIdentifier(database))) + db, err := postgres.openPostgresDB(u) + if err != nil { + return err + } + defer db.Close() + + _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS %s", + pq.QuoteIdentifier(name))) + + return err +} + +// DatabaseExists determines whether the database exists +func (postgres Driver) DatabaseExists(u *url.URL) (bool, error) { + name := shared.DatabaseName(u) + + db, err := postgres.openPostgresDB(u) + if err != nil { + return false, err + } + defer db.Close() + + exists := false + err = db.QueryRow("SELECT true FROM pg_database WHERE datname = $1", name). + Scan(&exists) + if err == sql.ErrNoRows { + return false, nil + } + + return exists, err } // HasMigrationsTable returns true if the schema_migrations table exists diff --git a/driver/postgres/postgres_test.go b/driver/postgres/postgres_test.go index e352d5a..55cb5c2 100644 --- a/driver/postgres/postgres_test.go +++ b/driver/postgres/postgres_test.go @@ -61,3 +61,36 @@ func TestCreateDropDatabase(t *testing.T) { require.Equal(t, "pq: database \"dbmate\" does not exist", err.Error()) }() } + +func TestDatabaseExists(t *testing.T) { + d := postgres.Driver{} + u := testURL(t) + + // drop any existing database + err := d.DropDatabase(u) + require.Nil(t, err) + + // DatabaseExists should return false + exists, err := d.DatabaseExists(u) + require.Nil(t, err) + require.Equal(t, false, exists) + + // create database + err = d.CreateDatabase(u) + require.Nil(t, err) + + // DatabaseExists should return true + exists, err = d.DatabaseExists(u) + require.Nil(t, err) + require.Equal(t, true, exists) +} + +func TestDatabaseExists_error(t *testing.T) { + d := postgres.Driver{} + u := testURL(t) + u.User = url.User("invalid") + + exists, err := d.DatabaseExists(u) + require.Equal(t, "pq: role \"invalid\" does not exist", err.Error()) + require.Equal(t, false, exists) +} diff --git a/main.go b/main.go index 7ff7357..668fb62 100644 --- a/main.go +++ b/main.go @@ -35,20 +35,6 @@ func NewApp() *cli.App { } app.Commands = []cli.Command{ - { - Name: "migrate", - Usage: "Migrate to the latest version", - Action: func(ctx *cli.Context) { - runCommand(MigrateCommand, ctx) - }, - }, - { - Name: "rollback", - Usage: "Rollback the most recent migration", - Action: func(ctx *cli.Context) { - runCommand(RollbackCommand, ctx) - }, - }, { Name: "new", Usage: "Generate a new migration file", @@ -56,6 +42,13 @@ func NewApp() *cli.App { runCommand(NewCommand, ctx) }, }, + { + Name: "up", + Usage: "Create database (if necessary) and migrate to the latest version", + Action: func(ctx *cli.Context) { + runCommand(UpCommand, ctx) + }, + }, { Name: "create", Usage: "Create database", @@ -70,6 +63,20 @@ func NewApp() *cli.App { runCommand(DropCommand, ctx) }, }, + { + Name: "migrate", + Usage: "Migrate to the latest version", + Action: func(ctx *cli.Context) { + runCommand(MigrateCommand, ctx) + }, + }, + { + Name: "rollback", + Usage: "Rollback the most recent migration", + Action: func(ctx *cli.Context) { + runCommand(RollbackCommand, ctx) + }, + }, } return app