whatcanGOwrong

This commit is contained in:
2024-09-19 21:38:24 -04:00
commit d0ae4d841d
17908 changed files with 4096831 additions and 0 deletions
@@ -0,0 +1,3 @@
# aws_s3
`s3://<bucket>/<prefix>`
@@ -0,0 +1,152 @@
package awss3
import (
"fmt"
"io"
"net/url"
"os"
"path"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3iface"
"github.com/golang-migrate/migrate/v4/source"
)
func init() {
source.Register("s3", &s3Driver{})
}
type s3Driver struct {
s3client s3iface.S3API
config *Config
migrations *source.Migrations
}
type Config struct {
Bucket string
Prefix string
}
func (s *s3Driver) Open(folder string) (source.Driver, error) {
config, err := parseURI(folder)
if err != nil {
return nil, err
}
sess, err := session.NewSession()
if err != nil {
return nil, err
}
return WithInstance(s3.New(sess), config)
}
func WithInstance(s3client s3iface.S3API, config *Config) (source.Driver, error) {
driver := &s3Driver{
config: config,
s3client: s3client,
migrations: source.NewMigrations(),
}
if err := driver.loadMigrations(); err != nil {
return nil, err
}
return driver, nil
}
func parseURI(uri string) (*Config, error) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
prefix := strings.Trim(u.Path, "/")
if prefix != "" {
prefix += "/"
}
return &Config{
Bucket: u.Host,
Prefix: prefix,
}, nil
}
func (s *s3Driver) loadMigrations() error {
output, err := s.s3client.ListObjects(&s3.ListObjectsInput{
Bucket: aws.String(s.config.Bucket),
Prefix: aws.String(s.config.Prefix),
Delimiter: aws.String("/"),
})
if err != nil {
return err
}
for _, object := range output.Contents {
_, fileName := path.Split(aws.StringValue(object.Key))
m, err := source.DefaultParse(fileName)
if err != nil {
continue
}
if !s.migrations.Append(m) {
return fmt.Errorf("unable to parse file %v", aws.StringValue(object.Key))
}
}
return nil
}
func (s *s3Driver) Close() error {
return nil
}
func (s *s3Driver) First() (uint, error) {
v, ok := s.migrations.First()
if !ok {
return 0, os.ErrNotExist
}
return v, nil
}
func (s *s3Driver) Prev(version uint) (uint, error) {
v, ok := s.migrations.Prev(version)
if !ok {
return 0, os.ErrNotExist
}
return v, nil
}
func (s *s3Driver) Next(version uint) (uint, error) {
v, ok := s.migrations.Next(version)
if !ok {
return 0, os.ErrNotExist
}
return v, nil
}
func (s *s3Driver) ReadUp(version uint) (io.ReadCloser, string, error) {
if m, ok := s.migrations.Up(version); ok {
return s.open(m)
}
return nil, "", os.ErrNotExist
}
func (s *s3Driver) ReadDown(version uint) (io.ReadCloser, string, error) {
if m, ok := s.migrations.Down(version); ok {
return s.open(m)
}
return nil, "", os.ErrNotExist
}
func (s *s3Driver) open(m *source.Migration) (io.ReadCloser, string, error) {
key := path.Join(s.config.Prefix, m.Raw)
object, err := s.s3client.GetObject(&s3.GetObjectInput{
Bucket: aws.String(s.config.Bucket),
Key: aws.String(key),
})
if err != nil {
return nil, "", err
}
return object.Body, m.Identifier, nil
}
@@ -0,0 +1,127 @@
package awss3
import (
"errors"
"io"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
st "github.com/golang-migrate/migrate/v4/source/testing"
"github.com/stretchr/testify/assert"
)
func Test(t *testing.T) {
s3Client := fakeS3{
bucket: "some-bucket",
objects: map[string]string{
"staging/migrations/1_foobar.up.sql": "1 up",
"staging/migrations/1_foobar.down.sql": "1 down",
"prod/migrations/1_foobar.up.sql": "1 up",
"prod/migrations/1_foobar.down.sql": "1 down",
"prod/migrations/3_foobar.up.sql": "3 up",
"prod/migrations/4_foobar.up.sql": "4 up",
"prod/migrations/4_foobar.down.sql": "4 down",
"prod/migrations/5_foobar.down.sql": "5 down",
"prod/migrations/7_foobar.up.sql": "7 up",
"prod/migrations/7_foobar.down.sql": "7 down",
"prod/migrations/not-a-migration.txt": "",
"prod/migrations/0-random-stuff/whatever.txt": "",
},
}
driver, err := WithInstance(&s3Client, &Config{
Bucket: "some-bucket",
Prefix: "prod/migrations/",
})
if err != nil {
t.Fatal(err)
}
st.Test(t, driver)
}
func TestParseURI(t *testing.T) {
tests := []struct {
name string
uri string
config *Config
}{
{
"with prefix, no trailing slash",
"s3://migration-bucket/production",
&Config{
Bucket: "migration-bucket",
Prefix: "production/",
},
},
{
"without prefix, no trailing slash",
"s3://migration-bucket",
&Config{
Bucket: "migration-bucket",
},
},
{
"with prefix, trailing slash",
"s3://migration-bucket/production/",
&Config{
Bucket: "migration-bucket",
Prefix: "production/",
},
},
{
"without prefix, trailing slash",
"s3://migration-bucket/",
&Config{
Bucket: "migration-bucket",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := parseURI(test.uri)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.config, actual)
})
}
}
type fakeS3 struct {
s3.S3
bucket string
objects map[string]string
}
func (s *fakeS3) ListObjects(input *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
bucket := aws.StringValue(input.Bucket)
if bucket != s.bucket {
return nil, errors.New("bucket not found")
}
prefix := aws.StringValue(input.Prefix)
delimiter := aws.StringValue(input.Delimiter)
var output s3.ListObjectsOutput
for name := range s.objects {
if strings.HasPrefix(name, prefix) {
if delimiter == "" || !strings.Contains(strings.Replace(name, prefix, "", 1), delimiter) {
output.Contents = append(output.Contents, &s3.Object{
Key: aws.String(name),
})
}
}
}
return &output, nil
}
func (s *fakeS3) GetObject(input *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
bucket := aws.StringValue(input.Bucket)
if bucket != s.bucket {
return nil, errors.New("bucket not found")
}
if data, ok := s.objects[aws.StringValue(input.Key)]; ok {
body := io.NopCloser(strings.NewReader(data))
return &s3.GetObjectOutput{Body: body}, nil
}
return nil, errors.New("object not found")
}
@@ -0,0 +1 @@
.bitbucket_test_secrets
@@ -0,0 +1,14 @@
# bitbucket
This driver is catered for those that want to source migrations from bitbucket cloud(https://bitbucket.com).
`bitbucket://user:password@owner/repo/path#ref`
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| user | | The username of the user connecting |
| password | | User's password or an app password with repo read permission |
| owner | | the repo owner |
| repo | | the name of the repository |
| path | | path in repo to migrations |
| ref | | (optional) can be a SHA, branch, or tag |
@@ -0,0 +1,207 @@
package bitbucket
import (
"fmt"
"io"
nurl "net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/golang-migrate/migrate/v4/source"
"github.com/ktrysmt/go-bitbucket"
)
func init() {
source.Register("bitbucket", &Bitbucket{})
}
var (
ErrNoUserInfo = fmt.Errorf("no username:password provided")
ErrNoAccessToken = fmt.Errorf("no password/app password")
ErrInvalidRepo = fmt.Errorf("invalid repo")
ErrInvalidBitbucketClient = fmt.Errorf("expected *bitbucket.Client")
ErrNoDir = fmt.Errorf("no directory")
)
type Bitbucket struct {
config *Config
client *bitbucket.Client
migrations *source.Migrations
}
type Config struct {
Owner string
Repo string
Path string
Ref string
}
func (b *Bitbucket) Open(url string) (source.Driver, error) {
u, err := nurl.Parse(url)
if err != nil {
return nil, err
}
if u.User == nil {
return nil, ErrNoUserInfo
}
password, ok := u.User.Password()
if !ok {
return nil, ErrNoAccessToken
}
cl := bitbucket.NewBasicAuth(u.User.Username(), password)
cfg := &Config{}
// set owner, repo and path in repo
cfg.Owner = u.Host
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(pe) < 1 {
return nil, ErrInvalidRepo
}
cfg.Repo = pe[0]
if len(pe) > 1 {
cfg.Path = strings.Join(pe[1:], "/")
}
cfg.Ref = u.Fragment
bi, err := WithInstance(cl, cfg)
if err != nil {
return nil, err
}
return bi, nil
}
func WithInstance(client *bitbucket.Client, config *Config) (source.Driver, error) {
bi := &Bitbucket{
client: client,
config: config,
migrations: source.NewMigrations(),
}
if err := bi.readDirectory(); err != nil {
return nil, err
}
return bi, nil
}
func (b *Bitbucket) readDirectory() error {
b.ensureFields()
fOpt := &bitbucket.RepositoryFilesOptions{
Owner: b.config.Owner,
RepoSlug: b.config.Repo,
Ref: b.config.Ref,
Path: b.config.Path,
}
dirContents, err := b.client.Repositories.Repository.ListFiles(fOpt)
if err != nil {
return err
}
for _, fi := range dirContents {
m, err := source.DefaultParse(filepath.Base(fi.Path))
if err != nil {
continue // ignore files that we can't parse
}
if !b.migrations.Append(m) {
return fmt.Errorf("unable to parse file %v", fi.Path)
}
}
return nil
}
func (b *Bitbucket) ensureFields() {
if b.config == nil {
b.config = &Config{}
}
}
func (b *Bitbucket) Close() error {
return nil
}
func (b *Bitbucket) First() (version uint, er error) {
b.ensureFields()
if v, ok := b.migrations.First(); !ok {
return 0, &os.PathError{Op: "first", Path: b.config.Path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (b *Bitbucket) Prev(version uint) (prevVersion uint, err error) {
b.ensureFields()
if v, ok := b.migrations.Prev(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (b *Bitbucket) Next(version uint) (nextVersion uint, err error) {
b.ensureFields()
if v, ok := b.migrations.Next(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (b *Bitbucket) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
b.ensureFields()
if m, ok := b.migrations.Up(version); ok {
fBlobOpt := &bitbucket.RepositoryBlobOptions{
Owner: b.config.Owner,
RepoSlug: b.config.Repo,
Ref: b.config.Ref,
Path: path.Join(b.config.Path, m.Raw),
}
file, err := b.client.Repositories.Repository.GetFileBlob(fBlobOpt)
if err != nil {
return nil, "", err
}
if file != nil {
r := file.Content
return io.NopCloser(strings.NewReader(string(r))), m.Identifier, nil
}
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
}
func (b *Bitbucket) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
b.ensureFields()
if m, ok := b.migrations.Down(version); ok {
fBlobOpt := &bitbucket.RepositoryBlobOptions{
Owner: b.config.Owner,
RepoSlug: b.config.Repo,
Ref: b.config.Ref,
Path: path.Join(b.config.Path, m.Raw),
}
file, err := b.client.Repositories.Repository.GetFileBlob(fBlobOpt)
if err != nil {
return nil, "", err
}
if file != nil {
r := file.Content
return io.NopCloser(strings.NewReader(string(r))), m.Identifier, nil
}
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.config.Path, Err: os.ErrNotExist}
}
@@ -0,0 +1,33 @@
package bitbucket
import (
"bytes"
"os"
"testing"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
var BitbucketTestSecret = "" // username:password
func init() {
secrets, err := os.ReadFile(".bitbucket_test_secrets")
if err == nil {
BitbucketTestSecret = string(bytes.TrimSpace(secrets)[:])
}
}
func Test(t *testing.T) {
if len(BitbucketTestSecret) == 0 {
t.Skip("test requires .bitbucket_test_secrets")
}
b := &Bitbucket{}
d, err := b.Open("bitbucket://" + BitbucketTestSecret + "@abhishekbipp/test-migration/migrations/test#master")
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}
@@ -0,0 +1,118 @@
// Package source provides the Source interface.
// All source drivers must implement this interface, register themselves,
// optionally provide a `WithInstance` function and pass the tests
// in package source/testing.
package source
import (
"fmt"
"io"
nurl "net/url"
"sync"
)
var driversMu sync.RWMutex
var drivers = make(map[string]Driver)
// Driver is the interface every source driver must implement.
//
// How to implement a source driver?
// 1. Implement this interface.
// 2. Optionally, add a function named `WithInstance`.
// This function should accept an existing source instance and a Config{} struct
// and return a driver instance.
// 3. Add a test that calls source/testing.go:Test()
// 4. Add own tests for Open(), WithInstance() (when provided) and Close().
// All other functions are tested by tests in source/testing.
// Saves you some time and makes sure all source drivers behave the same way.
// 5. Call Register in init().
//
// Guidelines:
// - All configuration input must come from the URL string in func Open()
// or the Config{} struct in WithInstance. Don't os.Getenv().
// - Drivers are supposed to be read only.
// - Ideally don't load any contents (into memory) in Open or WithInstance.
type Driver interface {
// Open returns a new driver instance configured with parameters
// coming from the URL string. Migrate will call this function
// only once per instance.
Open(url string) (Driver, error)
// Close closes the underlying source instance managed by the driver.
// Migrate will call this function only once per instance.
Close() error
// First returns the very first migration version available to the driver.
// Migrate will call this function multiple times.
// If there is no version available, it must return os.ErrNotExist.
First() (version uint, err error)
// Prev returns the previous version for a given version available to the driver.
// Migrate will call this function multiple times.
// If there is no previous version available, it must return os.ErrNotExist.
Prev(version uint) (prevVersion uint, err error)
// Next returns the next version for a given version available to the driver.
// Migrate will call this function multiple times.
// If there is no next version available, it must return os.ErrNotExist.
Next(version uint) (nextVersion uint, err error)
// ReadUp returns the UP migration body and an identifier that helps
// finding this migration in the source for a given version.
// If there is no up migration available for this version,
// it must return os.ErrNotExist.
// Do not start reading, just return the ReadCloser!
ReadUp(version uint) (r io.ReadCloser, identifier string, err error)
// ReadDown returns the DOWN migration body and an identifier that helps
// finding this migration in the source for a given version.
// If there is no down migration available for this version,
// it must return os.ErrNotExist.
// Do not start reading, just return the ReadCloser!
ReadDown(version uint) (r io.ReadCloser, identifier string, err error)
}
// Open returns a new driver instance.
func Open(url string) (Driver, error) {
u, err := nurl.Parse(url)
if err != nil {
return nil, err
}
if u.Scheme == "" {
return nil, fmt.Errorf("source driver: invalid URL scheme")
}
driversMu.RLock()
d, ok := drivers[u.Scheme]
driversMu.RUnlock()
if !ok {
return nil, fmt.Errorf("source driver: unknown driver '%s' (forgotten import?)", u.Scheme)
}
return d.Open(url)
}
// Register globally registers a driver.
func Register(name string, driver Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("Register called twice for driver " + name)
}
drivers[name] = driver
}
// List lists the registered drivers
func List() []string {
driversMu.RLock()
defer driversMu.RUnlock()
names := make([]string, 0, len(drivers))
for n := range drivers {
names = append(names, n)
}
return names
}
@@ -0,0 +1,8 @@
package source
func ExampleDriver() {
// see source/stub for an example
// source/stub/stub.go has the driver implementation
// source/stub/stub_test.go runs source/testing/test.go:Test
}
@@ -0,0 +1,15 @@
package source
import "os"
// ErrDuplicateMigration is an error type for reporting duplicate migration
// files.
type ErrDuplicateMigration struct {
Migration
os.FileInfo
}
// Error implements error interface.
func (e ErrDuplicateMigration) Error() string {
return "duplicate migration file: " + e.Name()
}
@@ -0,0 +1,4 @@
# file
`file:///absolute/path`
`file://relative/path`
@@ -0,0 +1,66 @@
package file
import (
nurl "net/url"
"os"
"path/filepath"
"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
func init() {
source.Register("file", &File{})
}
type File struct {
iofs.PartialDriver
url string
path string
}
func (f *File) Open(url string) (source.Driver, error) {
p, err := parseURL(url)
if err != nil {
return nil, err
}
nf := &File{
url: url,
path: p,
}
if err := nf.Init(os.DirFS(p), "."); err != nil {
return nil, err
}
return nf, nil
}
func parseURL(url string) (string, error) {
u, err := nurl.Parse(url)
if err != nil {
return "", err
}
// concat host and path to restore full path
// host might be `.`
p := u.Opaque
if len(p) == 0 {
p = u.Host + u.Path
}
if len(p) == 0 {
// default to current directory if no path
wd, err := os.Getwd()
if err != nil {
return "", err
}
p = wd
} else if p[0:1] == "." || p[0:1] != "/" {
// make path absolute if relative
abs, err := filepath.Abs(p)
if err != nil {
return "", err
}
p = abs
}
return p, nil
}
@@ -0,0 +1,202 @@
package file
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"testing"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
const scheme = "file://"
func Test(t *testing.T) {
tmpDir := t.TempDir()
// write files that meet driver test requirements
mustWriteFile(t, tmpDir, "1_foobar.up.sql", "1 up")
mustWriteFile(t, tmpDir, "1_foobar.down.sql", "1 down")
mustWriteFile(t, tmpDir, "3_foobar.up.sql", "3 up")
mustWriteFile(t, tmpDir, "4_foobar.up.sql", "4 up")
mustWriteFile(t, tmpDir, "4_foobar.down.sql", "4 down")
mustWriteFile(t, tmpDir, "5_foobar.down.sql", "5 down")
mustWriteFile(t, tmpDir, "7_foobar.up.sql", "7 up")
mustWriteFile(t, tmpDir, "7_foobar.down.sql", "7 down")
f := &File{}
d, err := f.Open(scheme + tmpDir)
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}
func TestOpen(t *testing.T) {
tmpDir := t.TempDir()
mustWriteFile(t, tmpDir, "1_foobar.up.sql", "")
mustWriteFile(t, tmpDir, "1_foobar.down.sql", "")
if !filepath.IsAbs(tmpDir) {
t.Fatal("expected tmpDir to be absolute path")
}
f := &File{}
_, err := f.Open(scheme + tmpDir) // absolute path
if err != nil {
t.Fatal(err)
}
}
func TestOpenWithRelativePath(t *testing.T) {
tmpDir := t.TempDir()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer func() {
// rescue working dir after we are done
if err := os.Chdir(wd); err != nil {
t.Log(err)
}
}()
if err := os.Chdir(tmpDir); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(tmpDir, "foo"), os.ModePerm); err != nil {
t.Fatal(err)
}
mustWriteFile(t, filepath.Join(tmpDir, "foo"), "1_foobar.up.sql", "")
f := &File{}
// dir: foo
d, err := f.Open("file://foo")
if err != nil {
t.Fatal(err)
}
_, err = d.First()
if err != nil {
t.Fatalf("expected first file in working dir %v for foo", tmpDir)
}
// dir: ./foo
d, err = f.Open("file://./foo")
if err != nil {
t.Fatal(err)
}
_, err = d.First()
if err != nil {
t.Fatalf("expected first file in working dir %v for ./foo", tmpDir)
}
}
func TestOpenDefaultsToCurrentDirectory(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
f := &File{}
d, err := f.Open(scheme)
if err != nil {
t.Fatal(err)
}
if d.(*File).path != wd {
t.Fatal("expected driver to default to current directory")
}
}
func TestOpenWithDuplicateVersion(t *testing.T) {
tmpDir := t.TempDir()
mustWriteFile(t, tmpDir, "1_foo.up.sql", "") // 1 up
mustWriteFile(t, tmpDir, "1_bar.up.sql", "") // 1 up
f := &File{}
_, err := f.Open(scheme + tmpDir)
if err == nil {
t.Fatal("expected err")
}
}
func TestClose(t *testing.T) {
tmpDir := t.TempDir()
f := &File{}
d, err := f.Open(scheme + tmpDir)
if err != nil {
t.Fatal(err)
}
if d.Close() != nil {
t.Fatal("expected nil")
}
}
func mustWriteFile(t testing.TB, dir, file string, body string) {
if err := os.WriteFile(path.Join(dir, file), []byte(body), 06444); err != nil {
t.Fatal(err)
}
}
func mustCreateBenchmarkDir(t *testing.B) (dir string) {
tmpDir := t.TempDir()
for i := 0; i < 1000; i++ {
mustWriteFile(t, tmpDir, fmt.Sprintf("%v_foobar.up.sql", i), "")
mustWriteFile(t, tmpDir, fmt.Sprintf("%v_foobar.down.sql", i), "")
}
return tmpDir
}
func BenchmarkOpen(b *testing.B) {
dir := mustCreateBenchmarkDir(b)
defer func() {
if err := os.RemoveAll(dir); err != nil {
b.Error(err)
}
}()
b.ResetTimer()
for n := 0; n < b.N; n++ {
f := &File{}
_, err := f.Open(scheme + dir)
if err != nil {
b.Error(err)
}
}
b.StopTimer()
}
func BenchmarkNext(b *testing.B) {
dir := mustCreateBenchmarkDir(b)
defer func() {
if err := os.RemoveAll(dir); err != nil {
b.Error(err)
}
}()
f := &File{}
d, _ := f.Open(scheme + dir)
b.ResetTimer()
v, err := d.First()
for n := 0; n < b.N; n++ {
for !errors.Is(err, os.ErrNotExist) {
v, err = d.Next(v)
}
}
b.StopTimer()
}
@@ -0,0 +1 @@
.github_test_secrets
@@ -0,0 +1,16 @@
# github
This driver is catered for those that want to source migrations from [github.com](https://github.com). The URL scheme doesn't require a hostname, as it just simply defaults to `github.com`.
Authenticated client: `github://user:personal-access-token@owner/repo/path#ref`
Unauthenticated client: `github://owner/repo/path#ref`
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| user | | (optional) The username of the user connecting |
| personal-access-token | | (optional) An access token from GitHub (https://github.com/settings/tokens) |
| owner | | the repo owner |
| repo | | the name of the repository |
| path | | path in repo to migrations |
| ref | | (optional) can be a SHA, branch, or tag |
@@ -0,0 +1,5 @@
CREATE TABLE users (
user_id integer unique,
name varchar(40),
email varchar(40)
);
@@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN IF EXISTS city;
@@ -0,0 +1,3 @@
ALTER TABLE users ADD COLUMN city varchar(100);
@@ -0,0 +1,3 @@
CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email);
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1,5 @@
CREATE TABLE books (
user_id integer,
name varchar(40),
author varchar(40)
);
@@ -0,0 +1,5 @@
CREATE TABLE movies (
user_id integer,
name varchar(40),
director varchar(40)
);
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1,215 @@
package github
import (
"context"
"fmt"
"io"
"net/http"
nurl "net/url"
"os"
"path"
"strings"
"golang.org/x/oauth2"
"github.com/golang-migrate/migrate/v4/source"
"github.com/google/go-github/v39/github"
)
func init() {
source.Register("github", &Github{})
}
var (
ErrNoUserInfo = fmt.Errorf("no username:token provided")
ErrNoAccessToken = fmt.Errorf("no access token")
ErrInvalidRepo = fmt.Errorf("invalid repo")
ErrInvalidGithubClient = fmt.Errorf("expected *github.Client")
ErrNoDir = fmt.Errorf("no directory")
)
type Github struct {
config *Config
client *github.Client
options *github.RepositoryContentGetOptions
migrations *source.Migrations
}
type Config struct {
Owner string
Repo string
Path string
Ref string
}
func (g *Github) Open(url string) (source.Driver, error) {
u, err := nurl.Parse(url)
if err != nil {
return nil, err
}
// client defaults to http.DefaultClient
var client *http.Client
if u.User != nil {
password, ok := u.User.Password()
if !ok {
return nil, ErrNoUserInfo
}
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: password},
)
client = oauth2.NewClient(context.Background(), ts)
}
gn := &Github{
client: github.NewClient(client),
migrations: source.NewMigrations(),
options: &github.RepositoryContentGetOptions{Ref: u.Fragment},
}
gn.ensureFields()
// set owner, repo and path in repo
gn.config.Owner = u.Host
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(pe) < 1 {
return nil, ErrInvalidRepo
}
gn.config.Repo = pe[0]
if len(pe) > 1 {
gn.config.Path = strings.Join(pe[1:], "/")
}
if err := gn.readDirectory(); err != nil {
return nil, err
}
return gn, nil
}
func WithInstance(client *github.Client, config *Config) (source.Driver, error) {
gn := &Github{
client: client,
config: config,
migrations: source.NewMigrations(),
options: &github.RepositoryContentGetOptions{Ref: config.Ref},
}
if err := gn.readDirectory(); err != nil {
return nil, err
}
return gn, nil
}
func (g *Github) readDirectory() error {
g.ensureFields()
fileContent, dirContents, _, err := g.client.Repositories.GetContents(
context.Background(),
g.config.Owner,
g.config.Repo,
g.config.Path,
g.options,
)
if err != nil {
return err
}
if fileContent != nil {
return ErrNoDir
}
for _, fi := range dirContents {
m, err := source.DefaultParse(*fi.Name)
if err != nil {
continue // ignore files that we can't parse
}
if !g.migrations.Append(m) {
return fmt.Errorf("unable to parse file %v", *fi.Name)
}
}
return nil
}
func (g *Github) ensureFields() {
if g.config == nil {
g.config = &Config{}
}
}
func (g *Github) Close() error {
return nil
}
func (g *Github) First() (version uint, err error) {
g.ensureFields()
if v, ok := g.migrations.First(); !ok {
return 0, &os.PathError{Op: "first", Path: g.config.Path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (g *Github) Prev(version uint) (prevVersion uint, err error) {
g.ensureFields()
if v, ok := g.migrations.Prev(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (g *Github) Next(version uint) (nextVersion uint, err error) {
g.ensureFields()
if v, ok := g.migrations.Next(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (g *Github) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
g.ensureFields()
if m, ok := g.migrations.Up(version); ok {
r, _, err := g.client.Repositories.DownloadContents(
context.Background(),
g.config.Owner,
g.config.Repo,
path.Join(g.config.Path, m.Raw),
g.options,
)
if err != nil {
return nil, "", err
}
return r, m.Identifier, nil
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
}
func (g *Github) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
g.ensureFields()
if m, ok := g.migrations.Down(version); ok {
r, _, err := g.client.Repositories.DownloadContents(
context.Background(),
g.config.Owner,
g.config.Repo,
path.Join(g.config.Path, m.Raw),
g.options,
)
if err != nil {
return nil, "", err
}
return r, m.Identifier, nil
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.config.Path, Err: os.ErrNotExist}
}
@@ -0,0 +1,59 @@
package github
import (
"bytes"
"fmt"
"os"
"testing"
st "github.com/golang-migrate/migrate/v4/source/testing"
"github.com/stretchr/testify/assert"
)
var GithubTestSecret = "" // username:token
func init() {
secrets, err := os.ReadFile(".github_test_secrets")
if err == nil {
GithubTestSecret = string(bytes.TrimSpace(secrets)[:])
}
}
func Test(t *testing.T) {
if len(GithubTestSecret) == 0 {
t.Skip("test requires .github_test_secrets")
}
g := &Github{}
d, err := g.Open("github://" + GithubTestSecret + "@mattes/migrate_test_tmp/test#452b8003e7")
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}
func TestDefaultClient(t *testing.T) {
g := &Github{}
owner := "golang-migrate"
repo := "migrate"
path := "source/github/examples/migrations"
url := fmt.Sprintf("github://%s/%s/%s", owner, repo, path)
d, err := g.Open(url)
if err != nil {
t.Fatal(err)
}
ver, err := d.First()
if err != nil {
t.Fatal(err)
}
assert.Equal(t, uint(1085649617), ver)
ver, err = d.Next(ver)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, uint(1185749658), ver)
}
@@ -0,0 +1 @@
.github_test_secrets
@@ -0,0 +1,21 @@
# github ee
## GitHub Enterprise Edition
This driver is catered for those who run GitHub Enterprise under private infrastructure.
The below URL scheme illustrates how to source migration files from GitHub Enterprise.
GitHub client for Go requires API and Uploads endpoint hosts in order to create an instance of GitHub Enterprise Client. We're making an assumption that the API and Uploads are available under `https://api.*` and `https://uploads.*` respectively. [GitHub Enterprise Installation Guide](https://help.github.com/en/enterprise/2.15/admin/installation/enabling-subdomain-isolation) recommends that you enable Subdomain isolation feature.
`github-ee://user:personal-access-token@host/owner/repo/path?verify-tls=true#ref`
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| user | | The username of the user connecting |
| personal-access-token | | Personal access token from your GitHub Enterprise instance |
| owner | | the repo owner |
| repo | | the name of the repository |
| path | | path in repo to migrations |
| ref | | (optional) can be a SHA, branch, or tag |
| verify-tls | | (optional) defaults to `true`. This option sets `tls.Config.InsecureSkipVerify` accordingly |
@@ -0,0 +1,97 @@
package github_ee
import (
"crypto/tls"
"fmt"
"net/http"
nurl "net/url"
"strconv"
"strings"
"github.com/golang-migrate/migrate/v4/source"
gh "github.com/golang-migrate/migrate/v4/source/github"
"github.com/google/go-github/v39/github"
)
func init() {
source.Register("github-ee", &GithubEE{})
}
type GithubEE struct {
source.Driver
}
func (g *GithubEE) Open(url string) (source.Driver, error) {
verifyTLS := true
u, err := nurl.Parse(url)
if err != nil {
return nil, err
}
if o := u.Query().Get("verify-tls"); o != "" {
verifyTLS = parseBool(o, verifyTLS)
}
if u.User == nil {
return nil, gh.ErrNoUserInfo
}
password, ok := u.User.Password()
if !ok {
return nil, gh.ErrNoUserInfo
}
ghc, err := g.createGithubClient(u.Host, u.User.Username(), password, verifyTLS)
if err != nil {
return nil, err
}
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(pe) < 1 {
return nil, gh.ErrInvalidRepo
}
cfg := &gh.Config{
Owner: pe[0],
Repo: pe[1],
Ref: u.Fragment,
}
if len(pe) > 2 {
cfg.Path = strings.Join(pe[2:], "/")
}
i, err := gh.WithInstance(ghc, cfg)
if err != nil {
return nil, err
}
return &GithubEE{Driver: i}, nil
}
func (g *GithubEE) createGithubClient(host, username, password string, verifyTLS bool) (*github.Client, error) {
tr := &github.BasicAuthTransport{
Username: username,
Password: password,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: !verifyTLS},
},
}
apiHost := fmt.Sprintf("https://%s/api/v3", host)
uploadHost := fmt.Sprintf("https://uploads.%s", host)
return github.NewEnterpriseClient(apiHost, uploadHost, tr.Client())
}
func parseBool(val string, fallback bool) bool {
b, err := strconv.ParseBool(val)
if err != nil {
return fallback
}
return b
}
@@ -0,0 +1,44 @@
package github_ee
import (
"net/http"
"net/http/httptest"
nurl "net/url"
"testing"
)
func Test(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v3/repos/mattes/migrate_test_tmp/contents/test" {
w.WriteHeader(http.StatusNotFound)
return
}
if ref := r.URL.Query().Get("ref"); ref != "452b8003e7" {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("[]"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}))
defer ts.Close()
u, err := nurl.Parse(ts.URL)
if err != nil {
t.Fatal(err)
}
g := &GithubEE{}
_, err = g.Open("github-ee://foo:bar@" + u.Host + "/mattes/migrate_test_tmp/test?verify-tls=false#452b8003e7")
if err != nil {
t.Fatal(err)
}
}
@@ -0,0 +1 @@
.gitlab_test_secrets
@@ -0,0 +1,12 @@
# gitlab
`gitlab://user:personal-access-token@gitlab_url/project_id/path#ref`
| URL Query | WithInstance Config | Description |
|------------|---------------------|-------------|
| user | | The username of the user connecting |
| personal-access-token | | An access token from Gitlab (https://<gitlab_url>/profile/personal_access_tokens) |
| gitlab_url | | url of the gitlab server |
| project_id | | id of the repository |
| path | | path in repo to migrations |
| ref | | (optional) can be a SHA, branch, or tag |
@@ -0,0 +1,5 @@
CREATE TABLE users (
user_id integer unique,
name varchar(40),
email varchar(40)
);
@@ -0,0 +1 @@
ALTER TABLE users DROP COLUMN IF EXISTS city;
@@ -0,0 +1,3 @@
ALTER TABLE users ADD COLUMN city varchar(100);
@@ -0,0 +1,3 @@
CREATE UNIQUE INDEX CONCURRENTLY users_email_index ON users (email);
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1,5 @@
CREATE TABLE books (
user_id integer,
name varchar(40),
author varchar(40)
);
@@ -0,0 +1,5 @@
CREATE TABLE movies (
user_id integer,
name varchar(40),
director varchar(40)
);
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1 @@
-- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed interdum velit, tristique iaculis justo. Pellentesque ut porttitor dolor. Donec sit amet pharetra elit. Cras vel ligula ex. Phasellus posuere.
@@ -0,0 +1,237 @@
package gitlab
import (
"encoding/base64"
"fmt"
"io"
"net/http"
nurl "net/url"
"os"
"strconv"
"strings"
"github.com/golang-migrate/migrate/v4/source"
"github.com/xanzy/go-gitlab"
)
func init() {
source.Register("gitlab", &Gitlab{})
}
const DefaultMaxItemsPerPage = 100
var (
ErrNoUserInfo = fmt.Errorf("no username:token provided")
ErrNoAccessToken = fmt.Errorf("no access token")
ErrInvalidHost = fmt.Errorf("invalid host")
ErrInvalidProjectID = fmt.Errorf("invalid project id")
ErrInvalidResponse = fmt.Errorf("invalid response")
)
type Gitlab struct {
client *gitlab.Client
url string
projectID string
path string
listOptions *gitlab.ListTreeOptions
getOptions *gitlab.GetFileOptions
migrations *source.Migrations
}
type Config struct {
}
func (g *Gitlab) Open(url string) (source.Driver, error) {
u, err := nurl.Parse(url)
if err != nil {
return nil, err
}
if u.User == nil {
return nil, ErrNoUserInfo
}
password, ok := u.User.Password()
if !ok {
return nil, ErrNoAccessToken
}
gn := &Gitlab{
client: gitlab.NewClient(nil, password),
url: url,
migrations: source.NewMigrations(),
}
if u.Host != "" {
uri := nurl.URL{
Scheme: "https",
Host: u.Host,
}
err = gn.client.SetBaseURL(uri.String())
if err != nil {
return nil, ErrInvalidHost
}
}
pe := strings.Split(strings.Trim(u.Path, "/"), "/")
if len(pe) < 1 {
return nil, ErrInvalidProjectID
}
gn.projectID = pe[0]
if len(pe) > 1 {
gn.path = strings.Join(pe[1:], "/")
}
gn.listOptions = &gitlab.ListTreeOptions{
Path: &gn.path,
Ref: &u.Fragment,
ListOptions: gitlab.ListOptions{
PerPage: DefaultMaxItemsPerPage,
},
}
gn.getOptions = &gitlab.GetFileOptions{
Ref: &u.Fragment,
}
if err := gn.readDirectory(); err != nil {
return nil, err
}
return gn, nil
}
func WithInstance(client *gitlab.Client, config *Config) (source.Driver, error) {
gn := &Gitlab{
client: client,
migrations: source.NewMigrations(),
}
if err := gn.readDirectory(); err != nil {
return nil, err
}
return gn, nil
}
func (g *Gitlab) readDirectory() error {
var nodes []*gitlab.TreeNode
for {
n, response, err := g.client.Repositories.ListTree(g.projectID, g.listOptions)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK {
return ErrInvalidResponse
}
nodes = append(nodes, n...)
if response.CurrentPage >= response.TotalPages {
break
}
g.listOptions.ListOptions.Page = response.NextPage
}
for i := range nodes {
m, err := g.nodeToMigration(nodes[i])
if err != nil {
continue
}
if !g.migrations.Append(m) {
return fmt.Errorf("unable to parse file %v", nodes[i].Name)
}
}
return nil
}
func (g *Gitlab) nodeToMigration(node *gitlab.TreeNode) (*source.Migration, error) {
m := source.Regex.FindStringSubmatch(node.Name)
if len(m) == 5 {
versionUint64, err := strconv.ParseUint(m[1], 10, 64)
if err != nil {
return nil, err
}
return &source.Migration{
Version: uint(versionUint64),
Identifier: m[2],
Direction: source.Direction(m[3]),
Raw: g.path + "/" + node.Name,
}, nil
}
return nil, source.ErrParse
}
func (g *Gitlab) Close() error {
return nil
}
func (g *Gitlab) First() (version uint, er error) {
if v, ok := g.migrations.First(); !ok {
return 0, &os.PathError{Op: "first", Path: g.path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (g *Gitlab) Prev(version uint) (prevVersion uint, err error) {
if v, ok := g.migrations.Prev(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: g.path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (g *Gitlab) Next(version uint) (nextVersion uint, err error) {
if v, ok := g.migrations.Next(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: g.path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (g *Gitlab) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := g.migrations.Up(version); ok {
f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions)
if err != nil {
return nil, "", err
}
if response.StatusCode != http.StatusOK {
return nil, "", ErrInvalidResponse
}
content, err := base64.StdEncoding.DecodeString(f.Content)
if err != nil {
return nil, "", err
}
return io.NopCloser(strings.NewReader(string(content))), m.Identifier, nil
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
}
func (g *Gitlab) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := g.migrations.Down(version); ok {
f, response, err := g.client.RepositoryFiles.GetFile(g.projectID, m.Raw, g.getOptions)
if err != nil {
return nil, "", err
}
if response.StatusCode != http.StatusOK {
return nil, "", ErrInvalidResponse
}
content, err := base64.StdEncoding.DecodeString(f.Content)
if err != nil {
return nil, "", err
}
return io.NopCloser(strings.NewReader(string(content))), m.Identifier, nil
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: g.path, Err: os.ErrNotExist}
}
@@ -0,0 +1,32 @@
package gitlab
import (
"bytes"
"os"
"testing"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
var GitlabTestSecret = "" // username:token
func init() {
secrets, err := os.ReadFile(".gitlab_test_secrets")
if err == nil {
GitlabTestSecret = string(bytes.TrimSpace(secrets)[:])
}
}
func Test(t *testing.T) {
if len(GitlabTestSecret) == 0 {
t.Skip("test requires .gitlab_test_secrets")
}
g := &Gitlab{}
d, err := g.Open("gitlab://" + GitlabTestSecret + "@gitlab.com/11197284/migrations")
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}
@@ -0,0 +1,43 @@
# go_bindata
## Usage
### Read bindata with NewWithSourceInstance
```shell
go get -u github.com/jteeuwen/go-bindata/...
cd examples/migrations && go-bindata -pkg migrations .
```
```go
import (
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/source/go_bindata"
"github.com/golang-migrate/migrate/v4/source/go_bindata/examples/migrations"
)
func main() {
// wrap assets into Resource
s := bindata.Resource(migrations.AssetNames(),
func(name string) ([]byte, error) {
return migrations.Asset(name)
})
d, err := bindata.WithInstance(s)
m, err := migrate.NewWithSourceInstance("go-bindata", d, "database://foobar")
m.Up() // run your migrations and handle the errors above of course
}
```
### Read bindata with URL (todo)
This will restore the assets in a tmp directory and then
proxy to source/file. go-bindata must be in your `$PATH`.
```
migrate -source go-bindata://examples/migrations/bindata.go
```
@@ -0,0 +1,305 @@
// Code generated by go-bindata.
// sources:
// 1085649617_create_users_table.down.sql
// 1085649617_create_users_table.up.sql
// 1185749658_add_city_to_users.down.sql
// 1185749658_add_city_to_users.up.sql
// DO NOT EDIT!
package testdata
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var __1085649617_create_users_tableDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\xf0\x74\x53\x70\x8d\xf0\x0c\x0e\x09\x56\x28\x2d\x4e\x2d\x2a\xb6\xe6\x02\x04\x00\x00\xff\xff\x2c\x02\x3d\xa7\x1c\x00\x00\x00")
func _1085649617_create_users_tableDownSqlBytes() ([]byte, error) {
return bindataRead(
__1085649617_create_users_tableDownSql,
"1085649617_create_users_table.down.sql",
)
}
func _1085649617_create_users_tableDownSql() (*asset, error) {
bytes, err := _1085649617_create_users_tableDownSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "1085649617_create_users_table.down.sql", size: 28, mode: os.FileMode(420), modTime: time.Unix(1485750305, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __1085649617_create_users_tableUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\x0e\x72\x75\x0c\x71\x55\x08\x71\x74\xf2\x71\x55\x28\x2d\x4e\x2d\x2a\x56\xd0\xe0\x52\x00\xb3\xe2\x33\x53\x14\x32\xf3\x4a\x52\xd3\x53\x8b\x14\x4a\xf3\x32\x0b\x4b\x53\x75\xb8\x14\x14\xf2\x12\x73\x53\x15\x14\x14\x14\xca\x12\x8b\x92\x33\x12\x8b\x34\x4c\x0c\x34\x41\xc2\xa9\xb9\x89\x99\x39\xa8\xc2\x5c\x9a\xd6\x5c\x80\x00\x00\x00\xff\xff\xa3\x57\xbc\x0b\x5f\x00\x00\x00")
func _1085649617_create_users_tableUpSqlBytes() ([]byte, error) {
return bindataRead(
__1085649617_create_users_tableUpSql,
"1085649617_create_users_table.up.sql",
)
}
func _1085649617_create_users_tableUpSql() (*asset, error) {
bytes, err := _1085649617_create_users_tableUpSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "1085649617_create_users_table.up.sql", size: 95, mode: os.FileMode(420), modTime: time.Unix(1485803085, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __1185749658_add_city_to_usersDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x28\x2d\x4e\x2d\x2a\x56\x70\x09\xf2\x0f\x50\x70\xf6\xf7\x09\xf5\xf5\x53\xf0\x74\x53\x70\x8d\xf0\x0c\x0e\x09\x56\x48\xce\x2c\xa9\xb4\xe6\x02\x04\x00\x00\xff\xff\xb7\x52\x88\xd7\x2e\x00\x00\x00")
func _1185749658_add_city_to_usersDownSqlBytes() ([]byte, error) {
return bindataRead(
__1185749658_add_city_to_usersDownSql,
"1185749658_add_city_to_users.down.sql",
)
}
func _1185749658_add_city_to_usersDownSql() (*asset, error) {
bytes, err := _1185749658_add_city_to_usersDownSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "1185749658_add_city_to_users.down.sql", size: 46, mode: os.FileMode(420), modTime: time.Unix(1485750443, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __1185749658_add_city_to_usersUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x28\x2d\x4e\x2d\x2a\x56\x70\x74\x71\x51\x70\xf6\xf7\x09\xf5\xf5\x53\x48\xce\x2c\xa9\x54\x28\x4b\x2c\x4a\xce\x48\x2c\xd2\x30\x34\x30\xd0\xb4\xe6\xe2\xe2\x02\x04\x00\x00\xff\xff\xa8\x0f\x49\xc6\x32\x00\x00\x00")
func _1185749658_add_city_to_usersUpSqlBytes() ([]byte, error) {
return bindataRead(
__1185749658_add_city_to_usersUpSql,
"1185749658_add_city_to_users.up.sql",
)
}
func _1185749658_add_city_to_usersUpSql() (*asset, error) {
bytes, err := _1185749658_add_city_to_usersUpSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "1185749658_add_city_to_users.up.sql", size: 50, mode: os.FileMode(420), modTime: time.Unix(1485843733, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"1085649617_create_users_table.down.sql": _1085649617_create_users_tableDownSql,
"1085649617_create_users_table.up.sql": _1085649617_create_users_tableUpSql,
"1185749658_add_city_to_users.down.sql": _1185749658_add_city_to_usersDownSql,
"1185749658_add_city_to_users.up.sql": _1185749658_add_city_to_usersUpSql,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
//
// data/
// foo.txt
// img/
// a.png
// b.png
//
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"1085649617_create_users_table.down.sql": &bintree{_1085649617_create_users_tableDownSql, map[string]*bintree{}},
"1085649617_create_users_table.up.sql": &bintree{_1085649617_create_users_tableUpSql, map[string]*bintree{}},
"1185749658_add_city_to_users.down.sql": &bintree{_1185749658_add_city_to_usersDownSql, map[string]*bintree{}},
"1185749658_add_city_to_users.up.sql": &bintree{_1185749658_add_city_to_usersUpSql, map[string]*bintree{}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = os.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}
@@ -0,0 +1,118 @@
package bindata
import (
"bytes"
"fmt"
"io"
"os"
"github.com/golang-migrate/migrate/v4/source"
)
type AssetFunc func(name string) ([]byte, error)
func Resource(names []string, afn AssetFunc) *AssetSource {
return &AssetSource{
Names: names,
AssetFunc: afn,
}
}
type AssetSource struct {
Names []string
AssetFunc AssetFunc
}
func init() {
source.Register("go-bindata", &Bindata{})
}
type Bindata struct {
path string
assetSource *AssetSource
migrations *source.Migrations
}
func (b *Bindata) Open(url string) (source.Driver, error) {
return nil, fmt.Errorf("not yet implemented")
}
var (
ErrNoAssetSource = fmt.Errorf("expects *AssetSource")
)
func WithInstance(instance interface{}) (source.Driver, error) {
if _, ok := instance.(*AssetSource); !ok {
return nil, ErrNoAssetSource
}
as := instance.(*AssetSource)
bn := &Bindata{
path: "<go-bindata>",
assetSource: as,
migrations: source.NewMigrations(),
}
for _, fi := range as.Names {
m, err := source.DefaultParse(fi)
if err != nil {
continue // ignore files that we can't parse
}
if !bn.migrations.Append(m) {
return nil, fmt.Errorf("unable to parse file %v", fi)
}
}
return bn, nil
}
func (b *Bindata) Close() error {
return nil
}
func (b *Bindata) First() (version uint, err error) {
if v, ok := b.migrations.First(); !ok {
return 0, &os.PathError{Op: "first", Path: b.path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (b *Bindata) Prev(version uint) (prevVersion uint, err error) {
if v, ok := b.migrations.Prev(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("prev for version %v", version), Path: b.path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (b *Bindata) Next(version uint) (nextVersion uint, err error) {
if v, ok := b.migrations.Next(version); !ok {
return 0, &os.PathError{Op: fmt.Sprintf("next for version %v", version), Path: b.path, Err: os.ErrNotExist}
} else {
return v, nil
}
}
func (b *Bindata) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := b.migrations.Up(version); ok {
body, err := b.assetSource.AssetFunc(m.Raw)
if err != nil {
return nil, "", err
}
return io.NopCloser(bytes.NewReader(body)), m.Identifier, nil
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.path, Err: os.ErrNotExist}
}
func (b *Bindata) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := b.migrations.Down(version); ok {
body, err := b.assetSource.AssetFunc(m.Raw)
if err != nil {
return nil, "", err
}
return io.NopCloser(bytes.NewReader(body)), m.Identifier, nil
}
return nil, "", &os.PathError{Op: fmt.Sprintf("read version %v", version), Path: b.path, Err: os.ErrNotExist}
}
@@ -0,0 +1,43 @@
package bindata
import (
"testing"
"github.com/golang-migrate/migrate/v4/source/go_bindata/testdata"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
func Test(t *testing.T) {
// wrap assets into Resource first
s := Resource(testdata.AssetNames(),
func(name string) ([]byte, error) {
return testdata.Asset(name)
})
d, err := WithInstance(s)
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}
func TestWithInstance(t *testing.T) {
// wrap assets into Resource
s := Resource(testdata.AssetNames(),
func(name string) ([]byte, error) {
return testdata.Asset(name)
})
_, err := WithInstance(s)
if err != nil {
t.Fatal(err)
}
}
func TestOpen(t *testing.T) {
b := &Bindata{}
_, err := b.Open("")
if err == nil {
t.Fatal("expected err, because it's not implemented yet")
}
}
@@ -0,0 +1,395 @@
// Code generated by go-bindata.
// sources:
// 1_test.down.sql
// 1_test.up.sql
// 3_test.up.sql
// 4_test.down.sql
// 4_test.up.sql
// 5_test.down.sql
// 7_test.down.sql
// 7_test.up.sql
// DO NOT EDIT!
package testdata
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var __1_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _1_testDownSqlBytes() ([]byte, error) {
return bindataRead(
__1_testDownSql,
"1_test.down.sql",
)
}
func _1_testDownSql() (*asset, error) {
bytes, err := _1_testDownSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "1_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440324, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __1_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _1_testUpSqlBytes() ([]byte, error) {
return bindataRead(
__1_testUpSql,
"1_test.up.sql",
)
}
func _1_testUpSql() (*asset, error) {
bytes, err := _1_testUpSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "1_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440319, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __3_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _3_testUpSqlBytes() ([]byte, error) {
return bindataRead(
__3_testUpSql,
"3_test.up.sql",
)
}
func _3_testUpSql() (*asset, error) {
bytes, err := _3_testUpSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "3_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440331, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __4_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _4_testDownSqlBytes() ([]byte, error) {
return bindataRead(
__4_testDownSql,
"4_test.down.sql",
)
}
func _4_testDownSql() (*asset, error) {
bytes, err := _4_testDownSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "4_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440337, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __4_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _4_testUpSqlBytes() ([]byte, error) {
return bindataRead(
__4_testUpSql,
"4_test.up.sql",
)
}
func _4_testUpSql() (*asset, error) {
bytes, err := _4_testUpSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "4_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440335, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __5_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _5_testDownSqlBytes() ([]byte, error) {
return bindataRead(
__5_testDownSql,
"5_test.down.sql",
)
}
func _5_testDownSql() (*asset, error) {
bytes, err := _5_testDownSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "5_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440340, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __7_testDownSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _7_testDownSqlBytes() ([]byte, error) {
return bindataRead(
__7_testDownSql,
"7_test.down.sql",
)
}
func _7_testDownSql() (*asset, error) {
bytes, err := _7_testDownSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "7_test.down.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440343, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var __7_testUpSql = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func _7_testUpSqlBytes() ([]byte, error) {
return bindataRead(
__7_testUpSql,
"7_test.up.sql",
)
}
func _7_testUpSql() (*asset, error) {
bytes, err := _7_testUpSqlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "7_test.up.sql", size: 0, mode: os.FileMode(420), modTime: time.Unix(1486440347, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"1_test.down.sql": _1_testDownSql,
"1_test.up.sql": _1_testUpSql,
"3_test.up.sql": _3_testUpSql,
"4_test.down.sql": _4_testDownSql,
"4_test.up.sql": _4_testUpSql,
"5_test.down.sql": _5_testDownSql,
"7_test.down.sql": _7_testDownSql,
"7_test.up.sql": _7_testUpSql,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"1_test.down.sql": &bintree{_1_testDownSql, map[string]*bintree{}},
"1_test.up.sql": &bintree{_1_testUpSql, map[string]*bintree{}},
"3_test.up.sql": &bintree{_3_testUpSql, map[string]*bintree{}},
"4_test.down.sql": &bintree{_4_testDownSql, map[string]*bintree{}},
"4_test.up.sql": &bintree{_4_testUpSql, map[string]*bintree{}},
"5_test.down.sql": &bintree{_5_testDownSql, map[string]*bintree{}},
"7_test.down.sql": &bintree{_7_testDownSql, map[string]*bintree{}},
"7_test.up.sql": &bintree{_7_testUpSql, map[string]*bintree{}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = os.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}
@@ -0,0 +1,56 @@
// Package godoc_vfs contains a driver that reads migrations from a virtual file
// system.
//
// Implementations of the filesystem interface that read from zip files and
// maps, as well as the definition of the filesystem interface can be found in
// the golang.org/x/tools/godoc/vfs package.
package godoc_vfs
import (
"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/httpfs"
"golang.org/x/tools/godoc/vfs"
vfs_httpfs "golang.org/x/tools/godoc/vfs/httpfs"
)
func init() {
source.Register("godoc-vfs", &VFS{})
}
// VFS is an implementation of driver that returns migrations from a virtual
// file system.
type VFS struct {
httpfs.PartialDriver
fs vfs.FileSystem
path string
}
// Open implements the source.Driver interface for VFS.
//
// Calling this function panics, instead use the WithInstance function.
// See the package level documentation for an example.
func (b *VFS) Open(url string) (source.Driver, error) {
panic("not implemented")
}
// WithInstance creates a new driver from a virtual file system.
// If a tree named searchPath exists in the virtual filesystem, WithInstance
// searches for migration files there.
// It defaults to "/".
func WithInstance(fs vfs.FileSystem, searchPath string) (source.Driver, error) {
if searchPath == "" {
searchPath = "/"
}
bn := &VFS{
fs: fs,
path: searchPath,
}
if err := bn.Init(vfs_httpfs.New(fs), searchPath); err != nil {
return nil, err
}
return bn, nil
}
@@ -0,0 +1,33 @@
package godoc_vfs_test
import (
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/source/godoc_vfs"
"golang.org/x/tools/godoc/vfs/mapfs"
)
func Example_mapfs() {
fs := mapfs.New(map[string]string{
"1_foobar.up.sql": "1 up",
"1_foobar.down.sql": "1 down",
"3_foobar.up.sql": "3 up",
"4_foobar.up.sql": "4 up",
"4_foobar.down.sql": "4 down",
"5_foobar.down.sql": "5 down",
"7_foobar.up.sql": "7 up",
"7_foobar.down.sql": "7 down",
})
d, err := godoc_vfs.WithInstance(fs, "")
if err != nil {
panic("bad migrations found!")
}
m, err := migrate.NewWithSourceInstance("godoc-vfs", d, "database://foobar")
if err != nil {
panic("error creating the migrations")
}
err = m.Up()
if err != nil {
panic("up failed")
}
}
@@ -0,0 +1,40 @@
package godoc_vfs_test
import (
"testing"
"github.com/golang-migrate/migrate/v4/source/godoc_vfs"
st "github.com/golang-migrate/migrate/v4/source/testing"
"golang.org/x/tools/godoc/vfs/mapfs"
)
func TestVFS(t *testing.T) {
fs := mapfs.New(map[string]string{
"1_foobar.up.sql": "1 up",
"1_foobar.down.sql": "1 down",
"3_foobar.up.sql": "3 up",
"4_foobar.up.sql": "4 up",
"4_foobar.down.sql": "4 down",
"5_foobar.down.sql": "5 down",
"7_foobar.up.sql": "7 up",
"7_foobar.down.sql": "7 down",
})
d, err := godoc_vfs.WithInstance(fs, "")
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}
func TestOpen(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("Expected Open to panic")
}
}()
b := &godoc_vfs.VFS{}
if _, err := b.Open(""); err != nil {
t.Error(err)
}
}
@@ -0,0 +1,14 @@
# Google Cloud Storage
## Import
```go
import (
_ "github.com/golang-migrate/migrate/v4/source/google_cloud_storage"
)
```
## Connection String
`gcs://<bucket>/<prefix>`
@@ -0,0 +1,119 @@
package googlecloudstorage
import (
"fmt"
"io"
"net/url"
"os"
"path"
"strings"
"cloud.google.com/go/storage"
"context"
"github.com/golang-migrate/migrate/v4/source"
"google.golang.org/api/iterator"
)
func init() {
source.Register("gcs", &gcs{})
}
type gcs struct {
bucket *storage.BucketHandle
prefix string
migrations *source.Migrations
}
func (g *gcs) Open(folder string) (source.Driver, error) {
u, err := url.Parse(folder)
if err != nil {
return nil, err
}
client, err := storage.NewClient(context.Background())
if err != nil {
return nil, err
}
driver := gcs{
bucket: client.Bucket(u.Host),
prefix: strings.Trim(u.Path, "/") + "/",
migrations: source.NewMigrations(),
}
err = driver.loadMigrations()
if err != nil {
return nil, err
}
return &driver, nil
}
func (g *gcs) loadMigrations() error {
iter := g.bucket.Objects(context.Background(), &storage.Query{
Prefix: g.prefix,
Delimiter: "/",
})
object, err := iter.Next()
for ; err == nil; object, err = iter.Next() {
_, fileName := path.Split(object.Name)
m, parseErr := source.DefaultParse(fileName)
if parseErr != nil {
continue
}
if !g.migrations.Append(m) {
return fmt.Errorf("unable to parse file %v", object.Name)
}
}
if err != iterator.Done {
return err
}
return nil
}
func (g *gcs) Close() error {
return nil
}
func (g *gcs) First() (uint, error) {
v, ok := g.migrations.First()
if !ok {
return 0, os.ErrNotExist
}
return v, nil
}
func (g *gcs) Prev(version uint) (uint, error) {
v, ok := g.migrations.Prev(version)
if !ok {
return 0, os.ErrNotExist
}
return v, nil
}
func (g *gcs) Next(version uint) (uint, error) {
v, ok := g.migrations.Next(version)
if !ok {
return 0, os.ErrNotExist
}
return v, nil
}
func (g *gcs) ReadUp(version uint) (io.ReadCloser, string, error) {
if m, ok := g.migrations.Up(version); ok {
return g.open(m)
}
return nil, "", os.ErrNotExist
}
func (g *gcs) ReadDown(version uint) (io.ReadCloser, string, error) {
if m, ok := g.migrations.Down(version); ok {
return g.open(m)
}
return nil, "", os.ErrNotExist
}
func (g *gcs) open(m *source.Migration) (io.ReadCloser, string, error) {
objectPath := path.Join(g.prefix, m.Raw)
reader, err := g.bucket.Object(objectPath).NewReader(context.Background())
if err != nil {
return nil, "", err
}
return reader, m.Identifier, nil
}
@@ -0,0 +1,37 @@
package googlecloudstorage
import (
"testing"
"github.com/fsouza/fake-gcs-server/fakestorage"
"github.com/golang-migrate/migrate/v4/source"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
func Test(t *testing.T) {
server := fakestorage.NewServer([]fakestorage.Object{
{BucketName: "some-bucket", Name: "staging/migrations/1_foobar.up.sql", Content: []byte("1 up")},
{BucketName: "some-bucket", Name: "staging/migrations/1_foobar.down.sql", Content: []byte("1 down")},
{BucketName: "some-bucket", Name: "prod/migrations/1_foobar.up.sql", Content: []byte("1 up")},
{BucketName: "some-bucket", Name: "prod/migrations/1_foobar.down.sql", Content: []byte("1 down")},
{BucketName: "some-bucket", Name: "prod/migrations/3_foobar.up.sql", Content: []byte("3 up")},
{BucketName: "some-bucket", Name: "prod/migrations/4_foobar.up.sql", Content: []byte("4 up")},
{BucketName: "some-bucket", Name: "prod/migrations/4_foobar.down.sql", Content: []byte("4 down")},
{BucketName: "some-bucket", Name: "prod/migrations/5_foobar.down.sql", Content: []byte("5 down")},
{BucketName: "some-bucket", Name: "prod/migrations/7_foobar.up.sql", Content: []byte("7 up")},
{BucketName: "some-bucket", Name: "prod/migrations/7_foobar.down.sql", Content: []byte("7 down")},
{BucketName: "some-bucket", Name: "prod/migrations/not-a-migration.txt"},
{BucketName: "some-bucket", Name: "prod/migrations/0-random-stuff/whatever.txt"},
})
defer server.Stop()
driver := gcs{
bucket: server.Client().Bucket("some-bucket"),
prefix: "prod/migrations/",
migrations: source.NewMigrations(),
}
err := driver.loadMigrations()
if err != nil {
t.Fatal(err)
}
st.Test(t, &driver)
}
@@ -0,0 +1,49 @@
# httpfs
## Usage
This package could be used to create new migration source drivers that uses
`http.FileSystem` to read migration files.
Struct `httpfs.PartialDriver` partly implements the `source.Driver` interface. It has all
the methods except for `Open()`. Embedding this struct and adding `Open()` method
allows users of this package to create new migration sources. Example:
```go
struct mydriver {
httpfs.PartialDriver
}
func (d *mydriver) Open(url string) (source.Driver, error) {
var fs http.FileSystem
var path string
var ds mydriver
// acquire fs and path from url
// set-up ds if necessary
if err := ds.Init(fs, path); err != nil {
return nil, err
}
return &ds, nil
}
```
This package also provides a simple `source.Driver` implementation that works
with `http.FileSystem` provided by the user of this package. It is created with
`httpfs.New()` call.
Example of using `http.Dir()` to read migrations from `sql` directory:
```go
src, err := httpfs.New(http.Dir("sql"))
if err != nil {
// do something
}
m, err := migrate.NewWithSourceInstance("httpfs", src, "database://url")
if err != nil {
// do something
}
err = m.Up()
...
```
@@ -0,0 +1,31 @@
package httpfs
import (
"errors"
"net/http"
"github.com/golang-migrate/migrate/v4/source"
)
// driver is a migration source driver for reading migrations from
// http.FileSystem instances. It implements source.Driver interface and can be
// used as a migration source for the main migrate library.
type driver struct {
PartialDriver
}
// New creates a new migrate source driver from a http.FileSystem instance and a
// relative path to migration files within the virtual FS.
func New(fs http.FileSystem, path string) (source.Driver, error) {
var d driver
if err := d.Init(fs, path); err != nil {
return nil, err
}
return &d, nil
}
// Open completes the implementetion of source.Driver interface. Other methods
// are implemented by the embedded PartialDriver struct.
func (d *driver) Open(url string) (source.Driver, error) {
return nil, errors.New("Open() cannot be called on the httpfs passthrough driver")
}
@@ -0,0 +1,42 @@
package httpfs_test
import (
"net/http"
"testing"
"github.com/golang-migrate/migrate/v4/source/httpfs"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
func TestNewOK(t *testing.T) {
d, err := httpfs.New(http.Dir("testdata"), "sql")
if err != nil {
t.Errorf("New() expected not error, got: %s", err)
}
st.Test(t, d)
}
func TestNewErrors(t *testing.T) {
d, err := httpfs.New(http.Dir("does-not-exist"), "")
if err == nil {
t.Errorf("New() expected to return error")
}
if d != nil {
t.Errorf("New() expected to return nil driver")
}
}
func TestOpen(t *testing.T) {
d, err := httpfs.New(http.Dir("testdata/sql"), "")
if err != nil {
t.Error("New() expected no error")
return
}
d, err = d.Open("")
if d != nil {
t.Error("Open() expected to return nil driver")
}
if err == nil {
t.Error("Open() expected to return error")
}
}
@@ -0,0 +1,156 @@
package httpfs
import (
"errors"
"io"
"net/http"
"os"
"path"
"strconv"
"github.com/golang-migrate/migrate/v4/source"
)
// PartialDriver is a helper service for creating new source drivers working with
// http.FileSystem instances. It implements all source.Driver interface methods
// except for Open(). New driver could embed this struct and add missing Open()
// method.
//
// To prepare PartialDriver for use Init() function.
type PartialDriver struct {
migrations *source.Migrations
fs http.FileSystem
path string
}
// Init prepares not initialized PartialDriver instance to read migrations from a
// http.FileSystem instance and a relative path.
func (p *PartialDriver) Init(fs http.FileSystem, path string) error {
root, err := fs.Open(path)
if err != nil {
return err
}
files, err := root.Readdir(0)
if err != nil {
_ = root.Close()
return err
}
if err = root.Close(); err != nil {
return err
}
ms := source.NewMigrations()
for _, file := range files {
if file.IsDir() {
continue
}
m, err := source.DefaultParse(file.Name())
if err != nil {
continue // ignore files that we can't parse
}
if !ms.Append(m) {
return source.ErrDuplicateMigration{
Migration: *m,
FileInfo: file,
}
}
}
p.fs = fs
p.path = path
p.migrations = ms
return nil
}
// Close is part of source.Driver interface implementation. This is a no-op.
func (p *PartialDriver) Close() error {
return nil
}
// First is part of source.Driver interface implementation.
func (p *PartialDriver) First() (version uint, err error) {
if version, ok := p.migrations.First(); ok {
return version, nil
}
return 0, &os.PathError{
Op: "first",
Path: p.path,
Err: os.ErrNotExist,
}
}
// Prev is part of source.Driver interface implementation.
func (p *PartialDriver) Prev(version uint) (prevVersion uint, err error) {
if version, ok := p.migrations.Prev(version); ok {
return version, nil
}
return 0, &os.PathError{
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: os.ErrNotExist,
}
}
// Next is part of source.Driver interface implementation.
func (p *PartialDriver) Next(version uint) (nextVersion uint, err error) {
if version, ok := p.migrations.Next(version); ok {
return version, nil
}
return 0, &os.PathError{
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: os.ErrNotExist,
}
}
// ReadUp is part of source.Driver interface implementation.
func (p *PartialDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := p.migrations.Up(version); ok {
body, err := p.open(path.Join(p.path, m.Raw))
if err != nil {
return nil, "", err
}
return body, m.Identifier, nil
}
return nil, "", &os.PathError{
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: os.ErrNotExist,
}
}
// ReadDown is part of source.Driver interface implementation.
func (p *PartialDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := p.migrations.Down(version); ok {
body, err := p.open(path.Join(p.path, m.Raw))
if err != nil {
return nil, "", err
}
return body, m.Identifier, nil
}
return nil, "", &os.PathError{
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
Path: p.path,
Err: os.ErrNotExist,
}
}
func (p *PartialDriver) open(path string) (http.File, error) {
f, err := p.fs.Open(path)
if err == nil {
return f, nil
}
// Some non-standard file systems may return errors that don't include the path, that
// makes debugging harder.
if !errors.As(err, new(*os.PathError)) {
err = &os.PathError{
Op: "open",
Path: path,
Err: err,
}
}
return nil, err
}
@@ -0,0 +1,107 @@
package httpfs_test
import (
"errors"
"net/http"
"strings"
"testing"
"github.com/golang-migrate/migrate/v4/source"
"github.com/golang-migrate/migrate/v4/source/httpfs"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
type driver struct{ httpfs.PartialDriver }
func (d *driver) Open(url string) (source.Driver, error) { return nil, errors.New("X") }
type driverExample struct {
httpfs.PartialDriver
}
func (d *driverExample) Open(url string) (source.Driver, error) {
parts := strings.Split(url, ":")
dir := parts[0]
path := ""
if len(parts) >= 2 {
path = parts[1]
}
var de driverExample
return &de, de.Init(http.Dir(dir), path)
}
func TestDriverExample(t *testing.T) {
d, err := (*driverExample)(nil).Open("testdata:sql")
if err != nil {
t.Errorf("Open() returned error: %s", err)
}
st.Test(t, d)
}
func TestPartialDriverInit(t *testing.T) {
tests := []struct {
name string
fs http.FileSystem
path string
ok bool
}{
{
name: "valid dir and empty path",
fs: http.Dir("testdata/sql"),
ok: true,
},
{
name: "valid dir and non-empty path",
fs: http.Dir("testdata"),
path: "sql",
ok: true,
},
{
name: "invalid dir",
fs: http.Dir("does-not-exist"),
},
{
name: "file instead of dir",
fs: http.Dir("testdata/sql/1_foobar.up.sql"),
},
{
name: "dir with duplicates",
fs: http.Dir("testdata/duplicates"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var d driver
err := d.Init(test.fs, test.path)
if test.ok {
if err != nil {
t.Errorf("Init() returned error %s", err)
}
st.Test(t, &d)
if err = d.Close(); err != nil {
t.Errorf("Init().Close() returned error %s", err)
}
} else {
if err == nil {
t.Errorf("Init() expected error but did not get one")
}
}
})
}
}
func TestFirstWithNoMigrations(t *testing.T) {
var d driver
fs := http.Dir("testdata/no-migrations")
if err := d.Init(fs, ""); err != nil {
t.Errorf("No error on Init() expected, got: %v", err)
}
if _, err := d.First(); err == nil {
t.Errorf("Expected error on First(), got: %v", err)
}
}
@@ -0,0 +1,3 @@
# iofs
https://pkg.go.dev/github.com/golang-migrate/migrate/v4/source/iofs
@@ -0,0 +1,10 @@
/*
Package iofs provides the Go 1.16+ io/fs#FS driver.
It can accept various file systems (like embed.FS, archive/zip#Reader) implementing io/fs#FS.
This driver cannot be used with Go versions 1.15 and below.
Also, Opening with a URL scheme is not supported.
*/
package iofs
@@ -0,0 +1,32 @@
//go:build go1.16
// +build go1.16
package iofs_test
import (
"embed"
"log"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed testdata/migrations/*.sql
var fs embed.FS
func Example() {
d, err := iofs.New(fs, "testdata/migrations")
if err != nil {
log.Fatal(err)
}
m, err := migrate.NewWithSourceInstance("iofs", d, "postgres://postgres@localhost/postgres?sslmode=disable")
if err != nil {
log.Fatal(err)
}
err = m.Up()
if err != nil {
// ...
}
// ...
}
@@ -0,0 +1,176 @@
//go:build go1.16
// +build go1.16
package iofs
import (
"errors"
"fmt"
"io"
"io/fs"
"path"
"strconv"
"github.com/golang-migrate/migrate/v4/source"
)
type driver struct {
PartialDriver
}
// New returns a new Driver from io/fs#FS and a relative path.
func New(fsys fs.FS, path string) (source.Driver, error) {
var i driver
if err := i.Init(fsys, path); err != nil {
return nil, fmt.Errorf("failed to init driver with path %s: %w", path, err)
}
return &i, nil
}
// Open is part of source.Driver interface implementation.
// Open cannot be called on the iofs passthrough driver.
func (d *driver) Open(url string) (source.Driver, error) {
return nil, errors.New("Open() cannot be called on the iofs passthrough driver")
}
// PartialDriver is a helper service for creating new source drivers working with
// io/fs.FS instances. It implements all source.Driver interface methods
// except for Open(). New driver could embed this struct and add missing Open()
// method.
//
// To prepare PartialDriver for use Init() function.
type PartialDriver struct {
migrations *source.Migrations
fsys fs.FS
path string
}
// Init prepares not initialized IoFS instance to read migrations from a
// io/fs#FS instance and a relative path.
func (d *PartialDriver) Init(fsys fs.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,
}
}
}
d.fsys = fsys
d.path = path
d.migrations = ms
return nil
}
// Close is part of source.Driver interface implementation.
// Closes the file system if possible.
func (d *PartialDriver) Close() error {
c, ok := d.fsys.(io.Closer)
if !ok {
return nil
}
return c.Close()
}
// First is part of source.Driver interface implementation.
func (d *PartialDriver) First() (version uint, err error) {
if version, ok := d.migrations.First(); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "first",
Path: d.path,
Err: fs.ErrNotExist,
}
}
// Prev is part of source.Driver interface implementation.
func (d *PartialDriver) Prev(version uint) (prevVersion uint, err error) {
if version, ok := d.migrations.Prev(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "prev for version " + strconv.FormatUint(uint64(version), 10),
Path: d.path,
Err: fs.ErrNotExist,
}
}
// Next is part of source.Driver interface implementation.
func (d *PartialDriver) Next(version uint) (nextVersion uint, err error) {
if version, ok := d.migrations.Next(version); ok {
return version, nil
}
return 0, &fs.PathError{
Op: "next for version " + strconv.FormatUint(uint64(version), 10),
Path: d.path,
Err: fs.ErrNotExist,
}
}
// ReadUp is part of source.Driver interface implementation.
func (d *PartialDriver) ReadUp(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := d.migrations.Up(version); ok {
body, err := d.open(path.Join(d.path, m.Raw))
if err != nil {
return nil, "", err
}
return body, m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read up for version " + strconv.FormatUint(uint64(version), 10),
Path: d.path,
Err: fs.ErrNotExist,
}
}
// ReadDown is part of source.Driver interface implementation.
func (d *PartialDriver) ReadDown(version uint) (r io.ReadCloser, identifier string, err error) {
if m, ok := d.migrations.Down(version); ok {
body, err := d.open(path.Join(d.path, m.Raw))
if err != nil {
return nil, "", err
}
return body, m.Identifier, nil
}
return nil, "", &fs.PathError{
Op: "read down for version " + strconv.FormatUint(uint64(version), 10),
Path: d.path,
Err: fs.ErrNotExist,
}
}
func (d *PartialDriver) open(path string) (fs.File, error) {
f, err := d.fsys.Open(path)
if err == nil {
return f, nil
}
// Some non-standard file systems may return errors that don't include the path, that
// makes debugging harder.
if !errors.As(err, new(*fs.PathError)) {
err = &fs.PathError{
Op: "open",
Path: path,
Err: err,
}
}
return nil, err
}
@@ -0,0 +1,21 @@
//go:build go1.16
// +build go1.16
package iofs_test
import (
"testing"
"github.com/golang-migrate/migrate/v4/source/iofs"
st "github.com/golang-migrate/migrate/v4/source/testing"
)
func Test(t *testing.T) {
// reuse the embed.FS set in example_test.go
d, err := iofs.New(fs, "testdata/migrations")
if err != nil {
t.Fatal(err)
}
st.Test(t, d)
}
@@ -0,0 +1,133 @@
package source
import (
"sort"
)
// Direction is either up or down.
type Direction string
const (
Down Direction = "down"
Up Direction = "up"
)
// Migration is a helper struct for source drivers that need to
// build the full directory tree in memory.
// Migration is fully independent from migrate.Migration.
type Migration struct {
// Version is the version of this migration.
Version uint
// Identifier can be any string that helps identifying
// this migration in the source.
Identifier string
// Direction is either Up or Down.
Direction Direction
// Raw holds the raw location path to this migration in source.
// ReadUp and ReadDown will use this.
Raw string
}
// Migrations wraps Migration and has an internal index
// to keep track of Migration order.
type Migrations struct {
index uintSlice
migrations map[uint]map[Direction]*Migration
}
func NewMigrations() *Migrations {
return &Migrations{
index: make(uintSlice, 0),
migrations: make(map[uint]map[Direction]*Migration),
}
}
func (i *Migrations) Append(m *Migration) (ok bool) {
if m == nil {
return false
}
if i.migrations[m.Version] == nil {
i.migrations[m.Version] = make(map[Direction]*Migration)
}
// reject duplicate versions
if _, dup := i.migrations[m.Version][m.Direction]; dup {
return false
}
i.migrations[m.Version][m.Direction] = m
i.buildIndex()
return true
}
func (i *Migrations) buildIndex() {
i.index = make(uintSlice, 0, len(i.migrations))
for version := range i.migrations {
i.index = append(i.index, version)
}
sort.Slice(i.index, func(x, y int) bool {
return i.index[x] < i.index[y]
})
}
func (i *Migrations) First() (version uint, ok bool) {
if len(i.index) == 0 {
return 0, false
}
return i.index[0], true
}
func (i *Migrations) Prev(version uint) (prevVersion uint, ok bool) {
pos := i.findPos(version)
if pos >= 1 && len(i.index) > pos-1 {
return i.index[pos-1], true
}
return 0, false
}
func (i *Migrations) Next(version uint) (nextVersion uint, ok bool) {
pos := i.findPos(version)
if pos >= 0 && len(i.index) > pos+1 {
return i.index[pos+1], true
}
return 0, false
}
func (i *Migrations) Up(version uint) (m *Migration, ok bool) {
if _, ok := i.migrations[version]; ok {
if mx, ok := i.migrations[version][Up]; ok {
return mx, true
}
}
return nil, false
}
func (i *Migrations) Down(version uint) (m *Migration, ok bool) {
if _, ok := i.migrations[version]; ok {
if mx, ok := i.migrations[version][Down]; ok {
return mx, true
}
}
return nil, false
}
func (i *Migrations) findPos(version uint) int {
if len(i.index) > 0 {
ix := i.index.Search(version)
if ix < len(i.index) && i.index[ix] == version {
return ix
}
}
return -1
}
type uintSlice []uint
func (s uintSlice) Search(x uint) int {
return sort.Search(len(s), func(i int) bool { return s[i] >= x })
}
@@ -0,0 +1,46 @@
package source
import (
"testing"
)
func TestNewMigrations(t *testing.T) {
// TODO
}
func TestAppend(t *testing.T) {
// TODO
}
func TestBuildIndex(t *testing.T) {
// TODO
}
func TestFirst(t *testing.T) {
// TODO
}
func TestPrev(t *testing.T) {
// TODO
}
func TestUp(t *testing.T) {
// TODO
}
func TestDown(t *testing.T) {
// TODO
}
func TestFindPos(t *testing.T) {
m := Migrations{index: uintSlice{1, 2, 3}}
if p := m.findPos(0); p != -1 {
t.Errorf("expected -1, got %v", p)
}
if p := m.findPos(1); p != 0 {
t.Errorf("expected 0, got %v", p)
}
if p := m.findPos(3); p != 2 {
t.Errorf("expected 2, got %v", p)
}
}
@@ -0,0 +1,40 @@
package source
import (
"fmt"
"regexp"
"strconv"
)
var (
ErrParse = fmt.Errorf("no match")
)
var (
DefaultParse = Parse
DefaultRegex = Regex
)
// Regex matches the following pattern:
//
// 123_name.up.ext
// 123_name.down.ext
var Regex = regexp.MustCompile(`^([0-9]+)_(.*)\.(` + string(Down) + `|` + string(Up) + `)\.(.*)$`)
// Parse returns Migration for matching Regex pattern.
func Parse(raw string) (*Migration, error) {
m := Regex.FindStringSubmatch(raw)
if len(m) == 5 {
versionUint64, err := strconv.ParseUint(m[1], 10, 64)
if err != nil {
return nil, err
}
return &Migration{
Version: uint(versionUint64),
Identifier: m[2],
Direction: Direction(m[3]),
Raw: raw,
}, nil
}
return nil, ErrParse
}
@@ -0,0 +1,106 @@
package source
import (
"testing"
)
func TestParse(t *testing.T) {
tt := []struct {
name string
expectErr error
expectMigration *Migration
}{
{
name: "1_foobar.up.sql",
expectErr: nil,
expectMigration: &Migration{
Version: 1,
Identifier: "foobar",
Direction: Up,
Raw: "1_foobar.up.sql",
},
},
{
name: "1_foobar.down.sql",
expectErr: nil,
expectMigration: &Migration{
Version: 1,
Identifier: "foobar",
Direction: Down,
Raw: "1_foobar.down.sql",
},
},
{
name: "1_f-o_ob+ar.up.sql",
expectErr: nil,
expectMigration: &Migration{
Version: 1,
Identifier: "f-o_ob+ar",
Direction: Up,
Raw: "1_f-o_ob+ar.up.sql",
},
},
{
name: "1485385885_foobar.up.sql",
expectErr: nil,
expectMigration: &Migration{
Version: 1485385885,
Identifier: "foobar",
Direction: Up,
Raw: "1485385885_foobar.up.sql",
},
},
{
name: "20170412214116_date_foobar.up.sql",
expectErr: nil,
expectMigration: &Migration{
Version: 20170412214116,
Identifier: "date_foobar",
Direction: Up,
Raw: "20170412214116_date_foobar.up.sql",
},
},
{
name: "-1_foobar.up.sql",
expectErr: ErrParse,
expectMigration: nil,
},
{
name: "foobar.up.sql",
expectErr: ErrParse,
expectMigration: nil,
},
{
name: "1.up.sql",
expectErr: ErrParse,
expectMigration: nil,
},
{
name: "1_foobar.sql",
expectErr: ErrParse,
expectMigration: nil,
},
{
name: "1_foobar.up",
expectErr: ErrParse,
expectMigration: nil,
},
{
name: "1_foobar.down",
expectErr: ErrParse,
expectMigration: nil,
},
}
for i, v := range tt {
f, err := Parse(v.name)
if err != v.expectErr {
t.Errorf("expected %v, got %v, in %v", v.expectErr, err, i)
}
if v.expectMigration != nil && *f != *v.expectMigration {
t.Errorf("expected %+v, got %+v, in %v", *v.expectMigration, *f, i)
}
}
}
@@ -0,0 +1,29 @@
# pkger
```go
package main
import (
"errors"
"log"
"github.com/golang-migrate/migrate/v4"
"github.com/markbates/pkger"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/pkger"
_ "github.com/lib/pq"
)
func main() {
pkger.Include("/module/path/to/migrations")
m, err := migrate.New("pkger:///module/path/to/migrations", "postgres://postgres@localhost/postgres?sslmode=disable")
if err != nil {
log.Fatalln(err)
}
if err := m.Up(); errors.Is(err, migrate.ErrNoChange) {
log.Println(err)
} else if err != nil {
log.Fatalln(err)
}
}
```

Some files were not shown because too many files have changed in this diff Show More