Skip to content

Commit

Permalink
feat: added custom iofs driver for gorm-migrate (#534)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanmoysrt authored Apr 19, 2024
1 parent fe206a8 commit ca4816c
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 2 deletions.
208 changes: 208 additions & 0 deletions swiftwave_service/db/iofs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package db

/*
* This file is a part of golang-migrate package.
* Copied from https://github.com/golang-migrate/migrate/blob/14f6d3a92ff4c2581e60ca036375d737de047335/source/iofs/iofs.go
* Commit: 14f6d3a92ff4c2581e60ca036375d737de047335
* Upstream code Licensed Under MIT License: https://github.com/golang-migrate/migrate/tree/14f6d3a92ff4c2581e60ca036375d737de047335?tab=License-1-ov-file
* It has been modified to work with arm-based systems without breaking the backward compatibility of swiftwave due to integer overflow issue.
* https://github.com/golang-migrate/migrate/issues/213
* We are converting the datetime format to unix timestamp to avoid integer overflow issue.
*/

import (
"errors"
"fmt"
"github.com/fatih/color"
"github.com/golang-migrate/migrate/v4/source"
"io"
"io/fs"
"path"
"regexp"
"strconv"
"time"
)

type driver struct {
PartialDriver
}

var IOFSFileRegex = regexp.MustCompile(`^([0-9]+)_(.*)\.(` + string(source.Down) + `|` + string(source.Up) + `)\.(.*)$`)
var versionTimeLayout = "20060102150405"

// NewCustomIOFSDriver returns a new Driver from io/fs#FS and a relative path.
func NewCustomIOFSDriver(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 := Parse(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
}

// Parse returns Migration for matching IOFSFileRegex pattern.
func Parse(raw string) (*source.Migration, error) {
m := IOFSFileRegex.FindStringSubmatch(raw)
if len(m) == 5 {
t, err := time.Parse(versionTimeLayout, m[1])
if err != nil {
color.Red("Invalid version format in migration file name")
return nil, err
}
return &source.Migration{
Version: uint(t.Unix()),
Identifier: m[2],
Direction: source.Direction(m[3]),
Raw: raw,
}, nil
}
return nil, source.ErrParse
}
3 changes: 1 addition & 2 deletions swiftwave_service/db/migrate_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/swiftwave-org/swiftwave/swiftwave_service/logger"
"gorm.io/gorm"
)
Expand All @@ -15,7 +14,7 @@ import (
var migrationFilesFS embed.FS

func MigrateDatabase(client *gorm.DB) error {
migrationFSDriver, err := iofs.New(migrationFilesFS, "migrations")
migrationFSDriver, err := NewCustomIOFSDriver(migrationFilesFS, "migrations")
if err != nil {
logger.DatabaseLoggerError.Println("Failed to create migration fs driver")
logger.DatabaseLoggerError.Println(err)
Expand Down

0 comments on commit ca4816c

Please sign in to comment.