zephinzer/dev

View on GitHub
internal/db/migrate.go

Summary

Maintainability
A
35 mins
Test Coverage
package db

import (
    "database/sql"
    "fmt"

    "github.com/zephinzer/dev/internal/log"
)

// ApplyMigrations runs all the `migrations` on the sqlite3 database which
// is stored at the `databasePath`; since migrations are tied to tables, it's
// also requested that the `tableName` is provided
func ApplyMigrations(tableName string, migrations []string, databasePath string) error {
    connection, newConnectionError := NewConnection(databasePath)
    defer connection.Close()
    if newConnectionError != nil {
        return newConnectionError
    }
    if initError := InitTable(tableName, connection); initError != nil {
        return initError
    }
    for index, script := range migrations {
        log.Tracef("checking and applying migration[%v] (script: '%s')...\n", index, script)
        applied, validityError := IsMigrationAppliedAndValid(tableName, index, script, connection)
        if validityError != nil {
            return fmt.Errorf("encountered validation error at migration[%v] (script: '%s'): %s", index, script, validityError)
        } else if applied {
            log.Debugf("migration[%v] already applied\n", index)
        } else {
            if migrationError := RunMigration(tableName, index, script, connection); migrationError != nil {
                return fmt.Errorf("encountered migration error at migration[%v] (script: '%s'): %s", index, script, migrationError)
            }
            log.Infof("migration[%v] (\"%s\") successfully applied\n", index, script)
        }
    }
    return nil
}

// IsMigrationAppliedAndValid performs a check on whether the provided
// migration already exists in the database
func IsMigrationAppliedAndValid(
    tableName string,
    migrationIndex int,
    migrationScript string,
    connection *sql.DB,
) (bool, error) {
    row := connection.QueryRow(fmt.Sprintf("SELECT `index`, `script` FROM `%s_migrations` WHERE `index` = ? OR (`index` = ? AND `script` = ?)", tableName), migrationIndex, migrationIndex, migrationScript)
    var storedIndex int
    var storedScript string
    var scanError error
    if scanError = row.Scan(&storedIndex, &storedScript); scanError != nil {
        if scanError == sql.ErrNoRows {
            return false, nil
        }
        return false, fmt.Errorf("failed to retrieve migration from `%s`'s migrations table: %s", tableName, scanError)
    }
    if storedIndex != migrationIndex || storedScript != migrationScript {
        return true, fmt.Errorf("migration \"%s\" at index %v does not seem equivalent to stored migration \"%s\" seen last at index %v", migrationScript, migrationIndex, storedScript, storedIndex)
    }
    return true, nil
}

// RunMigration applies a single migration; this function assumes all
// database migrations are upward moving so design your migrations as such
func RunMigration(
    tableName string,
    migrationIndex int,
    migrationScript string,
    connection *sql.DB,
) error {
    _, execError := connection.Exec(migrationScript)
    if execError != nil {
        return fmt.Errorf("failed to run migration: %s", execError)
    }
    _, execError = connection.Exec(fmt.Sprintf("INSERT INTO `%s_migrations` (`index`, `script`) VALUES(?, ?)", tableName), migrationIndex, migrationScript)
    if execError != nil {
        return fmt.Errorf("failed to update migration table: %s", execError)
    }
    return nil
}