|
|
Package migrations contains an implementation of a simple database migration
mechanism that allows semi-automatic transitions between various database
versions as well as building the latest version of the database from
scratch.
Please look into README.md with further instructions how to use it.
|
package migrations
|
Documentation in literate-programming-style is available at:
https://redhatinsights.github.io/insights-operator-utils/packages/migrations/migrations.html
|
import (
"database/sql"
"fmt"
types "github.com/RedHatInsights/insights-results-types"
)
|
Version represents a version of the database.
|
type Version uint
|
Step represents an action performed to either increase
or decrease the migration version of the database.
|
type Step func ( tx * sql . Tx , driver types . DBDriver ) error
|
Migration type describes a single Migration.
|
type Migration struct {
StepUp Step
StepDown Step
}
var migrations [ ] Migration
|
Set initialized this package by setting
the migrations steps to 'newMigrations'
It is mandatory to call this function
before using this package
|
func Set ( newMigrations [ ] Migration ) {
migrations = newMigrations
}
|
GetMaxVersion returns the highest available migration version.
The DB version cannot be set to a value higher than this.
This value is equivalent to the length of the list of available migrations.
|
func GetMaxVersion ( ) Version {
return Version ( len ( migrations ) )
}
|
InitInfoTable ensures that the migration information table is created.
If it already exists, no changes will be made to the database.
Otherwise, a new migration information table will be created and initialized.
|
func InitInfoTable ( db * sql . DB ) error {
return withTransaction ( db , func ( tx * sql . Tx ) error {
_ , err := tx . Exec ( "CREATE TABLE IF NOT EXISTS migration_info (version INTEGER NOT NULL);" )
if err != nil {
return err
}
|
INSERT if there's no rows in the table
|
_ , err = tx . Exec ( "INSERT INTO migration_info (version) SELECT 0 WHERE NOT EXISTS (SELECT version FROM migration_info);" )
if err != nil {
return err
}
var rowCount uint
err = tx . QueryRow ( "SELECT COUNT(*) FROM migration_info;" ) . Scan ( & rowCount )
if err != nil {
return err
}
if rowCount != 1 {
return fmt . Errorf ( "unexpected number of rows in migration info table (expected: 1, reality: %d)" , rowCount )
}
return nil
} )
}
|
GetDBVersion reads the current version of the database from the migration info table.
|
func GetDBVersion ( db * sql . DB ) ( Version , error ) {
err := validateNumberOfRows ( db )
if err != nil {
return 0 , err
}
var version Version = 0
err = db . QueryRow ( "SELECT version FROM migration_info;" ) . Scan ( & version )
err = ConvertDBError ( err , nil )
return version , err
}
|
SetDBVersion attempts to get the database into the specified
target version using available migration steps.
|
func SetDBVersion ( db * sql . DB , dbDriver types . DBDriver , targetVer Version ) error {
maxVer := GetMaxVersion ( )
if targetVer > maxVer {
return fmt . Errorf ( "invalid target version (available version range is 0-%d)" , maxVer )
}
|
Get current database version.
|
currentVer , err := GetDBVersion ( db )
if err != nil {
return err
}
|
Current version is unexpectedly high.
|
if currentVer > maxVer {
return fmt . Errorf ( "current version (%d) is outside of available migration boundaries" , currentVer )
}
return execStepsInTx ( db , dbDriver , currentVer , targetVer )
}
|
updateVersionInDB updates the migration version number in the migration info table.
This function does NOT rollback in case of an error. The calling function is expected to do that.
|
func updateVersionInDB ( tx * sql . Tx , newVersion Version ) error {
res , err := tx . Exec ( "UPDATE migration_info SET version=$1;" , newVersion )
if err != nil {
return err
}
|
Check that there is exactly 1 row in the migration info table.
|
affected , err := res . RowsAffected ( )
if err != nil {
return err
}
if affected != 1 {
return fmt . Errorf ( "unexpected number of affected rows in migration info table (expected: 1, reality: %d)" , affected )
}
return nil
}
|
execStepsInTx executes the necessary migration steps in a single transaction.
|
func execStepsInTx ( db * sql . DB , dbDriver types . DBDriver , currentVer , targetVer Version ) error {
|
Already at target version.
|
if currentVer == targetVer {
return nil
}
return withTransaction ( db , func ( tx * sql . Tx ) error {
|
Upgrade to target version.
|
for currentVer < targetVer {
if err := migrations [ currentVer ] . StepUp ( tx , dbDriver ) ; err != nil {
err = ConvertDBError ( err , nil )
return err
}
currentVer ++
}
|
Downgrade to target version.
|
for currentVer > targetVer {
if err := migrations [ currentVer - 1 ] . StepDown ( tx , dbDriver ) ; err != nil {
err = ConvertDBError ( err , nil )
return err
}
currentVer --
}
if err := updateVersionInDB ( tx , currentVer ) ; err != nil {
return err
}
return nil
} )
}
func validateNumberOfRows ( db * sql . DB ) error {
numberOfRows , err := getNumberOfRows ( db )
if err != nil {
return err
}
if numberOfRows != 1 {
return fmt . Errorf ( "migration info table contain %v rows" , numberOfRows )
}
return nil
}
func getNumberOfRows ( db * sql . DB ) ( uint , error ) {
var count uint
err := db . QueryRow ( "SELECT COUNT(*) FROM migration_info;" ) . Scan ( & count )
err = ConvertDBError ( err , nil )
return count , err
}
func withTransaction ( db * sql . DB , txFunc func ( * sql . Tx ) error ) ( errOut error ) {
var tx * sql . Tx
tx , errOut = db . Begin ( )
if errOut != nil {
return
}
defer func ( ) {
if p := recover ( ) ; p != nil {
_ = tx . Rollback ( )
|
panic again
|
panic ( p )
} else if errOut != nil {
_ = tx . Rollback ( )
} else {
errOut = tx . Commit ( )
}
} ( )
errOut = txFunc ( tx )
return
}
|