feat(pgmigrate/examples): add minimal migration runner example

Demonstrates pgmigrate with embedded SQL migrations: up, down, status,
and reset commands. Two migrations (init + create-todos) show the
minimum viable pattern for using sqlmigrate with PostgreSQL.
This commit is contained in:
AJ ONeal 2026-04-08 22:01:41 -06:00
parent 58c6301678
commit a0e59d7a47
No known key found for this signature in database
7 changed files with 208 additions and 0 deletions

View File

@ -0,0 +1,22 @@
module github.com/therootcompany/golib/database/sqlmigrate/pgmigrate/examples/todos
go 1.26.1
require (
github.com/jackc/pgx/v5 v5.9.1
github.com/therootcompany/golib/database/sqlmigrate v1.0.0
github.com/therootcompany/golib/database/sqlmigrate/pgmigrate v0.0.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
replace (
github.com/therootcompany/golib/database/sqlmigrate => ../../..
github.com/therootcompany/golib/database/sqlmigrate/pgmigrate => ../..
)

View File

@ -0,0 +1,26 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,141 @@
// Command todos demonstrates pgmigrate with embedded SQL migrations.
//
// Usage:
//
// export PG_URL='postgres://user:pass@localhost:5432/dbname?sslmode=require'
// 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"
"embed"
"fmt"
stdfs "io/fs"
"os"
"strconv"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/therootcompany/golib/database/sqlmigrate"
"github.com/therootcompany/golib/database/sqlmigrate/pgmigrate"
)
//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()
pgURL := os.Getenv("PG_URL")
if pgURL == "" {
return fmt.Errorf("PG_URL environment variable is required")
}
pool, err := pgxpool.New(ctx, pgURL)
if err != nil {
return fmt.Errorf("connecting: %w", err)
}
defer pool.Close()
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)
}
runner := pgmigrate.New(pool)
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
}

View File

@ -0,0 +1,2 @@
DELETE FROM _migrations WHERE id = '00000001';
DROP TABLE IF EXISTS _migrations;

View File

@ -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');

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS todos;
DELETE FROM _migrations WHERE id = 'a1b2c3d4';

View File

@ -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');