github.com/solongordon/pop@v4.10.0+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 errors.WithStack(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 errors.WithStack(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.WithStack(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 errors.WithStack(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 errors.WithStack(err) 227 } 228 err = c.Dialect.DumpSchema(f) 229 if err != nil { 230 os.RemoveAll(schema) 231 return errors.WithStack(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 }