From 818871ed7670195205292e4db269af4577961896 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 8 Apr 2026 22:02:58 -0600 Subject: [PATCH] feat(mymigrate/examples): add minimal migration runner example Demonstrates mymigrate with embedded SQL migrations: up, down, status, and reset commands. Two migrations (init + create-todos) show the minimum viable pattern for using sqlmigrate with MySQL/MariaDB. Verified against MariaDB 11.4.9 via sclient --alpn mysql. --- .../mymigrate/examples/todos/go.mod | 16 ++ .../mymigrate/examples/todos/go.sum | 4 + .../mymigrate/examples/todos/main.go | 149 ++++++++++++++++++ .../migrations/0001_init-migrations.down.sql | 2 + .../migrations/0001_init-migrations.up.sql | 7 + .../migrations/0002_create-todos.down.sql | 2 + .../todos/migrations/0002_create-todos.up.sql | 8 + 7 files changed, 188 insertions(+) create mode 100644 database/sqlmigrate/mymigrate/examples/todos/go.mod create mode 100644 database/sqlmigrate/mymigrate/examples/todos/go.sum create mode 100644 database/sqlmigrate/mymigrate/examples/todos/main.go create mode 100644 database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.down.sql create mode 100644 database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.up.sql create mode 100644 database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.down.sql create mode 100644 database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.up.sql diff --git a/database/sqlmigrate/mymigrate/examples/todos/go.mod b/database/sqlmigrate/mymigrate/examples/todos/go.mod new file mode 100644 index 0000000..92bf50a --- /dev/null +++ b/database/sqlmigrate/mymigrate/examples/todos/go.mod @@ -0,0 +1,16 @@ +module github.com/therootcompany/golib/database/sqlmigrate/mymigrate/examples/todos + +go 1.26.1 + +require ( + github.com/go-sql-driver/mysql v1.9.3 + github.com/therootcompany/golib/database/sqlmigrate v1.0.0 + github.com/therootcompany/golib/database/sqlmigrate/mymigrate v0.0.0 +) + +require filippo.io/edwards25519 v1.1.0 // indirect + +replace ( + github.com/therootcompany/golib/database/sqlmigrate => ../../.. + github.com/therootcompany/golib/database/sqlmigrate/mymigrate => ../.. +) diff --git a/database/sqlmigrate/mymigrate/examples/todos/go.sum b/database/sqlmigrate/mymigrate/examples/todos/go.sum new file mode 100644 index 0000000..4bcdcfa --- /dev/null +++ b/database/sqlmigrate/mymigrate/examples/todos/go.sum @@ -0,0 +1,4 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= diff --git a/database/sqlmigrate/mymigrate/examples/todos/main.go b/database/sqlmigrate/mymigrate/examples/todos/main.go new file mode 100644 index 0000000..fbe36e0 --- /dev/null +++ b/database/sqlmigrate/mymigrate/examples/todos/main.go @@ -0,0 +1,149 @@ +// Command todos demonstrates mymigrate with embedded SQL migrations. +// +// Usage: +// +// export MY_URL='user:pass@tcp(host:3306)/dbname?multiStatements=true&parseTime=true' +// go run . up # apply all pending migrations +// go run . up 1 # apply next 1 migration +// go run . down # roll back last migration +// go run . down 2 # roll back last 2 migrations +// go run . status # show applied and pending +// go run . reset # roll back all migrations +package main + +import ( + "context" + "database/sql" + "embed" + "fmt" + stdfs "io/fs" + "os" + "strconv" + + _ "github.com/go-sql-driver/mysql" + + "github.com/therootcompany/golib/database/sqlmigrate" + "github.com/therootcompany/golib/database/sqlmigrate/mymigrate" +) + +//go:embed migrations/*.sql +var migrationsFS embed.FS + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + ctx := context.Background() + + myURL := os.Getenv("MY_URL") + if myURL == "" { + return fmt.Errorf("MY_URL environment variable is required\n Format: user:pass@tcp(host:port)/dbname?multiStatements=true&parseTime=true") + } + + db, err := sql.Open("mysql", myURL) + if err != nil { + return fmt.Errorf("opening database: %w", err) + } + defer db.Close() + + if err := db.PingContext(ctx); err != nil { + return fmt.Errorf("connecting: %w", err) + } + + runner, err := mymigrate.New(db) + if err != nil { + return err + } + + subFS, err := stdfs.Sub(migrationsFS, "migrations") + if err != nil { + return fmt.Errorf("reading migrations: %w", err) + } + migrations, err := sqlmigrate.Collect(subFS) + if err != nil { + return fmt.Errorf("collecting migrations: %w", err) + } + + args := os.Args[1:] + if len(args) == 0 { + args = []string{"status"} + } + + switch args[0] { + case "up": + n := -1 + if len(args) > 1 { + n, err = strconv.Atoi(args[1]) + if err != nil || n < 1 { + return fmt.Errorf("up count must be a positive integer") + } + } + applied, err := sqlmigrate.Up(ctx, runner, migrations, n) + if err != nil { + return err + } + if len(applied) == 0 { + fmt.Println("Already up-to-date.") + } else { + for _, name := range applied { + fmt.Printf(" applied: %s\n", name) + } + } + + case "down": + n := 1 + if len(args) > 1 { + n, err = strconv.Atoi(args[1]) + if err != nil || n < 1 { + return fmt.Errorf("down count must be a positive integer") + } + } + rolled, err := sqlmigrate.Down(ctx, runner, migrations, n) + if err != nil { + return err + } + if len(rolled) == 0 { + fmt.Println("Nothing to roll back.") + } else { + for _, name := range rolled { + fmt.Printf(" rolled back: %s\n", name) + } + } + + case "status": + status, err := sqlmigrate.GetStatus(ctx, runner, migrations) + if err != nil { + return err + } + fmt.Printf("Applied: %d\n", len(status.Applied)) + for _, name := range status.Applied { + fmt.Printf(" %s\n", name) + } + fmt.Printf("Pending: %d\n", len(status.Pending)) + for _, name := range status.Pending { + fmt.Printf(" %s\n", name) + } + + case "reset": + rolled, err := sqlmigrate.Down(ctx, runner, migrations, -1) + if err != nil { + return err + } + if len(rolled) == 0 { + fmt.Println("Nothing to reset.") + } else { + for _, name := range rolled { + fmt.Printf(" rolled back: %s\n", name) + } + } + + default: + return fmt.Errorf("unknown command %q (use: up, down, status, reset)", args[0]) + } + + return nil +} diff --git a/database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.down.sql b/database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.down.sql new file mode 100644 index 0000000..003ba8d --- /dev/null +++ b/database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.down.sql @@ -0,0 +1,2 @@ +DELETE FROM _migrations WHERE id = '00000001'; +DROP TABLE IF EXISTS _migrations; diff --git a/database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.up.sql b/database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.up.sql new file mode 100644 index 0000000..483429a --- /dev/null +++ b/database/sqlmigrate/mymigrate/examples/todos/migrations/0001_init-migrations.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS _migrations ( + id CHAR(8) PRIMARY KEY, + name VARCHAR(80) NULL UNIQUE, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO _migrations (name, id) VALUES ('0001_init-migrations', '00000001'); diff --git a/database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.down.sql b/database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.down.sql new file mode 100644 index 0000000..4d961b4 --- /dev/null +++ b/database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS todos; +DELETE FROM _migrations WHERE id = 'a1b2c3d4'; diff --git a/database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.up.sql b/database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.up.sql new file mode 100644 index 0000000..926996b --- /dev/null +++ b/database/sqlmigrate/mymigrate/examples/todos/migrations/0002_create-todos.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE todos ( + id CHAR(8) PRIMARY KEY, + title VARCHAR(255) NOT NULL, + done BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +INSERT INTO _migrations (name, id) VALUES ('0002_create-todos', 'a1b2c3d4');