Skip to content

Commit

Permalink
Add the migration cli
Browse files Browse the repository at this point in the history
  • Loading branch information
josephspurrier committed Jul 10, 2018
1 parent a6a6ba4 commit ba9f499
Show file tree
Hide file tree
Showing 9 changed files with 524 additions and 2 deletions.
9 changes: 8 additions & 1 deletion src/app/webapi/cmd/cliapp/cliapp.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"app/webapi/pkg/basemigrate"
"encoding/base64"
"fmt"
"log"
Expand All @@ -14,7 +15,11 @@ import (
var (
app = kingpin.New("cliapp", "A command-line application to perform tasks for the webapi.")

cGenerate = app.Command("generate", "Generate 256 bit (32 byte) base64 encoded JWT.")
cGenerate = app.Command("generate", "Generate 256 bit (32 byte) base64 encoded JWT.")
cDB = app.Command("migrate", "Perform actions on the database.")
cDBAll = cDB.Command("all", "Apply all changesets to the database.")
cDBReset = cDB.Command("reset", "Run all rollbacks on the database.")
cDBAllFile = cDBAll.Arg("file", "Filename of the migration file.").Required().String()
)

func main() {
Expand All @@ -30,5 +35,7 @@ func main() {

enc := base64.StdEncoding.EncodeToString(b)
fmt.Println(enc)
case cDBAll.FullCommand():
basemigrate.Migrate(*cDBAllFile, true)
}
}
2 changes: 1 addition & 1 deletion src/app/webapi/internal/testrequest/form.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package testrequest

import (
"app/webapi"
"io"
"net/http/httptest"
"net/url"
"strings"
"testing"

"app/webapi"
"app/webapi/component"
)

Expand Down
128 changes: 128 additions & 0 deletions src/app/webapi/pkg/basemigrate/basemigrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package basemigrate

import (
"database/sql"
"errors"
"fmt"
"os"
"strings"
)

const (
sqlChangelog = `CREATE TABLE IF NOT EXISTS databasechangelog (
id varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
author varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
filename varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
dateexecuted datetime NOT NULL,
orderexecuted int(11) NOT NULL,
md5sum varchar(35) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
description varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
tag varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
version varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`
)

const (
appVersion = "1.0"
elementChangeset = "--changeset "
elementRollback = "--rollback "
)

var (
// ErrInvalidHeader is when the changeset header is invalid.
ErrInvalidHeader = errors.New("invalid changeset header")
// ErrInvalidFormat is when a changeset is not found.
ErrInvalidFormat = errors.New("invalid changeset format")
)

// ParseFile will parse a file into changesets.
func ParseFile(filename string) ([]*Changeset, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()

return parse(f, filename)
}

// Migrate will migrate a file.
func Migrate(filename string, verbose bool) (err error) {
db, err := connect()
if err != nil {
return err
}

// Create the DATABASECHANGELOG.
_, err = db.Exec(sqlChangelog)

// Get the changesets.
arr, err := ParseFile(filename)
if err != nil {
return err
}

// Loop through each changeset.
for _, cs := range arr {
checksum := ""
newChecksum := cs.Checksum()

// Determine if the changeset was already applied.
// Count the number of rows.
err = db.Get(&checksum, `SELECT md5sum
FROM databasechangelog
WHERE id = ?
AND author = ?`, cs.id, cs.author)
if err == nil {
// Determine if the checksums match.
if checksum != newChecksum {
return fmt.Errorf("checksum does not match - existing changeset %v:%v has checksum %v, but new changeset has checksum %v",
cs.author, cs.id, checksum, newChecksum)
}

if verbose {
fmt.Printf("Changeset already applied: %v:%v\n", cs.author, cs.id)
}
continue
} else if err != nil && err != sql.ErrNoRows {
return err
}

arrQueries := strings.Split(cs.Changes(), ";")
// Loop through each change.
for _, q := range arrQueries {
if len(q) == 0 {
continue
}

// Execute the query.
_, err = db.Exec(q)
if err != nil {
return err
}
}

// Count the number of rows.
count := 0
err = db.Get(&count, `SELECT COUNT(*) FROM databasechangelog`)
if err != nil {
return err
}

// Insert the record.
_, err = db.Exec(`
INSERT INTO databasechangelog
(id,author,filename,dateexecuted,orderexecuted,md5sum,description,version)
VALUES(?,?,?,NOW(),?,?,?,?)
`, cs.id, cs.author, cs.filename, count+1, newChecksum, cs.description, cs.version)
if err != nil {
return err
}

if verbose {
fmt.Printf("Changeset applied: %v:%v\n", cs.author, cs.id)
}
}

return
}
36 changes: 36 additions & 0 deletions src/app/webapi/pkg/basemigrate/basemigrate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package basemigrate_test

import (
"testing"

"app/webapi/pkg/basemigrate"

"github.com/stretchr/testify/assert"
)

func TestMigration(t *testing.T) {
err := basemigrate.Migrate("testdata/success.sql", false)
assert.Nil(t, err)
}

func TestMigrationFailDuplicate(t *testing.T) {
err := basemigrate.Migrate("testdata/fail-duplicate.sql", false)
assert.Contains(t, err.Error(), "checksum does not match")
}

func TestParse(t *testing.T) {
arr, err := basemigrate.ParseFile("testdata/success.sql")
assert.Nil(t, err)
assert.Equal(t, 5, len(arr))

//basemigrate.Debug(arr)

/*for _, v := range arr {
fmt.Println(v.Changes())
fmt.Println(v.Rollbacks())
fmt.Println("MD5:", v.Checksum())
break
}
fmt.Println("Total:", len(arr))*/
}
69 changes: 69 additions & 0 deletions src/app/webapi/pkg/basemigrate/changeset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package basemigrate

import (
"strings"
)

// Changeset is a SQL changeset.
type Changeset struct {
id string
author string
filename string
md5 string
description string
version string

change []string
rollback []string
}

// ParseHeader will parse the header information.
func (cs *Changeset) ParseHeader(line string) error {
arr := strings.Split(line, ":")
if len(arr) != 2 {
return ErrInvalidHeader
}

cs.author = arr[0]
cs.id = arr[1]

return nil
}

// SetFileInfo will set the file information.
func (cs *Changeset) SetFileInfo(filename string, description string, version string) {
cs.filename = filename
cs.description = description
cs.version = version
}

// AddRollback will add a rollback command.
func (cs *Changeset) AddRollback(line string) {
if len(cs.rollback) == 0 {
cs.rollback = make([]string, 0)
}
cs.rollback = append(cs.rollback, line)
}

// AddChange will add a change command.
func (cs *Changeset) AddChange(line string) {
if len(cs.change) == 0 {
cs.change = make([]string, 0)
}
cs.change = append(cs.change, line)
}

// Changes will return all the changes.
func (cs *Changeset) Changes() string {
return strings.Join(cs.change, "\n")
}

// Rollbacks will return all the rollbacks.
func (cs *Changeset) Rollbacks() string {
return strings.Join(cs.rollback, "\n")
}

// Checksum returns an MD5 checksum for the changeset.
func (cs *Changeset) Checksum() string {
return md5sum(cs.Changes())
}
46 changes: 46 additions & 0 deletions src/app/webapi/pkg/basemigrate/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package basemigrate

import (
"bytes"
"crypto/md5"
"fmt"
"io"

"app/webapi/pkg/database"

"github.com/jmoiron/sqlx"
)

// connect will connect to the database.
func connect() (*sqlx.DB, error) {
dbc := new(database.Connection)
dbc.Hostname = "127.0.0.1"
dbc.Port = 3306
dbc.Username = "root"
dbc.Password = ""
dbc.Database = "webapitest"
dbc.Parameter = "parseTime=true&allowNativePasswords=true"

return dbc.Connect(true)
}

// md5sum will return a checksum from a string.
func md5sum(s string) string {
h := md5.New()
r := bytes.NewReader([]byte(s))
_, _ = io.Copy(h, r)
return fmt.Sprintf("%x", h.Sum(nil))
}

// Debug will return the SQL file.
func Debug(arr []*Changeset) {
for _, cs := range arr {
fmt.Printf("%v%v:%v\n", elementChangeset, cs.author, cs.id)
fmt.Println(cs.Changes())
fmt.Printf("%v%v\n", elementRollback, cs.Rollbacks())
fmt.Println("--md5", cs.Checksum())
break
}

fmt.Println("Total:", len(arr))
}
60 changes: 60 additions & 0 deletions src/app/webapi/pkg/basemigrate/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package basemigrate

import (
"bufio"
"io"
"path"
"strings"
)

// parse will split the SQL migration into pieces.
func parse(r io.Reader, filename string) ([]*Changeset, error) {
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)

// Array of changesets.
arr := make([]*Changeset, 0)

for scanner.Scan() {
// Get the line without leading or trailing spaces.
line := strings.TrimSpace(scanner.Text())

// Skip blank lines.
if len(line) == 0 {
continue
}

// Start recording the changeset.
if strings.HasPrefix(line, elementChangeset) {
// Create a new changeset.
cs := new(Changeset)
cs.ParseHeader(strings.TrimLeft(line, elementChangeset))
cs.SetFileInfo(path.Base(filename), "sql", appVersion)
arr = append(arr, cs)
continue
}

// If the length of the array is 0, then the first changeset is missing.
if len(arr) == 0 {
return nil, ErrInvalidFormat
}

// Determine if the line is a rollback.
if strings.HasPrefix(line, elementRollback) {
cs := arr[len(arr)-1]
cs.AddRollback(strings.TrimLeft(line, elementRollback))
continue
}

// Determine if the line is comment, ignore it.
if strings.HasPrefix(line, "--") {
continue
}

// Add the line as a changeset.
cs := arr[len(arr)-1]
cs.AddChange(line)
}

return arr, nil
}
Loading

0 comments on commit ba9f499

Please sign in to comment.