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.
87 lines
1.7 KiB
Go
Executable File
87 lines
1.7 KiB
Go
Executable File
package migrate
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
const table = "dbversion"
|
|
|
|
type Change struct {
|
|
Description string
|
|
Apply func(ctx Context) error
|
|
}
|
|
|
|
type Context interface {
|
|
Exec(query string, args ...any) (sql.Result, error)
|
|
Query(query string, args ...any) (*sql.Rows, error)
|
|
}
|
|
|
|
func Apply(ctx Context, d Dialect, migrations []Change) error {
|
|
if err := initialize(ctx, d); err != nil {
|
|
return err
|
|
}
|
|
|
|
currentVersion, err := dbVersion(ctx, d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
migrations = migrations[currentVersion:] // only apply what hasnt been been applied already
|
|
for _, m := range migrations {
|
|
if err := apply(ctx, d, m); err != nil {
|
|
return fmt.Errorf("error performing migration \"%s\": %w", m.Description, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func initialize(ctx Context, d Dialect) error {
|
|
if !versionTableExists(ctx, d) {
|
|
return createVersionTable(ctx, d)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func versionTableExists(ctx Context, d Dialect) bool {
|
|
rows, table_check := ctx.Query(d.TableExists(table))
|
|
if rows != nil {
|
|
defer rows.Close()
|
|
}
|
|
return table_check == nil
|
|
}
|
|
|
|
func apply(ctx Context, d Dialect, r Change) error {
|
|
if err := r.Apply(ctx); err != nil {
|
|
return err
|
|
}
|
|
return incrementVersion(ctx, d, r.Description)
|
|
}
|
|
|
|
func createVersionTable(ctx Context, d Dialect) error {
|
|
_, err := ctx.Exec(d.CreateTable(table))
|
|
return err
|
|
}
|
|
|
|
func incrementVersion(ctx Context, d Dialect, description string) error {
|
|
_, err := ctx.Exec(d.InsertVersion(table), description, time.Now())
|
|
return err
|
|
}
|
|
|
|
func dbVersion(ctx Context, d Dialect) (id int64, err error) {
|
|
rows, err := ctx.Query(d.CheckVersion(table))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
if rows.Next() {
|
|
err = rows.Scan(&id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
return id, nil
|
|
}
|