github.com/pjdufour-truss/pop@v4.11.2-0.20190705085848-4c90b0ff4d5a+incompatible/migrator.go (about)

     1  package pop
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"regexp"
     8  	"sort"
     9  	"text/tabwriter"
    10  	"time"
    11  
    12  	"github.com/gobuffalo/pop/logging"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  var mrx = regexp.MustCompile(`^(\d+)_([^.]+)(\.[a-z0-9]+)?\.(up|down)\.(sql|fizz)$`)
    17  
    18  // NewMigrator returns a new "blank" migrator. It is recommended
    19  // to use something like MigrationBox or FileMigrator. A "blank"
    20  // Migrator should only be used as the basis for a new type of
    21  // migration system.
    22  func NewMigrator(c *Connection) Migrator {
    23  	return Migrator{
    24  		Connection: c,
    25  		Migrations: map[string]Migrations{
    26  			"up":   {},
    27  			"down": {},
    28  		},
    29  	}
    30  }
    31  
    32  // Migrator forms the basis of all migrations systems.
    33  // It does the actual heavy lifting of running migrations.
    34  // When building a new migration system, you should embed this
    35  // type into your migrator.
    36  type Migrator struct {
    37  	Connection *Connection
    38  	SchemaPath string
    39  	Migrations map[string]Migrations
    40  }
    41  
    42  // UpLogOnly insert pending "up" migrations logs only, without applying the patch.
    43  // It's used when loading the schema dump, instead of the migrations.
    44  func (m Migrator) UpLogOnly() error {
    45  	c := m.Connection
    46  	return m.exec(func() error {
    47  		mtn := c.MigrationTableName()
    48  		mfs := m.Migrations["up"]
    49  		sort.Sort(mfs)
    50  		return c.Transaction(func(tx *Connection) error {
    51  			for _, mi := range mfs {
    52  				if mi.DBType != "all" && mi.DBType != c.Dialect.Name() {
    53  					// Skip migration for non-matching dialect
    54  					continue
    55  				}
    56  				exists, err := c.Where("version = ?", mi.Version).Exists(mtn)
    57  				if err != nil {
    58  					return errors.Wrapf(err, "problem checking for migration version %s", mi.Version)
    59  				}
    60  				if exists {
    61  					continue
    62  				}
    63  				_, err = tx.Store.Exec(fmt.Sprintf("insert into %s (version) values ('%s')", mtn, mi.Version))
    64  				if err != nil {
    65  					return errors.Wrapf(err, "problem inserting migration version %s", mi.Version)
    66  				}
    67  			}
    68  			return nil
    69  		})
    70  	})
    71  }
    72  
    73  // Up runs pending "up" migrations and applies them to the database.
    74  func (m Migrator) Up() error {
    75  	c := m.Connection
    76  	return m.exec(func() error {
    77  		mtn := c.MigrationTableName()
    78  		mfs := m.Migrations["up"]
    79  		sort.Sort(mfs)
    80  		applied := 0
    81  		for _, mi := range mfs {
    82  			if mi.DBType != "all" && mi.DBType != c.Dialect.Name() {
    83  				// Skip migration for non-matching dialect
    84  				continue
    85  			}
    86  			exists, err := c.Where("version = ?", mi.Version).Exists(mtn)
    87  			if err != nil {
    88  				return errors.Wrapf(err, "problem checking for migration version %s", mi.Version)
    89  			}
    90  			if exists {
    91  				continue
    92  			}
    93  			err = c.Transaction(func(tx *Connection) error {
    94  				err := mi.Run(tx)
    95  				if err != nil {
    96  					return err
    97  				}
    98  				_, err = tx.Store.Exec(fmt.Sprintf("insert into %s (version) values ('%s')", mtn, mi.Version))
    99  				return errors.Wrapf(err, "problem inserting migration version %s", mi.Version)
   100  			})
   101  			if err != nil {
   102  				return err
   103  			}
   104  			log(logging.Info, "> %s", mi.Name)
   105  			applied++
   106  		}
   107  		if applied == 0 {
   108  			log(logging.Info, "Migrations already up to date, nothing to apply")
   109  		}
   110  		return nil
   111  	})
   112  }
   113  
   114  // Down runs pending "down" migrations and rolls back the
   115  // database by the specified number of steps.
   116  func (m Migrator) Down(step int) error {
   117  	c := m.Connection
   118  	return m.exec(func() error {
   119  		mtn := c.MigrationTableName()
   120  		count, err := c.Count(mtn)
   121  		if err != nil {
   122  			return errors.Wrap(err, "migration down: unable count existing migration")
   123  		}
   124  		mfs := m.Migrations["down"]
   125  		sort.Sort(sort.Reverse(mfs))
   126  		// skip all runned migration
   127  		if len(mfs) > count {
   128  			mfs = mfs[len(mfs)-count:]
   129  		}
   130  		// run only required steps
   131  		if step > 0 && len(mfs) >= step {
   132  			mfs = mfs[:step]
   133  		}
   134  		for _, mi := range mfs {
   135  			exists, err := c.Where("version = ?", mi.Version).Exists(mtn)
   136  			if err != nil || !exists {
   137  				return errors.Wrapf(err, "problem checking for migration version %s", mi.Version)
   138  			}
   139  			err = c.Transaction(func(tx *Connection) error {
   140  				err := mi.Run(tx)
   141  				if err != nil {
   142  					return err
   143  				}
   144  				err = tx.RawQuery(fmt.Sprintf("delete from %s where version = ?", mtn), mi.Version).Exec()
   145  				return errors.Wrapf(err, "problem deleting migration version %s", mi.Version)
   146  			})
   147  			if err != nil {
   148  				return err
   149  			}
   150  
   151  			log(logging.Info, "< %s", mi.Name)
   152  		}
   153  		return nil
   154  	})
   155  }
   156  
   157  // Reset the database by running the down migrations followed by the up migrations.
   158  func (m Migrator) Reset() error {
   159  	err := m.Down(-1)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	return m.Up()
   164  }
   165  
   166  // CreateSchemaMigrations sets up a table to track migrations. This is an idempotent
   167  // operation.
   168  func (m Migrator) CreateSchemaMigrations() error {
   169  	c := m.Connection
   170  	mtn := c.MigrationTableName()
   171  	err := c.Open()
   172  	if err != nil {
   173  		return errors.Wrap(err, "could not open connection")
   174  	}
   175  	_, err = c.Store.Exec(fmt.Sprintf("select * from %s", mtn))
   176  	if err == nil {
   177  		return nil
   178  	}
   179  
   180  	return c.Transaction(func(tx *Connection) error {
   181  		schemaMigrations := newSchemaMigrations(mtn)
   182  		smSQL, err := c.Dialect.FizzTranslator().CreateTable(schemaMigrations)
   183  		if err != nil {
   184  			return errors.Wrap(err, "could not build SQL for schema migration table")
   185  		}
   186  		err = tx.RawQuery(smSQL).Exec()
   187  		if err != nil {
   188  			return errors.Wrap(err, smSQL)
   189  		}
   190  		return nil
   191  	})
   192  }
   193  
   194  // Status prints out the status of applied/pending migrations.
   195  func (m Migrator) Status() error {
   196  	err := m.CreateSchemaMigrations()
   197  	if err != nil {
   198  		return err
   199  	}
   200  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent)
   201  	fmt.Fprintln(w, "Version\tName\tStatus\t")
   202  	for _, mf := range m.Migrations["up"] {
   203  		exists, err := m.Connection.Where("version = ?", mf.Version).Exists(m.Connection.MigrationTableName())
   204  		if err != nil {
   205  			return errors.Wrapf(err, "problem with migration")
   206  		}
   207  		state := "Pending"
   208  		if exists {
   209  			state = "Applied"
   210  		}
   211  		fmt.Fprintf(w, "%s\t%s\t%s\t\n", mf.Version, mf.Name, state)
   212  	}
   213  	return w.Flush()
   214  }
   215  
   216  // DumpMigrationSchema will generate a file of the current database schema
   217  // based on the value of Migrator.SchemaPath
   218  func (m Migrator) DumpMigrationSchema() error {
   219  	if m.SchemaPath == "" {
   220  		return nil
   221  	}
   222  	c := m.Connection
   223  	schema := filepath.Join(m.SchemaPath, "schema.sql")
   224  	f, err := os.Create(schema)
   225  	if err != nil {
   226  		return err
   227  	}
   228  	err = c.Dialect.DumpSchema(f)
   229  	if err != nil {
   230  		os.RemoveAll(schema)
   231  		return err
   232  	}
   233  	return nil
   234  }
   235  
   236  func (m Migrator) exec(fn func() error) error {
   237  	now := time.Now()
   238  	defer func() {
   239  		err := m.DumpMigrationSchema()
   240  		if err != nil {
   241  			log(logging.Warn, "Migrator: unable to dump schema: %v", err)
   242  		}
   243  	}()
   244  	defer printTimer(now)
   245  
   246  	err := m.CreateSchemaMigrations()
   247  	if err != nil {
   248  		return errors.Wrap(err, "Migrator: problem creating schema migrations")
   249  	}
   250  	return fn()
   251  }
   252  
   253  func printTimer(timerStart time.Time) {
   254  	diff := time.Since(timerStart).Seconds()
   255  	if diff > 60 {
   256  		log(logging.Info, "%.4f minutes", diff/60)
   257  	} else {
   258  		log(logging.Info, "%.4f seconds", diff)
   259  	}
   260  }