github.com/rjgonzale/pop/v5@v5.1.3-dev/migrator.go (about)

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