woodpecker/pkg/database/migrate/migrate.go
Nurahmadie 51519b62dd Actually use --driver option to set database driver for drone.
This includes refactoring database setup, and migration system.

Remove setupDatabase from main and use `Init` method from database
package.

At database package, defines Init method which actually initiate
database with options given from comand line flag. I think `--path`
wont be used anywhere so I plan to remove it later.

Both meddler and migration initiated here, then we call `Set` method
to setup all the tables, etc. Here I think I want to separate database
schema and turn it into migration script instead, later maybe.

At migration package I made some tweak to `Operation` interface.
Realized that it's ludicrous to let migration driver re-implement `Exec`
and `Query`, I made migration script to receive The whole
migrationDriver struct which contains both Operation implementor, and
the Tx itself. This made possible thanks to Go struct being able to
promote its member, now our migration is more transparent.

There's also stub implementation for bot mysql and postgresql, will
implement this really soon.
2014-03-08 12:19:28 +07:00

212 lines
4.7 KiB
Go

// Usage
// migrate.To(2)
// .Add(Version_1)
// .Add(Version_2)
// .Add(Version_3)
// .Exec(db)
//
// migrate.ToLatest()
// .Add(Version_1)
// .Add(Version_2)
// .Add(Version_3)
// .SetDialect(migrate.MySQL)
// .Exec(db)
//
// migrate.ToLatest()
// .Add(Version_1)
// .Add(Version_2)
// .Add(Version_3)
// .Backup(path)
// .Exec()
package migrate
import (
"database/sql"
"log"
)
const migrationTableStmt = `
CREATE TABLE IF NOT EXISTS migration (
revision NUMBER PRIMARY KEY
)
`
const migrationSelectStmt = `
SELECT revision FROM migration
WHERE revision = ?
`
const migrationSelectMaxStmt = `
SELECT max(revision) FROM migration
`
const insertRevisionStmt = `
INSERT INTO migration (revision) VALUES (?)
`
const deleteRevisionStmt = `
DELETE FROM migration where revision = ?
`
// Operation interface covers basic migration operations.
// Implementation details is specific for each database,
// see migrate/sqlite.go for implementation reference.
type Operation interface {
CreateTable(tableName string, args []string) (sql.Result, error)
RenameTable(tableName, newName string) (sql.Result, error)
DropTable(tableName string) (sql.Result, error)
AddColumn(tableName, columnSpec string) (sql.Result, error)
DropColumns(tableName string, columnsToDrop []string) (sql.Result, error)
RenameColumns(tableName string, columnChanges map[string]string) (sql.Result, error)
}
type Revision interface {
Up(mg *MigrationDriver) error
Down(mg *MigrationDriver) error
Revision() int64
}
type MigrationDriver struct {
Tx *sql.Tx
Operation
}
type Migration struct {
db *sql.DB
revs []Revision
}
type DriverFunction func(tx *sql.Tx) *MigrationDriver
var Driver DriverFunction
func New(db *sql.DB) *Migration {
return &Migration{db: db}
}
// Add the Revision to the list of migrations.
func (m *Migration) Add(rev ...Revision) *Migration {
m.revs = append(m.revs, rev...)
return m
}
// Execute the full list of migrations.
func (m *Migration) Migrate() error {
var target int64
if len(m.revs) > 0 {
// get the last revision number in
// the list. This is what we'll
// migrate toward.
target = m.revs[len(m.revs)-1].Revision()
}
return m.MigrateTo(target)
}
// Execute all database migration until
// you are at the specified revision number.
// If the revision number is less than the
// current revision, then we will downgrade.
func (m *Migration) MigrateTo(target int64) error {
// make sure the migration table is created.
if _, err := m.db.Exec(migrationTableStmt); err != nil {
return err
}
// get the current revision
var current int64
m.db.QueryRow(migrationSelectMaxStmt).Scan(&current)
// already up to date
if current == target {
log.Println("Database already up-to-date.")
return nil
}
// should we downgrade?
if target < current {
return m.down(target, current)
}
// else upgrade
return m.up(target, current)
}
func (m *Migration) up(target, current int64) error {
// create the database transaction
tx, err := m.db.Begin()
if err != nil {
return err
}
mg := Driver(tx)
// loop through and execute revisions
for _, rev := range m.revs {
if rev.Revision() > current && rev.Revision() <= target {
current = rev.Revision()
// execute the revision Upgrade.
if err := rev.Up(mg); err != nil {
log.Printf("Failed to upgrade to Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
// update the revision number in the database
if _, err := tx.Exec(insertRevisionStmt, current); err != nil {
log.Printf("Failed to register Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
log.Printf("Successfully upgraded to Revision %v\n", current)
}
}
return tx.Commit()
}
func (m *Migration) down(target, current int64) error {
// create the database transaction
tx, err := m.db.Begin()
if err != nil {
return err
}
mg := Driver(tx)
// reverse the list of revisions
revs := []Revision{}
for _, rev := range m.revs {
revs = append([]Revision{rev}, revs...)
}
// loop through the (reversed) list of
// revisions and execute.
for _, rev := range revs {
if rev.Revision() > target {
current = rev.Revision()
// execute the revision Upgrade.
if err := rev.Down(mg); err != nil {
log.Printf("Failed to downgrade from Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
// update the revision number in the database
if _, err := tx.Exec(deleteRevisionStmt, current); err != nil {
log.Printf("Failed to unregistser Revision Number %v\n", current)
log.Println(err)
return tx.Rollback()
}
log.Printf("Successfully downgraded from Revision %v\n", current)
}
}
return tx.Commit()
}