Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ production:
table: migrations
```

To process only the specified directory, set `dir: migrations/sqlite3`, to process the specified directory and all of its subdirectories recursively, set `dir: migrations/sqlite3/*`.

(See more examples for different set ups [here](test-integration/dbconfig.yml))

Also one can obtain env variables in datasource field via `os.ExpandEnv` embedded call for the field.
Expand Down
76 changes: 76 additions & 0 deletions migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
Expand Down Expand Up @@ -300,6 +302,80 @@ func migrationFromFile(dir http.FileSystem, root string, info os.FileInfo) (*Mig
return migration, nil
}

type RecursiveFileMigrationSource struct {
Dir string
}

var _ MigrationSource = (*RecursiveFileMigrationSource)(nil)

func (r RecursiveFileMigrationSource) FindMigrations() ([]*Migration, error) {
dirfs := os.DirFS(r.Dir)
migrationIDs := make(map[string]string)
var migrations []*Migration
if err := fs.WalkDir(dirfs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return fmt.Errorf("Error while walkdir %s: %w", path, err)
}
if d.IsDir() {
return nil
}

migrationID := d.Name()
if existPath, exist := migrationIDs[migrationID]; exist {
return fmt.Errorf("duplicate ID '%s' found in both path %s and path %s", migrationID, existPath, path)
}
migrationIDs[migrationID] = path

migration, err := migrationFromFS(dirfs, migrationID, path)
if err != nil {
return err
}
migrations = append(migrations, migration)
return nil
}); err != nil {
return nil, err
}

// Make sure migrations are sorted
sort.Sort(byId(migrations))

return migrations, nil
}

func migrationFromFS(dirfs fs.FS, migrationID, path string) (*Migration, error) {
file, err := dirfs.Open(path)
if err != nil {
return nil, fmt.Errorf("Error while opening %s: %w", path, err)
}
defer func() { _ = file.Close() }()

rs, ok := file.(io.ReadSeeker)
if !ok {
data, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("Error while read file %s: %w", path, err)
}
rs = io.NewSectionReader(bytes.NewReader(data), 0, int64(len(data)))
}
migration, err := ParseMigration(migrationID, rs)
if err != nil {
return nil, fmt.Errorf("Error while parsing %s: %w", path, err)
}

return migration, nil
}

func MakeFileMigrationSource(dir string) MigrationSource {
if _, last := filepath.Split(dir); last == "*" {
return RecursiveFileMigrationSource{
Dir: filepath.Dir(dir),
}
}
return FileMigrationSource{
Dir: dir,
}
}

// Migrations from a bindata asset set.
type AssetMigrationSource struct {
// Asset should return content of file in path if exists
Expand Down
34 changes: 34 additions & 0 deletions migrate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"database/sql"
"embed"
"net/http"
"path/filepath"
"time"

"github.com/go-gorp/gorp/v3"
Expand Down Expand Up @@ -130,6 +131,39 @@ func (s *SqliteMigrateSuite) TestFileMigrate(c *C) {
c.Assert(id, Equals, int64(1))
}

func (s *SqliteMigrateSuite) TestRecursiveFileMigrate(c *C) {
migrations := &RecursiveFileMigrationSource{
Dir: "test-migrations",
}

// Executes two migrations
n, err := Exec(s.Db, "sqlite3", migrations, Up)
c.Assert(err, IsNil)
c.Assert(n, Equals, 4)

// Has data
id, err := s.DbMap.SelectInt("SELECT id FROM people")
c.Assert(err, IsNil)
c.Assert(id, Equals, int64(1))

name, err := s.DbMap.SelectStr("SELECT name FROM people")
c.Assert(err, IsNil)
c.Assert(name, Equals, "test")
}

func (s *SqliteMigrateSuite) TestMakeFileMigrationSource(c *C) {
{
dir := filepath.Join("aaa", "bbb", "ccc")
got := MakeFileMigrationSource(dir)
c.Assert(got, Equals, FileMigrationSource{Dir: dir})
}
{
dir := filepath.Join("aaa", "bbb", "*")
got := MakeFileMigrationSource(dir)
c.Assert(got, Equals, RecursiveFileMigrationSource{Dir: filepath.Join("aaa", "bbb")})
}
}

func (s *SqliteMigrateSuite) TestHttpFileSystemMigrate(c *C) {
migrations := &HttpFileSystemMigrationSource{
FileSystem: http.Dir("test-migrations"),
Expand Down
4 changes: 1 addition & 3 deletions sql-migrate/command_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ func ApplyMigrations(dir migrate.MigrationDirection, dryrun bool, limit int, ver
}
defer db.Close()

source := migrate.FileMigrationSource{
Dir: env.Dir,
}
source := migrate.MakeFileMigrationSource(env.Dir)

if dryrun {
var migrations []*migrate.PlannedMigration
Expand Down
5 changes: 5 additions & 0 deletions test-migrations/dir1/3_add_column.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE people ADD COLUMN name varchar(255);

-- +migrate Down
ALTER TABLE people DROP COLUMN name varchar(255);
5 changes: 5 additions & 0 deletions test-migrations/dir1/dir2/4_set_name.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- +migrate Up
UPDATE people SET name = 'test' WHERE id = 1;

-- +migrate Down
UPDATE people SET name = NULL WHERE id = 1;