137 lines
4.8 KiB
Go
137 lines
4.8 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
)
|
|
|
|
const createMigrationsTableSQL = `CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
version VARCHAR(100) PRIMARY KEY,
|
|
dirty BOOLEAN NOT NULL DEFAULT TRUE,
|
|
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)`
|
|
|
|
func writeMigration(t *testing.T, name, sqlText string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(sqlText), 0o600); err != nil {
|
|
t.Fatalf("write migration: %v", err)
|
|
}
|
|
return dir
|
|
}
|
|
|
|
func expectCleanState(mock sqlmock.Sqlmock, applied ...string) {
|
|
mock.ExpectExec(regexp.QuoteMeta(createMigrationsTableSQL)).WillReturnResult(sqlmock.NewResult(0, 0))
|
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM schema_migrations WHERE dirty = TRUE ORDER BY applied_at DESC LIMIT 1`)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"version"}))
|
|
rows := sqlmock.NewRows([]string{"version"})
|
|
for _, version := range applied {
|
|
rows.AddRow(version)
|
|
}
|
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM schema_migrations WHERE dirty = FALSE`)).WillReturnRows(rows)
|
|
}
|
|
|
|
func TestRunAppliesEachMigrationOnce(t *testing.T) {
|
|
dir := writeMigration(t, "001_create_widgets.sql", `CREATE TABLE widgets (id BIGINT PRIMARY KEY);`)
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("open sqlmock: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
|
|
expectCleanState(mock)
|
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO schema_migrations (version, dirty) VALUES (?, TRUE)`)).
|
|
WithArgs("001_create_widgets.sql").WillReturnResult(sqlmock.NewResult(1, 1))
|
|
mock.ExpectExec(regexp.QuoteMeta(`CREATE TABLE widgets (id BIGINT PRIMARY KEY)`)).
|
|
WillReturnResult(sqlmock.NewResult(0, 0))
|
|
mock.ExpectExec(regexp.QuoteMeta(`UPDATE schema_migrations SET dirty = FALSE, applied_at = CURRENT_TIMESTAMP WHERE version = ?`)).
|
|
WithArgs("001_create_widgets.sql").WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
if err := Run(context.Background(), db, dir); err != nil {
|
|
t.Fatalf("first run: %v", err)
|
|
}
|
|
|
|
expectCleanState(mock, "001_create_widgets.sql")
|
|
if err := Run(context.Background(), db, dir); err != nil {
|
|
t.Fatalf("second run: %v", err)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRunLeavesFailedMigrationDirty(t *testing.T) {
|
|
dir := writeMigration(t, "002_broken.sql", `CREATE TABLE broken (id BIGINT); ALTER TABLE missing ADD value INT;`)
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("open sqlmock: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
|
|
expectCleanState(mock)
|
|
mock.ExpectExec(regexp.QuoteMeta(`INSERT INTO schema_migrations (version, dirty) VALUES (?, TRUE)`)).
|
|
WithArgs("002_broken.sql").WillReturnResult(sqlmock.NewResult(1, 1))
|
|
mock.ExpectExec(regexp.QuoteMeta(`CREATE TABLE broken (id BIGINT)`)).WillReturnResult(sqlmock.NewResult(0, 0))
|
|
mock.ExpectExec(regexp.QuoteMeta(`ALTER TABLE missing ADD value INT`)).WillReturnError(errors.New("missing table"))
|
|
|
|
err = Run(context.Background(), db, dir)
|
|
if err == nil {
|
|
t.Fatal("expected migration failure")
|
|
}
|
|
for _, fragment := range []string{"002_broken.sql", "statement 2", "dirty", "manual repair"} {
|
|
if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(fragment)) {
|
|
t.Fatalf("error %q does not contain %q", err, fragment)
|
|
}
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRunStopsWhenDirtyMigrationExists(t *testing.T) {
|
|
dir := writeMigration(t, "003_should_not_run.sql", `CREATE TABLE should_not_run (id BIGINT);`)
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("open sqlmock: %v", err)
|
|
}
|
|
t.Cleanup(func() { db.Close() })
|
|
|
|
mock.ExpectExec(regexp.QuoteMeta(createMigrationsTableSQL)).WillReturnResult(sqlmock.NewResult(0, 0))
|
|
mock.ExpectQuery(regexp.QuoteMeta(`SELECT version FROM schema_migrations WHERE dirty = TRUE ORDER BY applied_at DESC LIMIT 1`)).
|
|
WillReturnRows(sqlmock.NewRows([]string{"version"}).AddRow("002_broken.sql"))
|
|
|
|
err = Run(context.Background(), db, dir)
|
|
if err == nil || !strings.Contains(err.Error(), "002_broken.sql") {
|
|
t.Fatalf("expected dirty migration error, got %v", err)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSplitStatementsPreservesSemicolonsInsideStrings(t *testing.T) {
|
|
statements, err := splitStatements("INSERT INTO notes(value) VALUES ('a;b'); -- comment\nUPDATE notes SET value = \"c;d\";")
|
|
if err != nil {
|
|
t.Fatalf("split statements: %v", err)
|
|
}
|
|
want := []string{
|
|
"INSERT INTO notes(value) VALUES ('a;b')",
|
|
"UPDATE notes SET value = \"c;d\"",
|
|
}
|
|
if len(statements) != len(want) {
|
|
t.Fatalf("got %d statements, want %d: %#v", len(statements), len(want), statements)
|
|
}
|
|
for i := range want {
|
|
if statements[i] != want[i] {
|
|
t.Fatalf("statement %d = %q, want %q", i+1, statements[i], want[i])
|
|
}
|
|
}
|
|
}
|