2017-05-04 20:58:23 -07:00
|
|
|
package dbmate
|
2015-11-25 10:57:58 -08:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
2020-11-19 15:04:42 +13:00
|
|
|
"errors"
|
2015-11-25 10:57:58 -08:00
|
|
|
"fmt"
|
2021-02-18 23:10:57 +01:00
|
|
|
"io"
|
2015-11-25 10:57:58 -08:00
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"regexp"
|
2016-08-15 22:42:07 -07:00
|
|
|
"sort"
|
2015-11-25 10:57:58 -08:00
|
|
|
"time"
|
2020-11-19 15:04:42 +13:00
|
|
|
|
|
|
|
|
"github.com/amacneil/dbmate/pkg/dbutil"
|
2015-11-25 10:57:58 -08:00
|
|
|
)
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// DefaultMigrationsDir specifies default directory to find migration files
|
2018-04-15 18:37:57 -07:00
|
|
|
const DefaultMigrationsDir = "./db/migrations"
|
2017-05-04 20:58:23 -07:00
|
|
|
|
2020-11-17 18:11:24 +13:00
|
|
|
// DefaultMigrationsTableName specifies default database tables to record migraitons in
|
|
|
|
|
const DefaultMigrationsTableName = "schema_migrations"
|
|
|
|
|
|
2018-01-22 20:38:40 -08:00
|
|
|
// DefaultSchemaFile specifies default location for schema.sql
|
2018-04-15 18:37:57 -07:00
|
|
|
const DefaultSchemaFile = "./db/schema.sql"
|
|
|
|
|
|
|
|
|
|
// DefaultWaitInterval specifies length of time between connection attempts
|
|
|
|
|
const DefaultWaitInterval = time.Second
|
|
|
|
|
|
|
|
|
|
// DefaultWaitTimeout specifies maximum time for connection attempts
|
|
|
|
|
const DefaultWaitTimeout = 60 * time.Second
|
2018-01-22 20:38:40 -08:00
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// DB allows dbmate actions to be performed on a specified database
|
|
|
|
|
type DB struct {
|
2020-11-17 18:11:24 +13:00
|
|
|
AutoDumpSchema bool
|
|
|
|
|
DatabaseURL *url.URL
|
|
|
|
|
MigrationsDir string
|
|
|
|
|
MigrationsTableName string
|
|
|
|
|
SchemaFile string
|
|
|
|
|
Verbose bool
|
|
|
|
|
WaitBefore bool
|
|
|
|
|
WaitInterval time.Duration
|
|
|
|
|
WaitTimeout time.Duration
|
2022-01-07 21:12:30 +01:00
|
|
|
Limit int
|
|
|
|
|
TargetVersion string
|
2021-02-18 23:10:57 +01:00
|
|
|
Log io.Writer
|
2017-05-04 20:58:23 -07:00
|
|
|
}
|
|
|
|
|
|
2020-02-27 23:24:11 -06:00
|
|
|
// migrationFileRegexp pattern for valid migration files
|
|
|
|
|
var migrationFileRegexp = regexp.MustCompile(`^\d.*\.sql$`)
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// StatusResult represents an available migration status
|
|
|
|
|
type StatusResult struct {
|
|
|
|
|
Filename string
|
|
|
|
|
Applied bool
|
2020-02-27 23:24:11 -06:00
|
|
|
}
|
|
|
|
|
|
2018-01-22 20:38:40 -08:00
|
|
|
// New initializes a new dbmate database
|
|
|
|
|
func New(databaseURL *url.URL) *DB {
|
2017-05-04 20:58:23 -07:00
|
|
|
return &DB{
|
2020-11-17 18:11:24 +13:00
|
|
|
AutoDumpSchema: true,
|
|
|
|
|
DatabaseURL: databaseURL,
|
|
|
|
|
MigrationsDir: DefaultMigrationsDir,
|
|
|
|
|
MigrationsTableName: DefaultMigrationsTableName,
|
|
|
|
|
SchemaFile: DefaultSchemaFile,
|
|
|
|
|
WaitBefore: false,
|
|
|
|
|
WaitInterval: DefaultWaitInterval,
|
|
|
|
|
WaitTimeout: DefaultWaitTimeout,
|
2022-01-07 21:12:30 +01:00
|
|
|
Limit: -1,
|
|
|
|
|
TargetVersion: "",
|
2021-02-18 23:10:57 +01:00
|
|
|
Log: os.Stdout,
|
2015-11-27 10:34:42 -08:00
|
|
|
}
|
2017-05-04 20:58:23 -07:00
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// GetDriver initializes the appropriate database driver
|
2017-05-04 20:58:23 -07:00
|
|
|
func (db *DB) GetDriver() (Driver, error) {
|
2020-11-19 15:04:42 +13:00
|
|
|
if db.DatabaseURL == nil || db.DatabaseURL.Scheme == "" {
|
2021-03-09 18:44:30 +11:00
|
|
|
return nil, errors.New("invalid url, have you set your --url flag or DATABASE_URL environment variable?")
|
2020-11-19 15:04:42 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
driverFunc := drivers[db.DatabaseURL.Scheme]
|
|
|
|
|
if driverFunc == nil {
|
|
|
|
|
return nil, fmt.Errorf("unsupported driver: %s", db.DatabaseURL.Scheme)
|
2020-11-17 18:11:24 +13:00
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
config := DriverConfig{
|
|
|
|
|
DatabaseURL: db.DatabaseURL,
|
|
|
|
|
MigrationsTableName: db.MigrationsTableName,
|
2021-02-18 23:10:57 +01:00
|
|
|
Log: db.Log,
|
2020-11-19 15:04:42 +13:00
|
|
|
}
|
2020-11-17 18:11:24 +13:00
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
return driverFunc(config), nil
|
2017-05-04 20:58:23 -07:00
|
|
|
}
|
2015-11-27 10:34:42 -08:00
|
|
|
|
2018-04-15 18:37:57 -07:00
|
|
|
// Wait blocks until the database server is available. It does not verify that
|
|
|
|
|
// the specified database exists, only that the host is ready to accept connections.
|
|
|
|
|
func (db *DB) Wait() error {
|
|
|
|
|
drv, err := db.GetDriver()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
return db.wait(drv)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (db *DB) wait(drv Driver) error {
|
2018-04-15 18:37:57 -07:00
|
|
|
// attempt connection to database server
|
2020-11-19 15:04:42 +13:00
|
|
|
err := drv.Ping()
|
2018-04-15 18:37:57 -07:00
|
|
|
if err == nil {
|
|
|
|
|
// connection successful
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprint(db.Log, "Waiting for database")
|
2018-04-15 18:37:57 -07:00
|
|
|
for i := 0 * time.Second; i < db.WaitTimeout; i += db.WaitInterval {
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprint(db.Log, ".")
|
2018-04-15 18:37:57 -07:00
|
|
|
time.Sleep(db.WaitInterval)
|
|
|
|
|
|
|
|
|
|
// attempt connection to database server
|
2020-11-19 15:04:42 +13:00
|
|
|
err = drv.Ping()
|
2018-04-15 18:37:57 -07:00
|
|
|
if err == nil {
|
|
|
|
|
// connection successful
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprint(db.Log, "\n")
|
2018-04-15 18:37:57 -07:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if we find outselves here, we could not connect within the timeout
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprint(db.Log, "\n")
|
2018-04-15 18:37:57 -07:00
|
|
|
return fmt.Errorf("unable to connect to database: %s", err)
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-22 20:38:40 -08:00
|
|
|
// CreateAndMigrate creates the database (if necessary) and runs migrations
|
|
|
|
|
func (db *DB) CreateAndMigrate() error {
|
2020-11-19 15:04:42 +13:00
|
|
|
drv, err := db.GetDriver()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-31 20:47:27 +00:00
|
|
|
if db.WaitBefore {
|
2020-11-19 15:04:42 +13:00
|
|
|
err := db.wait(drv)
|
2019-12-31 20:47:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-27 10:34:42 -08:00
|
|
|
// 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)
|
2020-11-19 15:04:42 +13:00
|
|
|
exists, err := drv.DatabaseExists()
|
2015-11-27 10:34:42 -08:00
|
|
|
if err == nil && !exists {
|
2020-11-19 15:04:42 +13:00
|
|
|
if err := drv.CreateDatabase(); err != nil {
|
2015-11-27 10:34:42 -08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// migrate
|
2020-11-19 15:04:42 +13:00
|
|
|
return db.migrate(drv)
|
2015-11-27 10:34:42 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Create creates the current database
|
|
|
|
|
func (db *DB) Create() error {
|
2020-11-19 15:04:42 +13:00
|
|
|
drv, err := db.GetDriver()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-31 20:47:27 +00:00
|
|
|
if db.WaitBefore {
|
2020-11-19 15:04:42 +13:00
|
|
|
err := db.wait(drv)
|
2019-12-31 20:47:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
return drv.CreateDatabase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop drops the current database (if it exists)
|
|
|
|
|
func (db *DB) Drop() error {
|
2017-05-04 20:58:23 -07:00
|
|
|
drv, err := db.GetDriver()
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-31 20:47:27 +00:00
|
|
|
if db.WaitBefore {
|
2020-11-19 15:04:42 +13:00
|
|
|
err := db.wait(drv)
|
2019-12-31 20:47:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
return drv.DropDatabase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DumpSchema writes the current database schema to a file
|
|
|
|
|
func (db *DB) DumpSchema() error {
|
2017-05-04 20:58:23 -07:00
|
|
|
drv, err := db.GetDriver()
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
return db.dumpSchema(drv)
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
func (db *DB) dumpSchema(drv Driver) error {
|
2019-12-31 20:47:27 +00:00
|
|
|
if db.WaitBefore {
|
2020-11-19 15:04:42 +13:00
|
|
|
err := db.wait(drv)
|
2019-12-31 20:47:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
sqlDB, err := db.openDatabaseForMigration(drv)
|
2018-01-22 20:38:40 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2020-11-19 15:04:42 +13:00
|
|
|
defer dbutil.MustClose(sqlDB)
|
2018-01-22 20:38:40 -08:00
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
schema, err := drv.DumpSchema(sqlDB)
|
2018-01-22 20:38:40 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprintf(db.Log, "Writing: %s\n", db.SchemaFile)
|
2018-01-22 20:38:40 -08:00
|
|
|
|
|
|
|
|
// ensure schema directory exists
|
|
|
|
|
if err = ensureDir(filepath.Dir(db.SchemaFile)); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// write schema to file
|
2021-08-27 12:13:37 +08:00
|
|
|
return os.WriteFile(db.SchemaFile, schema, 0644)
|
2018-01-22 20:38:40 -08:00
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 10:57:58 -08:00
|
|
|
const migrationTemplate = "-- migrate:up\n\n\n-- migrate:down\n\n"
|
|
|
|
|
|
2018-01-22 20:38:40 -08:00
|
|
|
// NewMigration creates a new migration file
|
|
|
|
|
func (db *DB) NewMigration(name string) error {
|
2015-11-25 10:57:58 -08:00
|
|
|
// new migration name
|
|
|
|
|
timestamp := time.Now().UTC().Format("20060102150405")
|
|
|
|
|
if name == "" {
|
2017-05-04 20:58:23 -07:00
|
|
|
return fmt.Errorf("please specify a name for the new migration")
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
name = fmt.Sprintf("%s_%s.sql", timestamp, name)
|
|
|
|
|
|
|
|
|
|
// create migrations dir if missing
|
2018-01-22 20:38:40 -08:00
|
|
|
if err := ensureDir(db.MigrationsDir); err != nil {
|
|
|
|
|
return err
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// check file does not already exist
|
2017-05-04 20:58:23 -07:00
|
|
|
path := filepath.Join(db.MigrationsDir, name)
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprintf(db.Log, "Creating migration: %s\n", path)
|
2015-11-25 10:57:58 -08:00
|
|
|
|
|
|
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
2017-05-04 20:58:23 -07:00
|
|
|
return fmt.Errorf("file already exists")
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// write new migration
|
|
|
|
|
file, err := os.Create(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
defer dbutil.MustClose(file)
|
2015-11-25 10:57:58 -08:00
|
|
|
_, err = file.WriteString(migrationTemplate)
|
2018-04-15 19:59:56 -07:00
|
|
|
return err
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
func doTransaction(sqlDB *sql.DB, txFunc func(dbutil.Transaction) error) error {
|
|
|
|
|
tx, err := sqlDB.Begin()
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := txFunc(tx); err != nil {
|
2015-11-27 14:27:44 -08:00
|
|
|
if err1 := tx.Rollback(); err1 != nil {
|
|
|
|
|
return err1
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 10:57:58 -08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tx.Commit()
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
func (db *DB) openDatabaseForMigration(drv Driver) (*sql.DB, error) {
|
|
|
|
|
sqlDB, err := drv.Open()
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
2020-11-19 15:04:42 +13:00
|
|
|
return nil, err
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
if err := drv.CreateMigrationsTable(sqlDB); err != nil {
|
|
|
|
|
dbutil.MustClose(sqlDB)
|
|
|
|
|
return nil, err
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
return sqlDB, nil
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Migrate migrates database to the latest version
|
|
|
|
|
func (db *DB) Migrate() error {
|
2020-11-19 15:04:42 +13:00
|
|
|
drv, err := db.GetDriver()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return db.migrate(drv)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (db *DB) migrate(drv Driver) error {
|
2020-02-27 23:24:11 -06:00
|
|
|
files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp)
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
if len(files) == 0 {
|
2017-05-04 20:58:23 -07:00
|
|
|
return fmt.Errorf("no migration files found")
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2019-12-31 20:47:27 +00:00
|
|
|
if db.WaitBefore {
|
2020-11-19 15:04:42 +13:00
|
|
|
err := db.wait(drv)
|
2019-12-31 20:47:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
sqlDB, err := db.openDatabaseForMigration(drv)
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
2015-11-25 10:57:58 -08:00
|
|
|
return err
|
|
|
|
|
}
|
2020-11-19 15:04:42 +13:00
|
|
|
defer dbutil.MustClose(sqlDB)
|
2015-11-25 10:57:58 -08:00
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
applied, err := drv.SelectMigrations(sqlDB, db.Limit)
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
for _, filename := range files {
|
2015-11-25 10:57:58 -08:00
|
|
|
ver := migrationVersion(filename)
|
2022-01-07 21:12:30 +01:00
|
|
|
if ok := applied[ver]; ok && ver != db.TargetVersion {
|
2015-11-25 10:57:58 -08:00
|
|
|
// migration already applied
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprintf(db.Log, "Applying: %s\n", filename)
|
2015-11-25 10:57:58 -08:00
|
|
|
|
2019-05-16 19:39:47 -07:00
|
|
|
up, _, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
execMigration := func(tx dbutil.Transaction) error {
|
2015-11-25 10:57:58 -08:00
|
|
|
// run actual migration
|
2020-06-25 12:26:09 -07:00
|
|
|
result, err := tx.Exec(up.Contents)
|
|
|
|
|
if err != nil {
|
2015-11-25 10:57:58 -08:00
|
|
|
return err
|
2020-06-25 12:26:09 -07:00
|
|
|
} else if db.Verbose {
|
2021-02-18 23:10:57 +01:00
|
|
|
db.printVerbose(result)
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// record migration
|
2018-01-07 12:52:10 -08:00
|
|
|
return drv.InsertMigration(tx, ver)
|
2019-05-16 19:39:47 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if up.Options.Transaction() {
|
|
|
|
|
// begin transaction
|
|
|
|
|
err = doTransaction(sqlDB, execMigration)
|
|
|
|
|
} else {
|
|
|
|
|
// run outside of transaction
|
|
|
|
|
err = execMigration(sqlDB)
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-27 13:55:14 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-01-07 21:12:30 +01:00
|
|
|
|
|
|
|
|
if ver == db.TargetVersion {
|
|
|
|
|
fmt.Fprintf(db.Log, "Reached target version %s\n", ver)
|
|
|
|
|
break
|
|
|
|
|
}
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2018-01-22 20:38:40 -08:00
|
|
|
// automatically update schema file, silence errors
|
|
|
|
|
if db.AutoDumpSchema {
|
2020-11-19 15:04:42 +13:00
|
|
|
_ = db.dumpSchema(drv)
|
2018-01-22 20:38:40 -08:00
|
|
|
}
|
|
|
|
|
|
2015-11-25 10:57:58 -08:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-18 23:10:57 +01:00
|
|
|
func (db *DB) printVerbose(result sql.Result) {
|
2020-11-19 15:04:42 +13:00
|
|
|
lastInsertID, err := result.LastInsertId()
|
|
|
|
|
if err == nil {
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprintf(db.Log, "Last insert ID: %d\n", lastInsertID)
|
2020-11-19 15:04:42 +13:00
|
|
|
}
|
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
|
|
|
if err == nil {
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprintf(db.Log, "Rows affected: %d\n", rowsAffected)
|
2020-11-19 15:04:42 +13:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
func findMigrationFiles(dir string, re *regexp.Regexp) ([]string, error) {
|
2021-08-27 12:13:37 +08:00
|
|
|
files, err := os.ReadDir(dir)
|
2015-11-25 10:57:58 -08:00
|
|
|
if err != nil {
|
2017-05-04 20:58:23 -07:00
|
|
|
return nil, fmt.Errorf("could not find migrations directory `%s`", dir)
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
matches := []string{}
|
2015-11-25 10:57:58 -08:00
|
|
|
for _, file := range files {
|
|
|
|
|
if file.IsDir() {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
name := file.Name()
|
2015-11-25 12:26:57 -08:00
|
|
|
if !re.MatchString(name) {
|
2015-11-25 10:57:58 -08:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
matches = append(matches, name)
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
sort.Strings(matches)
|
2015-11-25 10:57:58 -08:00
|
|
|
|
2016-08-15 22:42:07 -07:00
|
|
|
return matches, nil
|
2015-11-25 10:57:58 -08:00
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
func findMigrationFile(dir string, ver string) (string, error) {
|
|
|
|
|
if ver == "" {
|
|
|
|
|
panic("migration version is required")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ver = regexp.QuoteMeta(ver)
|
|
|
|
|
re := regexp.MustCompile(fmt.Sprintf(`^%s.*\.sql$`, ver))
|
|
|
|
|
|
|
|
|
|
files, err := findMigrationFiles(dir, re)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(files) == 0 {
|
2017-05-04 20:58:23 -07:00
|
|
|
return "", fmt.Errorf("can't find migration file: %s*.sql", ver)
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return files[0], nil
|
|
|
|
|
}
|
|
|
|
|
|
2015-11-25 10:57:58 -08:00
|
|
|
func migrationVersion(filename string) string {
|
|
|
|
|
return regexp.MustCompile(`^\d+`).FindString(filename)
|
|
|
|
|
}
|
|
|
|
|
|
2017-05-04 20:58:23 -07:00
|
|
|
// Rollback rolls back the most recent migration
|
|
|
|
|
func (db *DB) Rollback() error {
|
2020-11-19 15:04:42 +13:00
|
|
|
drv, err := db.GetDriver()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-31 20:47:27 +00:00
|
|
|
if db.WaitBefore {
|
2020-11-19 15:04:42 +13:00
|
|
|
err := db.wait(drv)
|
2019-12-31 20:47:27 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
sqlDB, err := db.openDatabaseForMigration(drv)
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2020-11-19 15:04:42 +13:00
|
|
|
defer dbutil.MustClose(sqlDB)
|
2015-11-25 12:26:57 -08:00
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
limit := db.Limit
|
|
|
|
|
// default limit is -1, if we don't specify a version it should only rollback one version, not all
|
|
|
|
|
if limit <= 0 && db.TargetVersion == "" {
|
|
|
|
|
limit = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applied, err := drv.SelectMigrations(sqlDB, limit)
|
2015-11-25 12:26:57 -08:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
if len(applied) == 0 {
|
|
|
|
|
return fmt.Errorf("can't rollback, no migrations found")
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
var versions []string
|
|
|
|
|
for v := range applied {
|
|
|
|
|
versions = append(versions, v)
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
// new → old
|
|
|
|
|
sort.Sort(sort.Reverse(sort.StringSlice(versions)))
|
2015-11-25 12:26:57 -08:00
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
if db.TargetVersion != "" {
|
|
|
|
|
cache := map[string]bool{}
|
|
|
|
|
found := false
|
|
|
|
|
|
|
|
|
|
// latest version comes first, so take every version until the version matches
|
|
|
|
|
for _, ver := range versions {
|
|
|
|
|
if ver == db.TargetVersion {
|
|
|
|
|
found = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
cache[ver] = true
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
return fmt.Errorf("target version not found")
|
|
|
|
|
}
|
|
|
|
|
applied = cache
|
2015-11-25 12:26:57 -08:00
|
|
|
}
|
|
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
for version := range applied {
|
|
|
|
|
filename, err := findMigrationFile(db.MigrationsDir, version)
|
2020-06-25 12:26:09 -07:00
|
|
|
if err != nil {
|
2015-11-25 12:26:57 -08:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
fmt.Fprintf(db.Log, "Rolling back: %s\n", filename)
|
|
|
|
|
_, down, err := parseMigration(filepath.Join(db.MigrationsDir, filename))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2019-05-16 19:39:47 -07:00
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
execMigration := func(tx dbutil.Transaction) error {
|
|
|
|
|
// rollback migration
|
|
|
|
|
result, err := tx.Exec(down.Contents)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else if db.Verbose {
|
|
|
|
|
db.printVerbose(result)
|
|
|
|
|
}
|
2019-05-16 19:39:47 -07:00
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
// remove migration record
|
|
|
|
|
return drv.DeleteMigration(tx, version)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if down.Options.Transaction() {
|
|
|
|
|
// begin transaction
|
|
|
|
|
err = doTransaction(sqlDB, execMigration)
|
|
|
|
|
} else {
|
|
|
|
|
// run outside of transaction
|
|
|
|
|
err = execMigration(sqlDB)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2015-11-27 13:55:14 -08:00
|
|
|
}
|
2015-11-25 12:26:57 -08:00
|
|
|
|
2018-01-22 20:38:40 -08:00
|
|
|
// automatically update schema file, silence errors
|
|
|
|
|
if db.AutoDumpSchema {
|
2020-11-19 15:04:42 +13:00
|
|
|
_ = db.dumpSchema(drv)
|
2018-01-22 20:38:40 -08:00
|
|
|
}
|
|
|
|
|
|
2015-11-25 12:26:57 -08:00
|
|
|
return nil
|
|
|
|
|
}
|
2020-02-27 23:24:11 -06:00
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
// Status shows the status of all migrations
|
|
|
|
|
func (db *DB) Status(quiet bool) (int, error) {
|
|
|
|
|
drv, err := db.GetDriver()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return -1, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results, err := db.CheckMigrationsStatus(drv)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return -1, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var totalApplied int
|
|
|
|
|
var line string
|
|
|
|
|
|
|
|
|
|
for _, res := range results {
|
|
|
|
|
if res.Applied {
|
|
|
|
|
line = fmt.Sprintf("[X] %s", res.Filename)
|
|
|
|
|
totalApplied++
|
|
|
|
|
} else {
|
|
|
|
|
line = fmt.Sprintf("[ ] %s", res.Filename)
|
|
|
|
|
}
|
|
|
|
|
if !quiet {
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprintln(db.Log, line)
|
2020-11-19 15:04:42 +13:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
totalPending := len(results) - totalApplied
|
|
|
|
|
if !quiet {
|
2021-02-18 23:10:57 +01:00
|
|
|
fmt.Fprintln(db.Log)
|
|
|
|
|
fmt.Fprintf(db.Log, "Applied: %d\n", totalApplied)
|
|
|
|
|
fmt.Fprintf(db.Log, "Pending: %d\n", totalPending)
|
2020-11-19 15:04:42 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return totalPending, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CheckMigrationsStatus returns the status of all available mgirations
|
|
|
|
|
func (db *DB) CheckMigrationsStatus(drv Driver) ([]StatusResult, error) {
|
2020-02-27 23:24:11 -06:00
|
|
|
files, err := findMigrationFiles(db.MigrationsDir, migrationFileRegexp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(files) == 0 {
|
|
|
|
|
return nil, fmt.Errorf("no migration files found")
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
sqlDB, err := db.openDatabaseForMigration(drv)
|
2020-02-27 23:24:11 -06:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2020-11-19 15:04:42 +13:00
|
|
|
defer dbutil.MustClose(sqlDB)
|
2020-02-27 23:24:11 -06:00
|
|
|
|
2022-01-07 21:12:30 +01:00
|
|
|
applied, err := drv.SelectMigrations(sqlDB, db.Limit)
|
2020-02-27 23:24:11 -06:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-19 15:04:42 +13:00
|
|
|
var results []StatusResult
|
2020-02-27 23:24:11 -06:00
|
|
|
|
|
|
|
|
for _, filename := range files {
|
|
|
|
|
ver := migrationVersion(filename)
|
2020-11-19 15:04:42 +13:00
|
|
|
res := StatusResult{Filename: filename}
|
2020-02-27 23:24:11 -06:00
|
|
|
if ok := applied[ver]; ok {
|
2020-11-19 15:04:42 +13:00
|
|
|
res.Applied = true
|
2020-02-27 23:24:11 -06:00
|
|
|
} else {
|
2020-11-19 15:04:42 +13:00
|
|
|
res.Applied = false
|
2020-02-27 23:24:11 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results = append(results, res)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return results, nil
|
|
|
|
|
}
|