optimise migrate api for console on cli (#2895)

This commit is contained in:
Aravind Shankar 2019-09-18 11:06:16 +05:30 committed by Shahidh K Muhammed
parent 0a64ef99b5
commit 5f3294f4a0
19 changed files with 111 additions and 154 deletions

View File

@ -3,7 +3,6 @@ package commands
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"sync" "sync"
"github.com/fatih/color" "github.com/fatih/color"
@ -11,9 +10,9 @@ import (
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hasura/graphql-engine/cli" "github.com/hasura/graphql-engine/cli"
"github.com/hasura/graphql-engine/cli/migrate"
"github.com/hasura/graphql-engine/cli/migrate/api" "github.com/hasura/graphql-engine/cli/migrate/api"
"github.com/hasura/graphql-engine/cli/util" "github.com/hasura/graphql-engine/cli/util"
"github.com/hasura/graphql-engine/cli/version"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
@ -85,14 +84,9 @@ func (o *consoleOptions) run() error {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
// An Engine instance with the Logger and Recovery middleware already attached. // An Engine instance with the Logger and Recovery middleware already attached.
r := gin.New() g := gin.New()
r.Use(allowCors()) g.Use(allowCors())
// My Router struct
router := &cRouter{
r,
}
if o.EC.Version == nil { if o.EC.Version == nil {
return errors.New("cannot validate version, object is nil") return errors.New("cannot validate version, object is nil")
@ -103,7 +97,18 @@ func (o *consoleOptions) run() error {
return err return err
} }
router.setRoutes(o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.MigrationDir, metadataPath, o.EC.Logger, o.EC.Version) t, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, false)
if err != nil {
return err
}
// My Router struct
r := &cRouter{
g,
t,
}
r.setRoutes(o.EC.MigrationDir, metadataPath, o.EC.Logger)
consoleTemplateVersion := o.EC.Version.GetConsoleTemplateVersion() consoleTemplateVersion := o.EC.Version.GetConsoleTemplateVersion()
consoleAssetsVersion := o.EC.Version.GetConsoleAssetsVersion() consoleAssetsVersion := o.EC.Version.GetConsoleAssetsVersion()
@ -134,7 +139,7 @@ func (o *consoleOptions) run() error {
o.WG = wg o.WG = wg
wg.Add(1) wg.Add(1)
go func() { go func() {
err = router.Run(o.Address + ":" + o.APIPort) err = r.router.Run(o.Address + ":" + o.APIPort)
if err != nil { if err != nil {
o.EC.Logger.WithError(err).Errorf("error listening on port %s", o.APIPort) o.EC.Logger.WithError(err).Errorf("error listening on port %s", o.APIPort)
} }
@ -171,15 +176,16 @@ func (o *consoleOptions) run() error {
} }
type cRouter struct { type cRouter struct {
*gin.Engine router *gin.Engine
migrate *migrate.Migrate
} }
func (router *cRouter) setRoutes(nurl *url.URL, adminSecret, migrationDir, metadataFile string, logger *logrus.Logger, v *version.Version) { func (r *cRouter) setRoutes(migrationDir, metadataFile string, logger *logrus.Logger) {
apis := router.Group("/apis") apis := r.router.Group("/apis")
{ {
apis.Use(setLogger(logger)) apis.Use(setLogger(logger))
apis.Use(setFilePath(migrationDir)) apis.Use(setFilePath(migrationDir))
apis.Use(setDataPath(nurl, getAdminSecretHeaderName(v), adminSecret)) apis.Use(setMigrate(r.migrate))
// Migrate api endpoints and middleware // Migrate api endpoints and middleware
migrateAPIs := apis.Group("/migrate") migrateAPIs := apis.Group("/migrate")
{ {
@ -198,11 +204,9 @@ func (router *cRouter) setRoutes(nurl *url.URL, adminSecret, migrationDir, metad
} }
} }
func setDataPath(nurl *url.URL, adminSecretHeader, adminSecret string) gin.HandlerFunc { func setMigrate(t *migrate.Migrate) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
host := getDataPath(nurl, adminSecretHeader, adminSecret) c.Set("migrate", t)
c.Set("dbpath", host)
c.Next() c.Next()
} }
} }

View File

@ -1,6 +1,7 @@
package commands package commands
import ( import (
"os"
"testing" "testing"
"time" "time"
@ -19,7 +20,7 @@ func TestConsoleCmd(t *testing.T) {
ec.Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond) ec.Spinner = spinner.New(spinner.CharSets[7], 100*time.Millisecond)
ec.ServerConfig = &cli.ServerConfig{ ec.ServerConfig = &cli.ServerConfig{
Endpoint: "http://localhost:8080", Endpoint: "http://localhost:8080",
AdminSecret: "", AdminSecret: os.Getenv("HASURA_GRAPHQL_TEST_ADMIN_SECRET"),
} }
ec.MetadataFile = []string{"metadata.yaml"} ec.MetadataFile = []string{"metadata.yaml"}
@ -33,7 +34,6 @@ func TestConsoleCmd(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("prepare failed: %v", err) t.Fatalf("prepare failed: %v", err)
} }
opts := &consoleOptions{ opts := &consoleOptions{
EC: ec, EC: ec,
APIPort: "9693", APIPort: "9693",

View File

@ -57,7 +57,7 @@ type metadataApplyOptions struct {
} }
func (o *metadataApplyOptions) run() error { func (o *metadataApplyOptions) run() error {
migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version) migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -61,7 +61,7 @@ type metadataClearOptions struct {
} }
func (o *metadataClearOptions) run() error { func (o *metadataClearOptions) run() error {
migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version) migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -64,7 +64,7 @@ type metadataExportOptions struct {
} }
func (o *metadataExportOptions) run() error { func (o *metadataExportOptions) run() error {
migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version) migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -57,7 +57,7 @@ type metadataReloadOptions struct {
} }
func (o *metadataReloadOptions) run() error { func (o *metadataReloadOptions) run() error {
migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version) migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -34,10 +34,10 @@ func NewMigrateCmd(ec *cli.ExecutionContext) *cobra.Command {
return migrateCmd return migrateCmd
} }
func newMigrate(dir string, db *url.URL, adminSecretValue string, logger *logrus.Logger, v *version.Version) (*migrate.Migrate, error) { func newMigrate(dir string, db *url.URL, adminSecretValue string, logger *logrus.Logger, v *version.Version, isCmd bool) (*migrate.Migrate, error) {
dbURL := getDataPath(db, getAdminSecretHeaderName(v), adminSecretValue) dbURL := getDataPath(db, getAdminSecretHeaderName(v), adminSecretValue)
fileURL := getFilePath(dir) fileURL := getFilePath(dir)
t, err := migrate.New(fileURL.String(), dbURL.String(), true, logger) t, err := migrate.New(fileURL.String(), dbURL.String(), isCmd, logger)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot create migrate instance") return nil, errors.Wrap(err, "cannot create migrate instance")
} }

View File

@ -71,7 +71,7 @@ func (o *migrateApplyOptions) run() error {
return errors.Wrap(err, "error validating flags") return errors.Wrap(err, "error validating flags")
} }
migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version) migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, true)
if err != nil { if err != nil {
return err return err
} }

View File

@ -109,7 +109,7 @@ func (o *migrateCreateOptions) run() (version int64, err error) {
var migrateDrv *migrate.Migrate var migrateDrv *migrate.Migrate
if o.sqlServer || o.metaDataServer { if o.sqlServer || o.metaDataServer {
migrateDrv, err = newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version) migrateDrv, err = newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, true)
if err != nil { if err != nil {
return 0, errors.Wrap(err, "cannot create migrate instance") return 0, errors.Wrap(err, "cannot create migrate instance")
} }

View File

@ -58,7 +58,7 @@ type migrateStatusOptions struct {
} }
func (o *migrateStatusOptions) run() (*migrate.Status, error) { func (o *migrateStatusOptions) run() (*migrate.Status, error) {
migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version) migrateDrv, err := newMigrate(o.EC.MigrationDir, o.EC.ServerConfig.ParsedEndpoint, o.EC.ServerConfig.AdminSecret, o.EC.Logger, o.EC.Version, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -4,39 +4,22 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hasura/graphql-engine/cli/migrate" "github.com/hasura/graphql-engine/cli/migrate"
"github.com/sirupsen/logrus"
) )
func MetadataAPI(c *gin.Context) { func MetadataAPI(c *gin.Context) {
// Get File url // Get migrate instance
sourcePtr, ok := c.Get("filedir") migratePtr, ok := c.Get("migrate")
if !ok {
return
}
sourceURL := sourcePtr.(*url.URL)
// Get hasuradb url
databasePtr, ok := c.Get("dbpath")
if !ok { if !ok {
return return
} }
// Convert to url.URL // Convert to url.URL
databaseURL := databasePtr.(*url.URL) t := migratePtr.(*migrate.Migrate)
// Get Logger
loggerPtr, ok := c.Get("logger")
if !ok {
return
}
logger := loggerPtr.(*logrus.Logger)
metadataFilePtr, ok := c.Get("metadataFile") metadataFilePtr, ok := c.Get("metadataFile")
if !ok { if !ok {
@ -44,17 +27,6 @@ func MetadataAPI(c *gin.Context) {
} }
metadataFile := metadataFilePtr.(string) metadataFile := metadataFilePtr.(string)
// Create new migrate
t, err := migrate.New(sourceURL.String(), databaseURL.String(), false, logger)
if err != nil {
if strings.HasPrefix(err.Error(), DataAPIError) {
c.JSON(http.StatusInternalServerError, &Response{Code: "data_api_error", Message: err.Error()})
return
}
c.JSON(http.StatusInternalServerError, &Response{Code: "internal_error", Message: err.Error()})
return
}
// Switch on request method // Switch on request method
switch c.Request.Method { switch c.Request.Method {
case "GET": case "GET":

View File

@ -32,14 +32,12 @@ type Request struct {
} }
func MigrateAPI(c *gin.Context) { func MigrateAPI(c *gin.Context) {
// Get File url migratePtr, ok := c.Get("migrate")
sourcePtr, ok := c.Get("filedir")
if !ok { if !ok {
return return
} }
// Get File url
// Get hasuradb url sourcePtr, ok := c.Get("filedir")
databasePtr, ok := c.Get("dbpath")
if !ok { if !ok {
return return
} }
@ -51,21 +49,10 @@ func MigrateAPI(c *gin.Context) {
} }
// Convert to url.URL // Convert to url.URL
databaseURL := databasePtr.(*url.URL) t := migratePtr.(*migrate.Migrate)
sourceURL := sourcePtr.(*url.URL) sourceURL := sourcePtr.(*url.URL)
logger := loggerPtr.(*logrus.Logger) logger := loggerPtr.(*logrus.Logger)
// Create new migrate
t, err := migrate.New(sourceURL.String(), databaseURL.String(), false, logger)
if err != nil {
if strings.HasPrefix(err.Error(), DataAPIError) {
c.JSON(http.StatusInternalServerError, &Response{Code: "data_api_error", Message: err.Error()})
return
}
c.JSON(http.StatusInternalServerError, &Response{Code: "internal_error", Message: err.Error()})
return
}
// Switch on request method // Switch on request method
switch c.Request.Method { switch c.Request.Method {
case "POST": case "POST":
@ -82,7 +69,7 @@ func MigrateAPI(c *gin.Context) {
timestamp := startTime.UnixNano() / int64(time.Millisecond) timestamp := startTime.UnixNano() / int64(time.Millisecond)
createOptions := cmd.New(timestamp, request.Name, sourceURL.Path) createOptions := cmd.New(timestamp, request.Name, sourceURL.Path)
err = createOptions.SetMetaUp(request.Up) err := createOptions.SetMetaUp(request.Up)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, &Response{Code: "create_file_error", Message: err.Error()}) c.JSON(http.StatusInternalServerError, &Response{Code: "create_file_error", Message: err.Error()})
return return
@ -99,25 +86,23 @@ func MigrateAPI(c *gin.Context) {
return return
} }
defer func() {
if err != nil {
err = createOptions.Delete()
if err != nil {
logger.Debug(err)
}
}
}()
// Rescan file system // Rescan file system
err = t.ReScan() err = t.ReScan()
if err != nil { if err != nil {
deleteErr := createOptions.Delete()
if deleteErr != nil {
c.JSON(http.StatusInternalServerError, &Response{Code: "delete_file_error", Message: deleteErr.Error()})
return
}
c.JSON(http.StatusInternalServerError, &Response{Code: "internal_error", Message: err.Error()}) c.JSON(http.StatusInternalServerError, &Response{Code: "internal_error", Message: err.Error()})
return return
} }
if err = t.Migrate(uint64(timestamp), "up"); err != nil { if err = t.Migrate(uint64(timestamp), "up"); err != nil {
deleteErr := createOptions.Delete()
if deleteErr != nil {
c.JSON(http.StatusInternalServerError, &Response{Code: "delete_file_error", Message: deleteErr.Error()})
return
}
if strings.HasPrefix(err.Error(), DataAPIError) { if strings.HasPrefix(err.Error(), DataAPIError) {
c.JSON(http.StatusBadRequest, &Response{Code: "data_api_error", Message: strings.TrimPrefix(err.Error(), DataAPIError)}) c.JSON(http.StatusBadRequest, &Response{Code: "data_api_error", Message: strings.TrimPrefix(err.Error(), DataAPIError)})
return return

View File

@ -2,12 +2,10 @@ package api
import ( import (
"net/http" "net/http"
"net/url"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/hasura/graphql-engine/cli/migrate" "github.com/hasura/graphql-engine/cli/migrate"
"github.com/sirupsen/logrus"
) )
type SettingReqeust struct { type SettingReqeust struct {
@ -16,40 +14,13 @@ type SettingReqeust struct {
} }
func SettingsAPI(c *gin.Context) { func SettingsAPI(c *gin.Context) {
// Get File url // Get migrate instance
sourcePtr, ok := c.Get("filedir") migratePtr, ok := c.Get("migrate")
if !ok { if !ok {
return return
} }
sourceURL := sourcePtr.(*url.URL) t := migratePtr.(*migrate.Migrate)
// Get hasuradb url
databasePtr, ok := c.Get("dbpath")
if !ok {
return
}
// Convert to url.URL
databaseURL := databasePtr.(*url.URL)
// Get Logger
loggerPtr, ok := c.Get("logger")
if !ok {
return
}
logger := loggerPtr.(*logrus.Logger)
// Create new migrate
t, err := migrate.New(sourceURL.String(), databaseURL.String(), false, logger)
if err != nil {
if strings.HasPrefix(err.Error(), DataAPIError) {
c.JSON(500, &Response{Code: "data_api_error", Message: err.Error()})
return
}
c.JSON(500, &Response{Code: "internal_error", Message: err.Error()})
return
}
// Switch on request method // Switch on request method
switch c.Request.Method { switch c.Request.Method {

View File

@ -49,6 +49,8 @@ type Driver interface {
// Migrate will call this function only once per instance. // Migrate will call this function only once per instance.
Close() error Close() error
Scan() error
// Lock should acquire a database lock so that only one migration process // Lock should acquire a database lock so that only one migration process
// can run at a time. Migrate will call this function before Run is called. // can run at a time. Migrate will call this function before Run is called.
// If the implementation can't provide this functionality, return nil. // If the implementation can't provide this functionality, return nil.

View File

@ -78,7 +78,7 @@ func WithInstance(config *Config, logger *log.Logger) (database.Driver, error) {
return nil, err return nil, err
} }
err := hx.getVersions() err := hx.Scan()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -145,6 +145,11 @@ func (h *HasuraDB) Close() error {
return nil return nil
} }
func (h *HasuraDB) Scan() error {
h.migrations = database.NewMigrations()
return h.getVersions()
}
func (h *HasuraDB) Lock() error { func (h *HasuraDB) Lock() error {
if h.isLocked { if h.isLocked {
return database.ErrLocked return database.ErrLocked
@ -164,6 +169,10 @@ func (h *HasuraDB) UnLock() error {
return nil return nil
} }
defer func() {
h.isLocked = false
}()
if len(h.migrationQuery.Args) == 0 { if len(h.migrationQuery.Args) == 0 {
return nil return nil
} }
@ -213,7 +222,6 @@ func (h *HasuraDB) UnLock() error {
} }
return horror.Error(h.config.isCMD) return horror.Error(h.config.isCMD)
} }
h.isLocked = false
return nil return nil
} }

View File

@ -119,6 +119,7 @@ func New(sourceUrl string, databaseUrl string, cmd bool, logger *log.Logger) (*M
if logger == nil { if logger == nil {
logger = log.New() logger = log.New()
} }
m.Logger = logger
sourceDrv, err := source.Open(sourceUrl, logger) sourceDrv, err := source.Open(sourceUrl, logger)
if err != nil { if err != nil {
@ -150,19 +151,17 @@ func newCommon(cmd bool) *Migrate {
} }
func (m *Migrate) ReScan() error { func (m *Migrate) ReScan() error {
sourceDrv, err := source.Open(m.sourceURL, m.Logger) err := m.sourceDrv.Scan()
if err != nil { if err != nil {
m.Logger.Debug(err) m.Logger.Debug(err)
return err return err
} }
m.sourceDrv = sourceDrv
databaseDrv, err := database.Open(m.databaseURL, m.isCMD, m.Logger) err = m.databaseDrv.Scan()
if err != nil { if err != nil {
m.Logger.Debug(err) m.Logger.Debug(err)
return err return err
} }
m.databaseDrv = databaseDrv
err = m.calculateStatus() err = m.calculateStatus()
if err != nil { if err != nil {
@ -1096,11 +1095,13 @@ func (m *Migrate) unlock() error {
m.isLockedMu.Lock() m.isLockedMu.Lock()
defer m.isLockedMu.Unlock() defer m.isLockedMu.Unlock()
defer func() {
m.isLocked = false
}()
if err := m.databaseDrv.UnLock(); err != nil { if err := m.databaseDrv.UnLock(); err != nil {
return err return err
} }
m.isLocked = false
return nil return nil
} }

View File

@ -40,6 +40,8 @@ type Driver interface {
// Migrate will call this function only once per instance. // Migrate will call this function only once per instance.
Close() error Close() error
Scan() error
// First returns the very first migration version available to the driver. // First returns the very first migration version available to the driver.
// Migrate will call this function multiple times. // Migrate will call this function multiple times.
// If there is no version available, it must return os.ErrNotExist. // If there is no version available, it must return os.ErrNotExist.

View File

@ -59,12 +59,6 @@ func (f *File) Open(url string, logger *log.Logger) (source.Driver, error) {
p = strings.TrimPrefix(p, "/") p = strings.TrimPrefix(p, "/")
} }
// scan directory
files, err := ioutil.ReadDir(p)
if err != nil {
return nil, err
}
nf := &File{ nf := &File{
url: url, url: url,
logger: logger, logger: logger,
@ -72,24 +66,9 @@ func (f *File) Open(url string, logger *log.Logger) (source.Driver, error) {
migrations: source.NewMigrations(), migrations: source.NewMigrations(),
} }
for _, fi := range files { err = nf.Scan()
if !fi.IsDir() { if err != nil {
m, err := source.DefaultParse(fi.Name(), p) return nil, err
if err != nil {
continue // ignore files that we can't parse
}
ok, err := source.IsEmptyFile(m, p)
if err != nil {
return nil, err
}
if !ok {
continue
}
err = nf.migrations.Append(m)
if err != nil {
return nil, err
}
}
} }
return nf, nil return nf, nil
} }
@ -99,6 +78,35 @@ func (f *File) Close() error {
return nil return nil
} }
func (f *File) Scan() error {
f.migrations = source.NewMigrations()
files, err := ioutil.ReadDir(f.path)
if err != nil {
return err
}
for _, fi := range files {
if !fi.IsDir() {
m, err := source.DefaultParse(fi.Name(), f.path)
if err != nil {
continue // ignore files that we can't parse
}
ok, err := source.IsEmptyFile(m, f.path)
if err != nil {
return err
}
if !ok {
continue
}
err = f.migrations.Append(m)
if err != nil {
return err
}
}
}
return nil
}
func (f *File) First() (version uint64, err error) { func (f *File) First() (version uint64, err error) {
if v, ok := f.migrations.First(); !ok { if v, ok := f.migrations.First(); !ok {
return 0, &os.PathError{Op: "first", Path: f.path, Err: os.ErrNotExist} return 0, &os.PathError{Op: "first", Path: f.path, Err: os.ErrNotExist}

View File

@ -41,6 +41,10 @@ func (s *Stub) Close() error {
return nil return nil
} }
func (s *Stub) Scan() error {
return nil
}
func (s *Stub) First() (version uint64, err error) { func (s *Stub) First() (version uint64, err error) {
if v, ok := s.Migrations.First(); !ok { if v, ok := s.Migrations.First(); !ok {
return 0, &os.PathError{Op: "first", Path: s.Url, Err: os.ErrNotExist} // TODO: s.Url can be empty when called with WithInstance return 0, &os.PathError{Op: "first", Path: s.Url, Err: os.ErrNotExist} // TODO: s.Url can be empty when called with WithInstance