Skip to content

feat(source): add embed source support #1285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
21 changes: 21 additions & 0 deletions source/embed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# embed

```golang
//go:embed *.sql
var MigrationFiles embed.FS

```

```golang
embed, err := migrations.NewEmbed(migrations.MigrationFiles, ".")
if err != nil {
klog.Error(fmt.Sprintf("newConnectionEngine migrations.NewEmbed error:%v", err))
return
}
m, err := migrate.NewWithInstance(
"embed",
embed,
"mysql",
dbdriver,
)
```
162 changes: 162 additions & 0 deletions source/embed/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package embed

import (
"embed"
"errors"
"fmt"
"io"
"io/fs"
"path"
"strconv"

"github.com/golang-migrate/migrate/v4/source"
)

type Embed struct {
FS embed.FS
migrations *source.Migrations
path string
}

// NewEmbed returns a new Driver using the embed.FS and a relative path.
func NewEmbed(fsys embed.FS, path string) (source.Driver, error) {
var e Embed
if err := e.Init(fsys, path); err != nil {
return nil, fmt.Errorf("failed to init embed driver with path %s: %w", path, err)
}
return &e, nil
}

// Open is part of source.Driver interface implementation.
// Open cannot be called on the embed driver directly as it's designed to use embed.FS.
func (e *Embed) Open(url string) (source.Driver, error) {
return nil, errors.New("Open() cannot be called on the embed driver")
}

// Init prepares Embed instance to read migrations from embed.FS and a relative path.
func (e *Embed) Init(fsys embed.FS, path string) error {
entries, err := fs.ReadDir(fsys, path)
if err != nil {
return err
}

ms := source.NewMigrations()
for _, e := range entries {
if e.IsDir() {
continue
}
m, err := source.DefaultParse(e.Name())
if err != nil {
continue
}
file, err := e.Info()
if err != nil {
return err
}
if !ms.Append(m) {
return source.ErrDuplicateMigration{
Migration: *m,
FileInfo: file,
}
}
}

e.FS = fsys
e.path = path
e.migrations = ms
return nil
}

// Close is part of source.Driver interface implementation.
func (e *Embed) Close() error {
// Since embed.FS doesn't support Close(), this method is a no-op
return nil
}

// First is part of source.Driver interface implementation.
func (e *Embed) First() (version uint, err error) {
if version, ok := e.migrations.First(); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "first",
Path: e.path,
Err: fs.ErrNotExist,
}
}

// Prev is part of source.Driver interface implementation.
func (e *Embed) Prev(version uint) (prevVersion uint, err error) {
if version, ok := e.migrations.Prev(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
Path: e.path,
Err: fs.ErrNotExist,
}
}

// Next is part of source.Driver interface implementation.
func (e *Embed) Next(version uint) (nextVersion uint, err error) {
if version, ok := e.migrations.Next(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
Path: e.path,
Err: fs.ErrNotExist,
}
}

// ReadUp is part of source.Driver interface implementation.
func (e *Embed) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := e.migrations.Up(version); ok {
body, err := e.FS.ReadFile(path.Join(e.path, m.Raw))
if err != nil {
return nil, "", err
}
return io.NopCloser(&fileReader{data: body}), m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
Path: e.path,
Err: fs.ErrNotExist,
}
}

// ReadDown is part of source.Driver interface implementation.
func (e *Embed) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := e.migrations.Down(version); ok {
body, err := e.FS.ReadFile(path.Join(e.path, m.Raw))
if err != nil {
return nil, "", err
}
return io.NopCloser(&fileReader{data: body}), m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
Path: e.path,
Err: fs.ErrNotExist,
}
}

// fileReader []byte to io.ReadCloser
type fileReader struct {
data []byte
pos int
}

func (fr *fileReader) Read(p []byte) (n int, err error) {
if fr.pos >= len(fr.data) {
return 0, io.EOF
}
n = copy(p, fr.data[fr.pos:])
fr.pos += n
return n, nil
}

func (fr *fileReader) Close() error {
// do nothing, as embed.FS does not require closing
return nil
}
204 changes: 204 additions & 0 deletions source/embed/embed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package embed

import (
"embed"
"errors"
"io"
"io/fs"
"testing"

"github.com/golang-migrate/migrate/v4/source"
st "github.com/golang-migrate/migrate/v4/source/testing"
)

//go:embed testmigrations/*.sql
var testFS embed.FS

const testPath = "testmigrations"

func Test(t *testing.T) {
driver, err := NewEmbed(testFS, testPath)
if err != nil {
t.Fatal(err)
}

st.Test(t, driver)
}

func TestNewEmbed_Success(t *testing.T) {
driver, err := NewEmbed(testFS, testPath)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if driver == nil {
t.Fatal("expected driver, got nil")
}
}

func TestNewEmbed_InvalidPath(t *testing.T) {
_, err := NewEmbed(testFS, "doesnotexist")
if err == nil {
t.Fatal("expected error for invalid path, got nil")
}
}

func TestEmbed_Open(t *testing.T) {
driver, _ := NewEmbed(testFS, "testmigrations")
_, err := driver.(*Embed).Open("someurl")
if err == nil || err.Error() != "Open() cannot be called on the embed driver" {
t.Fatalf("expected Open() error, got %v", err)
}
}

func TestEmbed_First(t *testing.T) {
driver, _ := NewEmbed(testFS, "testmigrations")
version, err := driver.First()
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if version == 0 {
t.Fatal("expected non-zero version")
}
}

func TestEmbed_First_Empty(t *testing.T) {
emptyFS := embed.FS{}
e := &Embed{}
e.FS = emptyFS
e.path = "empty"
e.migrations = source.NewMigrations()
_, err := e.First()
if err == nil {
t.Fatal("expected error for empty migrations")
}
}

func TestEmbed_PrevNext(t *testing.T) {
driver, _ := NewEmbed(testFS, "testmigrations")
first, _ := driver.First()
_, err := driver.Prev(first)
if err == nil {
t.Fatal("expected error for prev of first migration")
}
next, err := driver.Next(first)
if err != nil {
t.Fatalf("expected no error for next, got %v", err)
}
if next == 0 {
t.Fatal("expected next version to be non-zero")
}
}

func TestEmbed_ReadUpDown(t *testing.T) {
driver, _ := NewEmbed(testFS, "testmigrations")
first, _ := driver.First()
r, id, err := driver.ReadUp(first)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if r == nil || id == "" {
t.Fatal("expected valid reader and identifier")
}
b, err := io.ReadAll(r)
if err != nil {
t.Fatalf("failed to read: %v", err)
}
if len(b) == 0 {
t.Fatal("expected file content")
}
err = r.Close()
if err != nil {
t.Fatalf("failed to close reader: %v", err)
}

// Down migration may not exist for first, so test with next if available
next, _ := driver.Next(first)
rd, idd, err := driver.ReadDown(next)
if err == nil {
if rd == nil || idd == "" {
t.Fatal("expected valid reader and identifier for down")
}
err = rd.Close()
if err != nil {
t.Fatalf("failed to close reader: %v", err)
}
}
}

func TestEmbed_ReadUp_NotExist(t *testing.T) {
driver, _ := NewEmbed(testFS, "testmigrations")
_, _, err := driver.ReadUp(999999)
if err == nil {
t.Fatal("expected error for non-existent migration")
}
var pathErr *fs.PathError
if !errors.As(err, &pathErr) {
t.Fatalf("expected fs.PathError, got %T", err)
}
}

func TestEmbed_Close(t *testing.T) {
driver, _ := NewEmbed(testFS, "testmigrations")
if err := driver.Close(); err != nil {
t.Fatalf("expected no error, got %v", err)
}
}

func TestFileReader_ReadClose(t *testing.T) {
data := []byte("hello world")
fr := &fileReader{data: data}
buf := make([]byte, 5)
n, err := fr.Read(buf)
if n != 5 || err != nil {
t.Fatalf("expected to read 5 bytes, got %d, err %v", n, err)
}
n, err = fr.Read(buf)
if n != 5 || err != nil {
t.Fatalf("expected to read next 5 bytes, got %d, err %v", n, err)
}
n, err = fr.Read(buf)
if n != 1 || err != nil {
t.Fatalf("expected to read last byte, got %d, err %v", n, err)
}
n, err = fr.Read(buf)
if n != 0 || err != io.EOF {
t.Fatalf("expected EOF, got %d, err %v", n, err)
}
if err := fr.Close(); err != nil {
t.Fatalf("expected no error on close, got %v", err)
}
}

// createBenchmarkEmbed creates an Embed driver with test migrations
// This is a helper function for benchmarks
func createBenchmarkEmbed(b *testing.B) *Embed {
driver, err := NewEmbed(testFS, testPath)
if err != nil {
b.Fatal(err)
}
return driver.(*Embed)
}

func BenchmarkFirst(b *testing.B) {
e := createBenchmarkEmbed(b)
b.ResetTimer()
for n := 0; n < b.N; n++ {
_, err := e.First()
if err != nil {
b.Error(err)
}
}
b.StopTimer()
}

func BenchmarkNext(b *testing.B) {
e := createBenchmarkEmbed(b)
b.ResetTimer()
v, err := e.First()
for n := 0; n < b.N; n++ {
for !errors.Is(err, fs.ErrNotExist) {
v, err = e.Next(v)
}
}
b.StopTimer()
}
1 change: 1 addition & 0 deletions source/embed/testmigrations/1_foobar.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1 down
1 change: 1 addition & 0 deletions source/embed/testmigrations/1_foobar.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1 up
1 change: 1 addition & 0 deletions source/embed/testmigrations/3_foobar.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3 up
Loading