| // 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) |
| } |
| } |