blob: a72c8727e8b42f067cba74f4956e3a7c59bc499b [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Wrapper around rubenv/sql-migrate to allow MySQL SSL connections using
// dbutil (uses dbutil sqlconf files and flags with playground-specific
// defaults instead of rubenv/sql-migrate YAML config).
//
// WARNING: MySQL doesn't support rolling back DDL transactions, so any failure
// after migrations have started requires restoring from backup or manually
// repairing database state!
package main
import (
"database/sql"
"fmt"
"os"
"github.com/rubenv/sql-migrate"
"v.io/x/lib/cmdline"
"v.io/x/lib/dbutil"
"v.io/x/playground/lib"
)
const mysqlWarning = `
WARNING: MySQL doesn't support rolling back DDL transactions, so any failure
after migrations have started requires restoring from backup or manually
repairing database state!
`
// TODO(ivanpi): Add status command and sanity checks (e.g. "skipped" migrations are incorrectly applied by rubenv/sql-migrate).
// TODO(ivanpi): Guard against version skew corrupting data (e.g. add version check to client).
var cmdMigrate = &cmdline.Command{
Name: "migrate",
Short: "Database schema migrations",
Long: `
See github.com/rubenv/sql-migrate
` + mysqlWarning,
Children: []*cmdline.Command{cmdMigrateUp, cmdMigrateDown},
}
var cmdMigrateUp = &cmdline.Command{
Runner: runWithDBConn(runMigrate(migrate.Up, &flagMigrationsLimitUp)),
Name: "up",
Short: "Apply new database schema migrations",
Long: `
See github.com/rubenv/sql-migrate
` + mysqlWarning,
}
var cmdMigrateDown = &cmdline.Command{
Runner: runWithDBConn(runMigrate(migrate.Down, &flagMigrationsLimitDown)),
Name: "down",
Short: "Roll back database schema migrations",
Long: `
See github.com/rubenv/sql-migrate
` + mysqlWarning,
}
const (
migrationsTable = "migrations"
sqlDialect = "mysql"
pgMigrationsDir = "${JIRI_ROOT}/release/projects/playground/go/src/v.io/x/playground/migrations"
)
var (
flagMigrationsDir string
flagMigrationsLimitUp int
flagMigrationsLimitDown int
)
func init() {
cmdMigrate.Flags.StringVar(&flagMigrationsDir, "dir", pgMigrationsDir, "Path to directory containing migrations.")
cmdMigrateUp.Flags.IntVar(&flagMigrationsLimitUp, "limit", 0, "Maximum number of up migrations to apply. 0 for unlimited.")
cmdMigrateDown.Flags.IntVar(&flagMigrationsLimitDown, "limit", 1, "Maximum number of down migrations to apply. 0 for unlimited.")
}
// Returns a DBCommand for applying migrations in the provided direction.
func runMigrate(direction migrate.MigrationDirection, limit *int) DBCommand {
return func(db *sql.DB, env *cmdline.Env, args []string) error {
migrate.SetTable(migrationsTable)
source := migrate.FileMigrationSource{
Dir: os.ExpandEnv(flagMigrationsDir),
}
if *flagDryRun {
planned, _, err := migrate.PlanMigration(db, sqlDialect, source, direction, *limit)
if err != nil {
return fmt.Errorf("Failed getting migrations to apply: %v", err)
}
for i, m := range planned {
fmt.Fprintf(env.Stderr, "#%d: %q\n", i, m.Migration.Id)
for _, q := range m.Queries {
fmt.Fprint(env.Stderr, q)
}
}
return nil
} else {
amount, err := migrate.ExecMax(db, sqlDialect, source, direction, *limit)
if err != nil {
return fmt.Errorf("Migration FAILED (applied %d migrations): %v", amount, err)
}
if logVerbose() {
fmt.Fprintf(env.Stderr, "Successfully applied %d migrations\n", amount)
}
return nil
}
}
}
// Command to be wrapped with runWithDBConn().
type DBCommand func(db *sql.DB, env *cmdline.Env, args []string) error
// runWithDBConn is a wrapper method that handles opening and closing the
// database connection.
func runWithDBConn(fx DBCommand) cmdline.RunnerFunc {
return func(env *cmdline.Env, args []string) (rerr error) {
if *flagSQLConf == "" {
return env.UsageErrorf("SQL configuration file (-sqlconf) must be provided")
}
// Open database connection from config,
db, err := dbutil.NewSqlDBConnFromFile(*flagSQLConf, "SERIALIZABLE")
if err != nil {
return fmt.Errorf("Error opening database connection: %v", err)
}
// Best effort close.
defer func() {
if cerr := db.Close(); cerr != nil {
cerr = fmt.Errorf("Failed closing database connection: %v", cerr)
rerr = lib.MergeErrors(rerr, cerr, "\n")
}
}()
// Ping database to check connection.
if err := db.Ping(); err != nil {
return fmt.Errorf("Error connecting to database: %v", err)
}
// Run wrapped function.
return fx(db, env, args)
}
}