Skip to content

Commit

Permalink
feat: [#280] Refactor database migrate - optimize make:migration comm…
Browse files Browse the repository at this point in the history
…and (#606)

* feat: [#280] Refactor database migrate - optimize make:migration command

* chore: update mocks

* add Connection to Migration

* chore: update mocks

* fix unit test

---------

Co-authored-by: hwbrzzl <hwbrzzl@users.noreply.github.com>
  • Loading branch information
hwbrzzl and hwbrzzl authored Aug 21, 2024
1 parent 93a3ea5 commit 99d173a
Show file tree
Hide file tree
Showing 24 changed files with 1,348 additions and 44 deletions.
10 changes: 10 additions & 0 deletions contracts/database/migration/migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package migration

const (
DriverDefault = "default"
DriverSql = "sql"
)

type Driver interface {
Create(name string) error
}
4 changes: 4 additions & 0 deletions contracts/database/schema/blueprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package schema

type Blueprint interface {
}
27 changes: 27 additions & 0 deletions contracts/database/schema/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package schema

type Schema interface {
// Create a new table on the schema.
//Create(table string, callback func(table Blueprint))
// Connection Get the connection for the schema.
Connection() Schema
// DropIfExists Drop a table from the schema if exists.
//DropIfExists(table string)
// Register migrations.
Register([]Migration)
// Sql Execute a sql directly.
Sql(callback func(table Blueprint))
// Table Modify a table on the schema.
//Table(table string, callback func(table Blueprint))
}

type Migration interface {
// Signature Get the migration signature.
Signature() string
// Connection Get the connection for the migration.
Connection() string
// Up Run the migrations.
Up()
// Down Reverse the migrations.
Down()
}
3 changes: 3 additions & 0 deletions contracts/foundation/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/crypt"
"github.com/goravel/framework/contracts/database/orm"
"github.com/goravel/framework/contracts/database/schema"
"github.com/goravel/framework/contracts/database/seeder"
"github.com/goravel/framework/contracts/event"
"github.com/goravel/framework/contracts/filesystem"
Expand Down Expand Up @@ -70,6 +71,8 @@ type Container interface {
MakeRoute() route.Route
// MakeSchedule resolves the schedule instance.
MakeSchedule() schedule.Schedule
// MakeSchema resolves the schema instance.
MakeSchema() schema.Schema
// MakeSession resolves the session instance.
MakeSession() session.Manager
// MakeStorage resolves the storage instance.
Expand Down
25 changes: 13 additions & 12 deletions database/console/migrate_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/goravel/framework/contracts/config"
"github.com/goravel/framework/contracts/database/orm"
"github.com/goravel/framework/database/migration"
"github.com/goravel/framework/support/carbon"
"github.com/goravel/framework/support/file"
)
Expand All @@ -22,7 +23,7 @@ func NewMigrateCreator(config config.Config) *MigrateCreator {
}

// Create a new migration
func (receiver MigrateCreator) Create(name string, table string, create bool) error {
func (receiver *MigrateCreator) Create(name string, table string, create bool) error {
// First we will get the stub file for the migration, which serves as a type
// of template for the migration. Once we have those we will populate the
// various place-holders, save the file, and run the post create event.
Expand All @@ -42,7 +43,7 @@ func (receiver MigrateCreator) Create(name string, table string, create bool) er
}

// getStub Get the migration stub file.
func (receiver MigrateCreator) getStub(table string, create bool) (string, string) {
func (receiver *MigrateCreator) getStub(table string, create bool) (string, string) {
if table == "" {
return "", ""
}
Expand All @@ -51,33 +52,33 @@ func (receiver MigrateCreator) getStub(table string, create bool) (string, strin
switch orm.Driver(driver) {
case orm.DriverPostgresql:
if create {
return PostgresqlStubs{}.CreateUp(), PostgresqlStubs{}.CreateDown()
return migration.PostgresqlStubs{}.CreateUp(), migration.PostgresqlStubs{}.CreateDown()
}

return PostgresqlStubs{}.UpdateUp(), PostgresqlStubs{}.UpdateDown()
return migration.PostgresqlStubs{}.UpdateUp(), migration.PostgresqlStubs{}.UpdateDown()
case orm.DriverSqlite:
if create {
return SqliteStubs{}.CreateUp(), SqliteStubs{}.CreateDown()
return migration.SqliteStubs{}.CreateUp(), migration.SqliteStubs{}.CreateDown()
}

return SqliteStubs{}.UpdateUp(), SqliteStubs{}.UpdateDown()
return migration.SqliteStubs{}.UpdateUp(), migration.SqliteStubs{}.UpdateDown()
case orm.DriverSqlserver:
if create {
return SqlserverStubs{}.CreateUp(), SqlserverStubs{}.CreateDown()
return migration.SqlserverStubs{}.CreateUp(), migration.SqlserverStubs{}.CreateDown()
}

return SqlserverStubs{}.UpdateUp(), SqlserverStubs{}.UpdateDown()
return migration.SqlserverStubs{}.UpdateUp(), migration.SqlserverStubs{}.UpdateDown()
default:
if create {
return MysqlStubs{}.CreateUp(), MysqlStubs{}.CreateDown()
return migration.MysqlStubs{}.CreateUp(), migration.MysqlStubs{}.CreateDown()
}

return MysqlStubs{}.UpdateUp(), MysqlStubs{}.UpdateDown()
return migration.MysqlStubs{}.UpdateUp(), migration.MysqlStubs{}.UpdateDown()
}
}

// populateStub Populate the place-holders in the migration stub.
func (receiver MigrateCreator) populateStub(stub string, table string) string {
func (receiver *MigrateCreator) populateStub(stub string, table string) string {
stub = strings.ReplaceAll(stub, "DummyDatabaseCharset", receiver.config.GetString("database.connections."+receiver.config.GetString("database.default")+".charset"))

if table != "" {
Expand All @@ -88,7 +89,7 @@ func (receiver MigrateCreator) populateStub(stub string, table string) string {
}

// getPath Get the full path to the migration.
func (receiver MigrateCreator) getPath(name string, category string) string {
func (receiver *MigrateCreator) getPath(name string, category string) string {
pwd, _ := os.Getwd()

return fmt.Sprintf("%s/database/migrations/%s_%s.%s.sql", pwd, carbon.Now().ToShortDateTimeString(), name, category)
Expand Down
21 changes: 15 additions & 6 deletions database/console/migrate_make_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package console

import (
"errors"
"fmt"

"github.com/goravel/framework/contracts/config"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
contractsmigration "github.com/goravel/framework/contracts/database/migration"
"github.com/goravel/framework/database/migration"
"github.com/goravel/framework/support/color"
)

Expand Down Expand Up @@ -56,14 +59,20 @@ func (receiver *MigrateMakeCommand) Handle(ctx console.Context) error {
}
}

// We will attempt to guess the table name if this the migration has
// "create" in the name. This will allow us to provide a convenient way
// of creating migrations that create new tables for the application.
table, create := TableGuesser{}.Guess(name)
var migrationDriver contractsmigration.Driver
driver := receiver.config.GetString("database.migration.driver")

switch driver {
case contractsmigration.DriverDefault:
migrationDriver = migration.NewDefaultDriver()
case contractsmigration.DriverSql:
migrationDriver = migration.NewSqlDriver(receiver.config)
default:
return fmt.Errorf("unsupported migration driver: %s", driver)
}

// Write the migration file to disk.
migrateCreator := NewMigrateCreator(receiver.config)
if err := migrateCreator.Create(name, table, create); err != nil {
if err := migrationDriver.Create(name); err != nil {
return err
}

Expand Down
100 changes: 77 additions & 23 deletions database/console/migrate_make_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,83 @@ import (
)

func TestMigrateMakeCommand(t *testing.T) {
var (
mockConfig *configmock.Config
mockContext *consolemocks.Context
)

now := carbon.Now()
up := fmt.Sprintf("database/migrations/%s_%s.%s.sql", now.ToShortDateTimeString(), "create_users_table", "up")
down := fmt.Sprintf("database/migrations/%s_%s.%s.sql", now.ToShortDateTimeString(), "create_users_table", "down")

mockConfig := &configmock.Config{}
mockConfig.On("GetString", "database.default").Return("mysql").Times(3)
mockConfig.On("GetString", "database.connections.mysql.driver").Return("mysql").Once()
mockConfig.On("GetString", "database.connections.mysql.charset").Return("utf8mb4").Twice()

migrateMakeCommand := NewMigrateMakeCommand(mockConfig)
mockContext := &consolemocks.Context{}
mockContext.On("Argument", 0).Return("").Once()
mockContext.On("Ask", "Enter the migration name", mock.Anything).Return("", errors.New("the migration name cannot be empty")).Once()
err := migrateMakeCommand.Handle(mockContext)
assert.EqualError(t, err, "the migration name cannot be empty")
assert.False(t, file.Exists(up))
assert.False(t, file.Exists(down))

mockContext.On("Argument", 0).Return("create_users_table").Once()
assert.Nil(t, migrateMakeCommand.Handle(mockContext))
assert.True(t, file.Exists(up))
assert.True(t, file.Exists(down))
assert.Nil(t, file.Remove("database"))
carbon.SetTestNow(now)

beforeEach := func() {
mockConfig = &configmock.Config{}
mockContext = &consolemocks.Context{}
}

afterEach := func() {
mockConfig.AssertExpectations(t)
mockContext.AssertExpectations(t)
}

tests := []struct {
name string
setup func()
assert func()
expectErr error
}{
{
name: "the migration name is empty",
setup: func() {
mockContext.On("Argument", 0).Return("").Once()
mockContext.On("Ask", "Enter the migration name", mock.Anything).Return("", errors.New("the migration name cannot be empty")).Once()
},
assert: func() {},
expectErr: errors.New("the migration name cannot be empty"),
},
{
name: "default driver",
setup: func() {
mockConfig.On("GetString", "database.migration.driver").Return("default").Once()
mockContext.On("Argument", 0).Return("create_users_table").Once()
},
assert: func() {
migration := fmt.Sprintf("database/migrations/%s_%s.go", now.ToShortDateTimeString(), "create_users_table")

assert.True(t, file.Exists(migration))
},
},
{
name: "sql driver",
setup: func() {
mockConfig.On("GetString", "database.migration.driver").Return("sql").Once()
mockConfig.On("GetString", "database.default").Return("mysql").Times(3)
mockConfig.On("GetString", "database.connections.mysql.driver").Return("mysql").Once()
mockConfig.On("GetString", "database.connections.mysql.charset").Return("utf8mb4").Twice()
mockContext.On("Argument", 0).Return("create_users_table").Once()
},
assert: func() {
up := fmt.Sprintf("database/migrations/%s_%s.%s.sql", now.ToShortDateTimeString(), "create_users_table", "up")
down := fmt.Sprintf("database/migrations/%s_%s.%s.sql", now.ToShortDateTimeString(), "create_users_table", "down")

mockConfig.AssertExpectations(t)
assert.True(t, file.Exists(up))
assert.True(t, file.Exists(down))
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
beforeEach()
test.setup()

migrateMakeCommand := NewMigrateMakeCommand(mockConfig)
err := migrateMakeCommand.Handle(mockContext)
assert.Equal(t, test.expectErr, err)

test.assert()
afterEach()
})
}

assert.Nil(t, file.Remove("database"))
}
73 changes: 73 additions & 0 deletions database/migration/default_driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package migration

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/goravel/framework/support/carbon"
"github.com/goravel/framework/support/file"
"github.com/goravel/framework/support/str"
)

type DefaultDriver struct {
}

func NewDefaultDriver() *DefaultDriver {
return &DefaultDriver{}
}

func (r *DefaultDriver) Create(name string) error {
// We will attempt to guess the table name if this the migration has
// "create" in the name. This will allow us to provide a convenient way
// of creating migrations that create new tables for the application.
table, create := TableGuesser{}.Guess(name)

// First we will get the stub file for the migration, which serves as a type
// of template for the migration. Once we have those we will populate the
// various place-holders, save the file, and run the post create event.
stub := r.getStub(table, create)

// Prepend timestamp to the file name.
fileName := r.getFileName(name)

// Create the up.sql file.
if err := file.Create(r.getPath(fileName), r.populateStub(stub, fileName)); err != nil {
return err
}

return nil
}

// getStub Get the migration stub file.
func (r *DefaultDriver) getStub(table string, create bool) string {
if table == "" {
return Stubs{}.Empty()
}

if create {
return Stubs{}.Create()
}

return Stubs{}.Update()
}

// populateStub Populate the place-holders in the migration stub.
func (r *DefaultDriver) populateStub(stub, fileName string) string {
stub = strings.ReplaceAll(stub, "DummyMigration", str.Of(fileName).Prepend("m_").Studly().String())
stub = strings.ReplaceAll(stub, "DummyName", fileName)

return stub
}

// getPath Get the full path to the migration.
func (r *DefaultDriver) getPath(name string) string {
pwd, _ := os.Getwd()

return filepath.Join(pwd, "database", "migrations", name+".go")
}

func (r *DefaultDriver) getFileName(name string) string {
return fmt.Sprintf("%s_%s", carbon.Now().ToShortDateTimeString(), name)
}
Loading

0 comments on commit 99d173a

Please sign in to comment.