github.com/schparky/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 }