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 }