Skip to content

Commit

Permalink
big QueryBox refactor, switch to sql.Tx (#12)
Browse files Browse the repository at this point in the history
- the main goal is to make it easy to have RDBMS-specific code branches
- command runner functions handle database connection (if required)
- defer to other functions depending on database-specific functionality
- this pattern keeps related command code together
  - the other way to slice the pie is to keep RDBMS-specific code together (`postgres.go`, `mysql.go`)
  - but, the functionality is so similar, this should help promote code reuse
  - also, don't want a command with one database to change much from another database
- keeping `QueryBox` since it keeps a lot of the code simple
- the duplicative `if dbox.Type ==` calls isn't the best, and what to do for the `else`?
  - adding `if dbox.IsPostgres` helps, auto completion of methods
  - in the `else` just doing a `panic` since it's essentially unreachable
  • Loading branch information
tlhunter authored Feb 13, 2023
1 parent 78d9349 commit b745bf7
Show file tree
Hide file tree
Showing 20 changed files with 336 additions and 214 deletions.
25 changes: 4 additions & 21 deletions commands/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ type CommandUpFamilyResult struct {

func CommandAll(cfg config.MigConfig) result.Response {
dbox, err := database.Connect(cfg.Connection)

if err != nil {
return *result.NewErrorWithDetails("database connection error", "db_conn", err)
}
Expand All @@ -25,25 +24,20 @@ func CommandAll(cfg config.MigConfig) result.Response {

// First call to GetStatus is mostly unused. if it fails then don't continue.
status, err := migrations.GetStatus(cfg, dbox)

if err != nil {
return *result.NewErrorWithDetails("Encountered an error trying to get migrations status!", "retrieve_status", err)
}

if status.Skipped > 0 {
return *result.NewError("Refusing to run with skipped migrations! Run `mig status` for details.", "abort_skipped_migrations")
}

if status.Next == "" {
return *result.NewError("There are no migrations to run.", "no_migrations")
}

locked, err := database.ObtainLock(dbox)

if err != nil {
return *result.NewErrorWithDetails("Error obtaining lock for migration!", "obtain_lock", err)
}

if !locked {
return *result.NewError("Unable to obtain lock for migration!", "obtain_lock")
}
Expand All @@ -60,39 +54,30 @@ func CommandAll(cfg config.MigConfig) result.Response {

for {
status, err := migrations.GetStatus(cfg, dbox)
if err != nil {
return *result.NewErrorWithDetails("Encountered an error trying to get migrations status!", "retrieve_status", err)
}

next := status.Next

if next == "" {
break
}

filename := cfg.Migrations + "/" + next

queries, err := migrations.GetQueriesFromFile(filename)

if err != nil {
return *result.NewErrorWithDetails("Error attempting to read next migration file!", "read_next_migration", err)
}

var query string

if queries.UpTx {
query = BEGIN.For(dbox.Type) + queries.Up + END.For(dbox.Type)
} else {
query = queries.Up
}

_, err = dbox.Db.Exec(query)

err = dbox.ExecMaybeTx(queries.Up, queries.UpTx)
if err != nil {
return *result.NewErrorWithDetails("Encountered an error while running migration!", "migration_failed", err)
}

res.AddSuccessLn(color.GreenString("Migration %s was successfully applied!", next))

migration, err := migrations.AddMigrationWithBatch(dbox, next, batchId)

if err != nil {
res.SetError("The migration query executed but unable to track it in the migrations table!", "untracked_migration")
res.SetErrorDetails(err)
Expand All @@ -105,12 +90,10 @@ func CommandAll(cfg config.MigConfig) result.Response {
}

released, err := database.ReleaseLock(dbox)

if err != nil {
res.SetError("Error releasing lock after running migration!", "release_lock")
return *res
}

if !released {
res.SetError("Unable to release lock after running migration!", "release_lock")
}
Expand Down
19 changes: 1 addition & 18 deletions commands/down.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,25 @@ import (

func CommandDown(cfg config.MigConfig) result.Response {
dbox, err := database.Connect(cfg.Connection)

if err != nil {
return *result.NewErrorWithDetails("database connection error", "db_conn", err)
}

defer dbox.Db.Close()

status, err := migrations.GetStatus(cfg, dbox)

if err != nil {
return *result.NewErrorWithDetails("Encountered an error trying to get migrations status!", "retrieve_status", err)
}

last := status.Last

if last == nil {
return *result.NewError("There are no migrations to revert.", "nothing_to_revert")
}

filename := cfg.Migrations + "/" + last.Name

queries, err := migrations.GetQueriesFromFile(filename)

if err != nil {
res := *result.NewErrorWithDetails("Error attempting to read last migration file!", "unable_read_migration_file", err)

Expand All @@ -44,33 +40,21 @@ func CommandDown(cfg config.MigConfig) result.Response {
}

locked, err := database.ObtainLock(dbox)

if err != nil {
return *result.NewErrorWithDetails("Error obtaining lock for migration down!", "obtain_lock", err)
}

if !locked {
return *result.NewError("Unable to obtain lock for migrating down!", "obtain_lock")
}

var query string

if queries.DownTx {
query = BEGIN.For(dbox.Type) + queries.Down + END.For(dbox.Type)
} else {
query = queries.Down
}

_, err = dbox.Db.Exec(query)

err = dbox.ExecMaybeTx(queries.Down, queries.DownTx)
if err != nil {
return *result.NewErrorWithDetails("Encountered an error while running down migration!", "migration_failed", err)
}

res := result.NewSuccess(fmt.Sprintf("Down migration for %s was successfully applied!", last.Name))

err = migrations.RemoveMigration(dbox, last.Name, last.Id)

if err != nil {
res.SetError("The migration down query executed but unable to track it in the migrations table!", "untracked_migration")
res.SetErrorDetails(err)
Expand All @@ -79,7 +63,6 @@ func CommandDown(cfg config.MigConfig) result.Response {
}

released, err := database.ReleaseLock(dbox)

if err != nil {
res.SetError("Error obtaining lock for down migration!", "release_lock")
return *res
Expand Down
116 changes: 85 additions & 31 deletions commands/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,103 @@ import (
"github.com/tlhunter/mig/result"
)

var (
INIT = database.QueryBox{
Postgres: `CREATE TABLE migrations (
id serial NOT NULL,
name varchar(255) NULL,
batch int4 NULL,
migration_time timestamptz NULL,
CONSTRAINT migrations_pkey PRIMARY KEY (id)
);
CREATE TABLE migrations_lock (
"index" serial NOT NULL,
is_locked int4 NULL,
CONSTRAINT migrations_lock_pkey PRIMARY KEY (index)
);
INSERT INTO migrations_lock ("index", is_locked) VALUES(1, 0);`,
Mysql: `CREATE TABLE migrations (
id serial NOT NULL PRIMARY KEY,
name varchar(255) NULL,
batch int4 NULL,
migration_time TIMESTAMP NULL
);
CREATE TABLE migrations_lock (
` + "`index`" + ` serial NOT NULL PRIMARY KEY,
is_locked int4 NULL
);
INSERT INTO migrations_lock SET ` + "`index`" + ` = 1, is_locked = 0;`,
}
)

func CommandInit(cfg config.MigConfig) result.Response {
dbox, err := database.Connect(cfg.Connection)

if err != nil {
return *result.NewErrorWithDetails("database connection error", "db_conn", err)
}

defer dbox.Db.Close()

_, err = dbox.Exec(INIT)
if dbox.IsPostgres {
err = postgresInit(dbox)
} else if dbox.IsMysql {
err = mysqlInit(dbox)
} else {
panic("unknown database: " + dbox.Type)
}

if err != nil {
return *result.NewErrorWithDetails("error initializing mig!", "unable_init", err)
}

return *result.NewSuccess("successfully initialized mig")
}

func postgresInit(dbox database.DbBox) error {
tx, err := dbox.Db.Begin()
if err != nil {
return err
}

defer tx.Rollback()

_, err = tx.Exec(`CREATE TABLE migrations (
id serial NOT NULL,
name varchar(255) NULL,
batch int4 NULL,
migration_time timestamptz NULL,
CONSTRAINT migrations_pkey PRIMARY KEY (id)
);`)
if err != nil {
return err
}

_, err = tx.Exec(`CREATE TABLE migrations_lock (
"index" serial NOT NULL,
is_locked int4 NULL,
CONSTRAINT migrations_lock_pkey PRIMARY KEY (index)
);`)
if err != nil {
return err
}

_, err = tx.Exec(`INSERT INTO migrations_lock ("index", is_locked) VALUES(1, 0);`)
if err != nil {
return err
}

if err = tx.Commit(); err != nil {
return err
}

return nil
}

func mysqlInit(dbox database.DbBox) error {
tx, err := dbox.Db.Begin()
if err != nil {
return err
}

defer tx.Rollback()

_, err = tx.Exec(`CREATE TABLE migrations (
id serial NOT NULL PRIMARY KEY,
name varchar(255) NULL,
batch int4 NULL,
migration_time TIMESTAMP NULL
);`)
if err != nil {
return err
}

_, err = tx.Exec(`CREATE TABLE migrations_lock (
` + "`index`" + ` serial NOT NULL PRIMARY KEY,
is_locked int4 NULL
);`)
if err != nil {
return err
}

_, err = tx.Exec(`INSERT INTO migrations_lock SET ` + "`index`" + ` = 1, is_locked = 0;`)
if err != nil {
return err
}

if err = tx.Commit(); err != nil {
return err
}

return nil
}
2 changes: 0 additions & 2 deletions commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import (

func CommandList(cfg config.MigConfig) result.Response {
dbox, err := database.Connect(cfg.Connection)

if err != nil {
return *result.NewErrorWithDetails("database connection error", "db_conn", err)
}

defer dbox.Db.Close()

status, err := migrations.GetStatus(cfg, dbox)

if err != nil {
return *result.NewErrorWithDetails("unable to get migration status", "unable_get_status", err)
}
Expand Down
Loading

0 comments on commit b745bf7

Please sign in to comment.