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