Security Fixes:
- Add SQL injection protection in dialect.go using proper identifier quoting
- Implement quoteIdentifier() method to escape SQL identifiers safely
- Fix resource leak in dbVersion() by adding deferred rows.Close()
- Fix incorrect error handling in dbVersion() to properly propagate errors
Code Quality Improvements:
- Replace custom Error struct with idiomatic fmt.Errorf with %w verb
- Simplify error handling by replacing nested if-err-nil with early returns
- Remove named return values with implicit returns for clarity
- Update interface{} to any (Go 1.18+ style)
- Fix variable shadowing in Apply loop (use m.Description instead of migrations[i])
Test Improvements:
- Fix variable shadowing bug in createTestDB() that caused nil pointer panics
- Update SQL driver from github.com/mattn/go-sqlite3 to modernc.org/sqlite
- Fix driver name from "sqlite3" to "sqlite" for modernc.org/sqlite
- Add missing error check for r.Scan() in TestApply
- Make test error handling consistent by using t.Fatal() throughout
- Simplify test helper functions with early returns
Documentation Fixes:
- Fix README example to use 'Apply' field instead of incorrect 'F' field
- Update README example to match actual test code (sex instead of gender)
- Fix typos: "datbase" → "database", "datbases" → "databases"
- Improve README clarity with proper punctuation and formatting
- Update doc.go with correct spelling
Dependencies:
- Update go.mod to Go 1.25
- Switch to modernc.org/sqlite v1.44.0 (pure Go SQLite driver)
- Add all required indirect dependencies
All changes maintain backward compatibility and pass existing tests.
229 lines
4.3 KiB
Go
Executable File
229 lines
4.3 KiB
Go
Executable File
package migrate
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func TestHelperFuncs(t *testing.T) {
|
|
path, db, err := createTestDB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = teardownTestDB(path, db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCreateVersionTable(t *testing.T) {
|
|
path, db, err := createTestDB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = createVersionTable(db, Sqlite3())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err = teardownTestDB(path, db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestIncrementVersion(t *testing.T) {
|
|
path, db, err := createTestDB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sl3 := Sqlite3()
|
|
|
|
err = createVersionTable(db, sl3)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
descriptions := []string{
|
|
"this is a test",
|
|
"this is another test",
|
|
}
|
|
|
|
for _, d := range descriptions {
|
|
err = incrementVersion(db, sl3, d)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
rows, err := db.Query("SELECT id, description from dbversion")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var id int
|
|
var description string
|
|
for r := 1; rows.Next(); r++ {
|
|
err = rows.Scan(&id, &description)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if id != r || !strings.EqualFold(description, descriptions[r-1]) {
|
|
t.Fatalf("first row does not match %d %s: %d %s", id, descriptions[r-1], r, description)
|
|
}
|
|
|
|
}
|
|
|
|
if err = teardownTestDB(path, db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestDbVersion(t *testing.T) {
|
|
path, db, err := createTestDB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sl3 := Sqlite3()
|
|
|
|
err = createVersionTable(db, sl3)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ver, err := dbVersion(db, sl3)
|
|
if ver != 0 || err != nil {
|
|
t.Fatalf("version not 0 as expected (actual %d) or err: %#v", ver, err)
|
|
}
|
|
|
|
err = incrementVersion(db, sl3, "Test 1")
|
|
ver, err = dbVersion(db, sl3)
|
|
if ver != 1 {
|
|
t.Fatalf("version not 1 as expected (actual %d)", ver)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("err on dbversion of first increment: %#v", err)
|
|
}
|
|
|
|
// err = incrementVersion(db, d)
|
|
|
|
if err = teardownTestDB(path, db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestApply(t *testing.T) {
|
|
path, db, err := createTestDB()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sl3 := Sqlite3()
|
|
|
|
records :=
|
|
[]Change{
|
|
{
|
|
Description: "create people table",
|
|
Apply: func(ctx Context) (err error) {
|
|
_, err = ctx.Exec(`
|
|
CREATE TABLE people (
|
|
given_name VARCHAR(20),
|
|
surname VARCHAR(30),
|
|
sex CHAR(1),
|
|
age SMALLINT);
|
|
`)
|
|
return
|
|
},
|
|
},
|
|
{
|
|
Description: "Insert a person into people",
|
|
Apply: func(ctx Context) (err error) {
|
|
_, err = ctx.Exec(`INSERT INTO people VALUES('Henry','Colin','M', 42)`)
|
|
return
|
|
},
|
|
},
|
|
}
|
|
|
|
err = Apply(db, sl3, records)
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
r := db.QueryRow("SELECT given_name FROM people")
|
|
|
|
var given_name string
|
|
if err := r.Scan(&given_name); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if given_name != "Henry" {
|
|
t.Fatalf("second migration did not complete: %s != %s", given_name, "Henry")
|
|
}
|
|
|
|
// reapply and make sure we dont re-run anything
|
|
err = Apply(db, sl3, records)
|
|
ver, err := dbVersion(db, sl3)
|
|
if ver != 2 {
|
|
t.Fatalf("version not 2 as expected (actual %d)", ver)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("err on dbversion of re-apply: %#v", err)
|
|
}
|
|
|
|
// add bad (causes migrate.Error) case here.
|
|
|
|
ishouldntHideUserErrors := errors.New("I should fail")
|
|
|
|
records = append(records, Change{
|
|
Description: "Insert a person into people",
|
|
Apply: func(ctx Context) (err error) {
|
|
return ishouldntHideUserErrors
|
|
},
|
|
})
|
|
|
|
err = Apply(db, sl3, records)
|
|
|
|
if errors.Unwrap(err) != ishouldntHideUserErrors {
|
|
t.Fatalf("unexpected error returned that should have been record function error: %#v", err)
|
|
}
|
|
ver, err = dbVersion(db, sl3)
|
|
if ver != 2 {
|
|
t.Fatalf("version not 2 as expected (actual %d) after bad record apply", ver)
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("err on dbversion of re-apply with bad record: %#v", err)
|
|
}
|
|
|
|
if err = teardownTestDB(path, db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
}
|
|
|
|
func createTestDB() (path string, db *sql.DB, err error) {
|
|
f, err := os.CreateTemp(os.TempDir(), "migrate-test-db")
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
f.Close()
|
|
|
|
db, err = sql.Open("sqlite", f.Name())
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
return f.Name(), db, nil
|
|
}
|
|
func teardownTestDB(path string, db *sql.DB) error {
|
|
if err := db.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Remove(path)
|
|
}
|