ref(sqlmigrate): add subpath to Collect, add Latest/Drop convenience functions

API changes for v1:
- Collect(fsys, subpath) takes a subdirectory path (use "." for root),
  enabling embed.FS with //go:embed sql/migrations/*.sql
- Latest() applies all pending migrations (shorthand for Up with n=-1)
- Drop() rolls back all applied migrations (shorthand for Down with n=-1)
This commit is contained in:
AJ ONeal 2026-04-09 02:04:37 -06:00
parent 9c672a9d76
commit a3ecf5ac81
No known key found for this signature in database
2 changed files with 29 additions and 10 deletions

View File

@ -71,11 +71,20 @@ var idFromInsert = regexp.MustCompile(
`(?i)INSERT\s+INTO\s+_migrations\s*\(\s*name\s*,\s*id\s*\)\s*VALUES\s*\(\s*'[^']*'\s*,\s*'([0-9a-fA-F]+)'\s*\)`, `(?i)INSERT\s+INTO\s+_migrations\s*\(\s*name\s*,\s*id\s*\)\s*VALUES\s*\(\s*'[^']*'\s*,\s*'([0-9a-fA-F]+)'\s*\)`,
) )
// Collect reads .up.sql and .down.sql files from fsys, pairs them by // Collect reads .up.sql and .down.sql files from fsys under subpath,
// basename, and returns them sorted lexicographically by name. // pairs them by basename, and returns them sorted lexicographically by name.
// If subpath is "" or ".", the root of fsys is used.
// If the up SQL contains an INSERT INTO _migrations line, the hex ID // If the up SQL contains an INSERT INTO _migrations line, the hex ID
// is extracted and stored in Migration.ID. // is extracted and stored in Migration.ID.
func Collect(fsys fs.FS) ([]Migration, error) { func Collect(fsys fs.FS, subpath string) ([]Migration, error) {
if subpath != "" && subpath != "." {
var err error
fsys, err = fs.Sub(fsys, subpath)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrWalkFailed, err)
}
}
ups := map[string]string{} ups := map[string]string{}
downs := map[string]string{} downs := map[string]string{}
@ -285,3 +294,13 @@ func GetStatus(ctx context.Context, r Migrator, migrations []Migration) (*Status
Pending: pending, Pending: pending,
}, nil }, nil
} }
// Latest applies all pending migrations. Equivalent to Up(ctx, r, migrations, -1).
func Latest(ctx context.Context, r Migrator, migrations []Migration) ([]string, error) {
return Up(ctx, r, migrations, -1)
}
// Drop rolls back all applied migrations. Equivalent to Down(ctx, r, migrations, -1).
func Drop(ctx context.Context, r Migrator, migrations []Migration) ([]string, error) {
return Down(ctx, r, migrations, -1)
}

View File

@ -63,7 +63,7 @@ func TestCollect(t *testing.T) {
"001_first.up.sql": {Data: []byte("CREATE TABLE a;")}, "001_first.up.sql": {Data: []byte("CREATE TABLE a;")},
"001_first.down.sql": {Data: []byte("DROP TABLE a;")}, "001_first.down.sql": {Data: []byte("DROP TABLE a;")},
} }
migrations, err := sqlmigrate.Collect(fsys) migrations, err := sqlmigrate.Collect(fsys, ".")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -89,7 +89,7 @@ func TestCollect(t *testing.T) {
"001_init.up.sql": {Data: []byte("CREATE TABLE a;\nINSERT INTO _migrations (name, id) VALUES ('001_init', 'abcd1234');")}, "001_init.up.sql": {Data: []byte("CREATE TABLE a;\nINSERT INTO _migrations (name, id) VALUES ('001_init', 'abcd1234');")},
"001_init.down.sql": {Data: []byte("DROP TABLE a;\nDELETE FROM _migrations WHERE id = 'abcd1234';")}, "001_init.down.sql": {Data: []byte("DROP TABLE a;\nDELETE FROM _migrations WHERE id = 'abcd1234';")},
} }
migrations, err := sqlmigrate.Collect(fsys) migrations, err := sqlmigrate.Collect(fsys, ".")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -103,7 +103,7 @@ func TestCollect(t *testing.T) {
"001_init.up.sql": {Data: []byte("CREATE TABLE a;")}, "001_init.up.sql": {Data: []byte("CREATE TABLE a;")},
"001_init.down.sql": {Data: []byte("DROP TABLE a;")}, "001_init.down.sql": {Data: []byte("DROP TABLE a;")},
} }
migrations, err := sqlmigrate.Collect(fsys) migrations, err := sqlmigrate.Collect(fsys, ".")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -116,7 +116,7 @@ func TestCollect(t *testing.T) {
fsys := fstest.MapFS{ fsys := fstest.MapFS{
"001_only-up.up.sql": {Data: []byte("CREATE TABLE x;")}, "001_only-up.up.sql": {Data: []byte("CREATE TABLE x;")},
} }
_, err := sqlmigrate.Collect(fsys) _, err := sqlmigrate.Collect(fsys, ".")
if !errors.Is(err, sqlmigrate.ErrMissingDown) { if !errors.Is(err, sqlmigrate.ErrMissingDown) {
t.Errorf("got %v, want ErrMissingDown", err) t.Errorf("got %v, want ErrMissingDown", err)
} }
@ -126,7 +126,7 @@ func TestCollect(t *testing.T) {
fsys := fstest.MapFS{ fsys := fstest.MapFS{
"001_only-down.down.sql": {Data: []byte("DROP TABLE x;")}, "001_only-down.down.sql": {Data: []byte("DROP TABLE x;")},
} }
_, err := sqlmigrate.Collect(fsys) _, err := sqlmigrate.Collect(fsys, ".")
if !errors.Is(err, sqlmigrate.ErrMissingUp) { if !errors.Is(err, sqlmigrate.ErrMissingUp) {
t.Errorf("got %v, want ErrMissingUp", err) t.Errorf("got %v, want ErrMissingUp", err)
} }
@ -139,7 +139,7 @@ func TestCollect(t *testing.T) {
"README.md": {Data: []byte("# Migrations")}, "README.md": {Data: []byte("# Migrations")},
"_migrations.sql": {Data: []byte("SELECT name FROM _migrations;")}, "_migrations.sql": {Data: []byte("SELECT name FROM _migrations;")},
} }
migrations, err := sqlmigrate.Collect(fsys) migrations, err := sqlmigrate.Collect(fsys, ".")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -150,7 +150,7 @@ func TestCollect(t *testing.T) {
t.Run("empty fs", func(t *testing.T) { t.Run("empty fs", func(t *testing.T) {
fsys := fstest.MapFS{} fsys := fstest.MapFS{}
migrations, err := sqlmigrate.Collect(fsys) migrations, err := sqlmigrate.Collect(fsys, ".")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }