github.com/paweljw/pop/v5@v5.4.6/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 mfs.Filter(func(mf Migration) bool { 94 return m.migrationIsCompatible(c.Dialect, mf) 95 }) 96 sort.Sort(mfs) 97 for _, mi := range mfs { 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 mfs.Filter(func(mf Migration) bool { 144 return m.migrationIsCompatible(c.Dialect, mf) 145 }) 146 sort.Sort(sort.Reverse(mfs)) 147 // skip all ran migration 148 if len(mfs) > count { 149 mfs = mfs[len(mfs)-count:] 150 } 151 // run only required steps 152 if step > 0 && len(mfs) >= step { 153 mfs = mfs[:step] 154 } 155 for _, mi := range mfs { 156 exists, err := c.Where("version = ?", mi.Version).Exists(mtn) 157 if err != nil { 158 return errors.Wrapf(err, "problem checking for migration version %s", mi.Version) 159 } 160 if !exists { 161 return errors.Errorf("migration version %s does not exist", mi.Version) 162 } 163 err = c.Transaction(func(tx *Connection) error { 164 err := mi.Run(tx) 165 if err != nil { 166 return err 167 } 168 err = tx.RawQuery(fmt.Sprintf("delete from %s where version = ?", mtn), mi.Version).Exec() 169 return errors.Wrapf(err, "problem deleting migration version %s", mi.Version) 170 }) 171 if err != nil { 172 return err 173 } 174 175 log(logging.Info, "< %s", mi.Name) 176 } 177 return nil 178 }) 179 } 180 181 // Reset the database by running the down migrations followed by the up migrations. 182 func (m Migrator) Reset() error { 183 err := m.Down(-1) 184 if err != nil { 185 return err 186 } 187 return m.Up() 188 } 189 190 // CreateSchemaMigrations sets up a table to track migrations. This is an idempotent 191 // operation. 192 func CreateSchemaMigrations(c *Connection) error { 193 mtn := c.MigrationTableName() 194 err := c.Open() 195 if err != nil { 196 return errors.Wrap(err, "could not open connection") 197 } 198 _, err = c.Store.Exec(fmt.Sprintf("select * from %s", mtn)) 199 if err == nil { 200 return nil 201 } 202 203 return c.Transaction(func(tx *Connection) error { 204 schemaMigrations := newSchemaMigrations(mtn) 205 smSQL, err := c.Dialect.FizzTranslator().CreateTable(schemaMigrations) 206 if err != nil { 207 return errors.Wrap(err, "could not build SQL for schema migration table") 208 } 209 err = tx.RawQuery(smSQL).Exec() 210 if err != nil { 211 return errors.Wrap(err, smSQL) 212 } 213 return nil 214 }) 215 } 216 217 // CreateSchemaMigrations sets up a table to track migrations. This is an idempotent 218 // operation. 219 func (m Migrator) CreateSchemaMigrations() error { 220 return CreateSchemaMigrations(m.Connection) 221 } 222 223 // Status prints out the status of applied/pending migrations. 224 func (m Migrator) Status(out io.Writer) error { 225 err := m.CreateSchemaMigrations() 226 if err != nil { 227 return err 228 } 229 w := tabwriter.NewWriter(out, 0, 0, 3, ' ', tabwriter.TabIndent) 230 _, _ = fmt.Fprintln(w, "Version\tName\tStatus\t") 231 for _, mf := range m.Migrations["up"] { 232 exists, err := m.Connection.Where("version = ?", mf.Version).Exists(m.Connection.MigrationTableName()) 233 if err != nil { 234 return errors.Wrapf(err, "problem with migration") 235 } 236 state := "Pending" 237 if exists { 238 state = "Applied" 239 } 240 _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t\n", mf.Version, mf.Name, state) 241 } 242 return w.Flush() 243 } 244 245 // DumpMigrationSchema will generate a file of the current database schema 246 // based on the value of Migrator.SchemaPath 247 func (m Migrator) DumpMigrationSchema() error { 248 if m.SchemaPath == "" { 249 return nil 250 } 251 c := m.Connection 252 schema := filepath.Join(m.SchemaPath, "schema.sql") 253 f, err := os.Create(schema) 254 if err != nil { 255 return err 256 } 257 err = c.Dialect.DumpSchema(f) 258 if err != nil { 259 os.RemoveAll(schema) 260 return err 261 } 262 return nil 263 } 264 265 func (m Migrator) exec(fn func() error) error { 266 now := time.Now() 267 defer func() { 268 err := m.DumpMigrationSchema() 269 if err != nil { 270 log(logging.Warn, "Migrator: unable to dump schema: %v", err) 271 } 272 }() 273 defer printTimer(now) 274 275 err := m.CreateSchemaMigrations() 276 if err != nil { 277 return errors.Wrap(err, "Migrator: problem creating schema migrations") 278 } 279 return fn() 280 } 281 282 func printTimer(timerStart time.Time) { 283 diff := time.Since(timerStart).Seconds() 284 if diff > 60 { 285 log(logging.Info, "%.4f minutes", diff/60) 286 } else { 287 log(logging.Info, "%.4f seconds", diff) 288 } 289 }