diff --git a/contracts/database/migration/driver.go b/contracts/database/migration/driver.go index 48d4382f9..798061b90 100644 --- a/contracts/database/migration/driver.go +++ b/contracts/database/migration/driver.go @@ -9,5 +9,5 @@ type Driver interface { // Create a new migration file. Create(name string) error // Run the migrations according to paths. - Run(paths []string) error + Run() error } diff --git a/contracts/database/migration/repository.go b/contracts/database/migration/repository.go index 4c638539f..4af23d62a 100644 --- a/contracts/database/migration/repository.go +++ b/contracts/database/migration/repository.go @@ -8,11 +8,11 @@ type File struct { type Repository interface { // CreateRepository Create the migration repository data store. - CreateRepository() error + CreateRepository() // Delete Remove a migration from the log. Delete(migration string) error // DeleteRepository Delete the migration repository data store. - DeleteRepository() error + DeleteRepository() // GetLast Get the last migration batch. GetLast() ([]File, error) // GetMigrations Get the list of migrations. diff --git a/contracts/database/migration/schema.go b/contracts/database/migration/schema.go index bd606b3c6..224198301 100644 --- a/contracts/database/migration/schema.go +++ b/contracts/database/migration/schema.go @@ -1,22 +1,34 @@ package migration +import ( + "github.com/goravel/framework/contracts/database/orm" +) + type Schema interface { - // Create a new table on the schema. - Create(table string, callback func(table Blueprint)) error // Connection Get the connection for the schema. Connection(name string) Schema + // Create a new table on the schema. + Create(table string, callback func(table Blueprint)) // DropIfExists Drop a table from the schema if exists. - DropIfExists(table string) error + DropIfExists(table string) + // GetConnection Get the connection of the schema. + GetConnection() string // GetTables Get the tables that belong to the database. GetTables() ([]Table, error) // HasTable Determine if the given table exists. HasTable(table string) bool + // Migrations Get the migrations. + Migrations() []Migration + // Orm Get the orm instance. + Orm() orm.Orm // Register migrations. Register([]Migration) + // SetConnection Set the connection of the schema. + SetConnection(name string) // Sql Execute a sql directly. Sql(sql string) // Table Modify a table on the schema. - Table(table string, callback func(table Blueprint)) error + Table(table string, callback func(table Blueprint)) } type Migration interface { diff --git a/contracts/database/orm/orm.go b/contracts/database/orm/orm.go index 29b53da8e..67dcbd737 100644 --- a/contracts/database/orm/orm.go +++ b/contracts/database/orm/orm.go @@ -12,12 +12,14 @@ type Orm interface { Connection(name string) Orm // DB gets the underlying database connection. DB() (*sql.DB, error) - // Query gets a new query builder instance. - Query() Query // Factory gets a new factory instance for the given model name. Factory() Factory + // Name gets the current connection name. + Name() string // Observe registers an observer with the Orm. Observe(model any, observer Observer) + // Query gets a new query builder instance. + Query() Query // Refresh resets the Orm instance. Refresh() // Transaction runs a callback wrapped in a database transaction. diff --git a/database/console/driver/sqlite.go b/database/console/driver/sqlite.go index b38dd3bba..e09339181 100644 --- a/database/console/driver/sqlite.go +++ b/database/console/driver/sqlite.go @@ -1,4 +1,4 @@ -package sqlite +package driver import ( "database/sql" @@ -19,11 +19,9 @@ func init() { database.Register("sqlite", &Sqlite{}) } -var DefaultMigrationsTable = "schema_migrations" var ( - ErrDatabaseDirty = fmt.Errorf("database is dirty") - ErrNilConfig = fmt.Errorf("no config") - ErrNoDatabaseName = fmt.Errorf("no database name") + DefaultMigrationsTable = "schema_migrations" + ErrNilConfig = fmt.Errorf("no config") ) type Config struct { diff --git a/database/console/migration/migrate.go b/database/console/migration/migrate.go index a2eaad2ec..3ffd0f2bd 100644 --- a/database/console/migration/migrate.go +++ b/database/console/migration/migrate.go @@ -2,7 +2,6 @@ package migration import ( "database/sql" - "errors" "fmt" "github.com/golang-migrate/migrate/v4" @@ -14,12 +13,13 @@ import ( "github.com/goravel/framework/contracts/database" "github.com/goravel/framework/database/console/driver" databasedb "github.com/goravel/framework/database/db" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support" ) func getMigrate(config config.Config) (*migrate.Migrate, error) { connection := config.GetString("database.default") - driver := config.GetString("database.connections." + connection + ".driver") + dbDriver := database.Driver(config.GetString("database.connections." + connection + ".driver")) dir := "file://./database/migrations" if support.RelativePath != "" { dir = fmt.Sprintf("file://%s/database/migrations", support.RelativePath) @@ -28,10 +28,10 @@ func getMigrate(config config.Config) (*migrate.Migrate, error) { configBuilder := databasedb.NewConfigBuilder(config, connection) writeConfigs := configBuilder.Writes() if len(writeConfigs) == 0 { - return nil, errors.New("not found database configuration") + return nil, errors.OrmDatabaseConfigNotFound } - switch database.Driver(driver) { + switch dbDriver { case database.DriverMysql: mysqlDsn := databasedb.Dsn(writeConfigs[0]) if mysqlDsn == "" { @@ -81,7 +81,7 @@ func getMigrate(config config.Config) (*migrate.Migrate, error) { return nil, err } - instance, err := sqlite.WithInstance(db, &sqlite.Config{ + instance, err := driver.WithInstance(db, &driver.Config{ MigrationsTable: config.GetString("database.migrations.table"), }) if err != nil { @@ -110,6 +110,6 @@ func getMigrate(config config.Config) (*migrate.Migrate, error) { return migrate.NewWithDatabaseInstance(dir, "sqlserver", instance) default: - return nil, errors.New("database driver only support mysql, postgres, sqlite and sqlserver") + return nil, errors.OrmDriverNotSupported } } diff --git a/database/console/migration/migrate_command.go b/database/console/migration/migrate_command.go index 6cfd5267e..9a5d184da 100644 --- a/database/console/migration/migrate_command.go +++ b/database/console/migration/migrate_command.go @@ -1,58 +1,50 @@ package migration import ( - "errors" - - "github.com/golang-migrate/migrate/v4" - "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/support/color" ) type MigrateCommand struct { - config config.Config + driver migration.Driver } -func NewMigrateCommand(config config.Config) *MigrateCommand { +func NewMigrateCommand(config config.Config, schema migration.Schema) *MigrateCommand { + driver, err := GetDriver(config, schema) + if err != nil { + color.Red().Println(err.Error()) + return nil + } + return &MigrateCommand{ - config: config, + driver: driver, } } // Signature The name and signature of the console command. -func (receiver *MigrateCommand) Signature() string { +func (r *MigrateCommand) Signature() string { return "migrate" } // Description The console command description. -func (receiver *MigrateCommand) Description() string { +func (r *MigrateCommand) Description() string { return "Run the database migrations" } // Extend The console command extend. -func (receiver *MigrateCommand) Extend() command.Extend { +func (r *MigrateCommand) Extend() command.Extend { return command.Extend{ Category: "migrate", } } // Handle Execute the console command. -func (receiver *MigrateCommand) Handle(ctx console.Context) error { - m, err := getMigrate(receiver.config) - if err != nil { - return err - } - if m == nil { - color.Yellow().Println("Please fill database config first") - - return nil - } - - if err = m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { +func (r *MigrateCommand) Handle(ctx console.Context) error { + if err := r.driver.Run(); err != nil { color.Red().Println("Migration failed:", err.Error()) - return nil } diff --git a/database/console/migration/migrate_command_test.go b/database/console/migration/migrate_command_test.go deleted file mode 100644 index d3fb8bb2d..000000000 --- a/database/console/migration/migrate_command_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package migration - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/goravel/framework/database/gorm" - "github.com/goravel/framework/database/orm" - mocksconsole "github.com/goravel/framework/mocks/console" - "github.com/goravel/framework/support/env" - "github.com/goravel/framework/support/file" -) - -type Agent struct { - orm.Model - Name string -} - -func TestMigrateCommand(t *testing.T) { - if env.IsWindows() { - t.Skip("Skipping tests of using docker") - } - - testQueries := gorm.NewTestQueries().Queries() - for driver, testQuery := range testQueries { - query := testQuery.Query() - mockConfig := testQuery.MockConfig() - createMigrations(driver) - - migrateCommand := NewMigrateCommand(mockConfig) - mockContext := &mocksconsole.Context{} - assert.Nil(t, migrateCommand.Handle(mockContext)) - - var agent Agent - assert.Nil(t, query.Where("name", "goravel").First(&agent)) - assert.True(t, agent.ID > 0) - } - - defer assert.Nil(t, file.Remove("database")) -} diff --git a/database/console/migration/migrate_fresh_command.go b/database/console/migration/migrate_fresh_command.go index df73cec78..b88957ecd 100644 --- a/database/console/migration/migrate_fresh_command.go +++ b/database/console/migration/migrate_fresh_command.go @@ -1,7 +1,6 @@ package migration import ( - "errors" "strings" "github.com/golang-migrate/migrate/v4" @@ -9,6 +8,7 @@ import ( "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support/color" ) diff --git a/database/console/migration/migrate_fresh_command_test.go b/database/console/migration/migrate_fresh_command_test.go index 62f6d9f1e..fba79cba3 100644 --- a/database/console/migration/migrate_fresh_command_test.go +++ b/database/console/migration/migrate_fresh_command_test.go @@ -1,12 +1,17 @@ package migration import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + contractsmigration "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/migration" mocksconsole "github.com/goravel/framework/mocks/console" + mocksmigration "github.com/goravel/framework/mocks/database/migration" "github.com/goravel/framework/support/env" "github.com/goravel/framework/support/file" ) @@ -20,45 +25,59 @@ func TestMigrateFreshCommand(t *testing.T) { for driver, testQuery := range testQueries { query := testQuery.Query() mockConfig := testQuery.MockConfig() - createMigrations(driver) + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql).Once() + mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.charset", testQuery.Docker().Driver().String())).Return("utf8bm4").Once() + + mockSchema := mocksmigration.NewSchema(t) + migration.CreateTestMigrations(driver) mockContext := mocksconsole.NewContext(t) mockArtisan := mocksconsole.NewArtisan(t) - migrateCommand := NewMigrateCommand(mockConfig) + + migrateCommand := NewMigrateCommand(mockConfig, mockSchema) + require.NotNil(t, migrateCommand) assert.Nil(t, migrateCommand.Handle(mockContext)) - mockContext.On("OptionBool", "seed").Return(false).Once() + + mockContext.EXPECT().OptionBool("seed").Return(false).Once() + migrateFreshCommand := NewMigrateFreshCommand(mockConfig, mockArtisan) assert.Nil(t, migrateFreshCommand.Handle(mockContext)) - var agent Agent + var agent migration.Agent err := query.Where("name", "goravel").First(&agent) assert.Nil(t, err) assert.True(t, agent.ID > 0) // Test MigrateFreshCommand with --seed flag and seeders specified - mockContext = &mocksconsole.Context{} - mockArtisan = &mocksconsole.Artisan{} - mockContext.On("OptionBool", "seed").Return(true).Once() - mockContext.On("OptionSlice", "seeder").Return([]string{"MockSeeder"}).Once() - mockArtisan.On("Call", "db:seed --seeder MockSeeder").Return(nil).Once() + mockContext = mocksconsole.NewContext(t) + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql).Once() + + mockArtisan = mocksconsole.NewArtisan(t) + mockContext.EXPECT().OptionBool("seed").Return(true).Once() + mockContext.EXPECT().OptionSlice("seeder").Return([]string{"MockSeeder"}).Once() + mockArtisan.EXPECT().Call("db:seed --seeder MockSeeder").Once() + migrateFreshCommand = NewMigrateFreshCommand(mockConfig, mockArtisan) assert.Nil(t, migrateFreshCommand.Handle(mockContext)) - var agent1 Agent + var agent1 migration.Agent err = query.Where("name", "goravel").First(&agent1) assert.Nil(t, err) assert.True(t, agent1.ID > 0) // Test MigrateFreshCommand with --seed flag and no seeders specified - mockContext = &mocksconsole.Context{} - mockArtisan = &mocksconsole.Artisan{} - mockContext.On("OptionBool", "seed").Return(true).Once() - mockContext.On("OptionSlice", "seeder").Return([]string{}).Once() - mockArtisan.On("Call", "db:seed").Return(nil).Once() + mockContext = mocksconsole.NewContext(t) + mockArtisan = mocksconsole.NewArtisan(t) + mockContext.EXPECT().OptionBool("seed").Return(true).Once() + mockContext.EXPECT().OptionSlice("seeder").Return([]string{}).Once() + mockArtisan.EXPECT().Call("db:seed").Once() + migrateFreshCommand = NewMigrateFreshCommand(mockConfig, mockArtisan) assert.Nil(t, migrateFreshCommand.Handle(mockContext)) - var agent2 Agent + var agent2 migration.Agent err = query.Where("name", "goravel").First(&agent2) assert.Nil(t, err) assert.True(t, agent2.ID > 0) diff --git a/database/console/migration/migrate_make_command.go b/database/console/migration/migrate_make_command.go index de7061741..f9e765fe6 100644 --- a/database/console/migration/migrate_make_command.go +++ b/database/console/migration/migrate_make_command.go @@ -1,20 +1,21 @@ package migration import ( - "errors" - "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/contracts/database/migration" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support/color" ) type MigrateMakeCommand struct { config config.Config + schema migration.Schema } -func NewMigrateMakeCommand(config config.Config) *MigrateMakeCommand { - return &MigrateMakeCommand{config: config} +func NewMigrateMakeCommand(config config.Config, schema migration.Schema) *MigrateMakeCommand { + return &MigrateMakeCommand{config: config, schema: schema} } // Signature The name and signature of the console command. @@ -45,7 +46,7 @@ func (r *MigrateMakeCommand) Handle(ctx console.Context) error { name, err = ctx.Ask("Enter the migration name", console.AskOption{ Validate: func(s string) error { if s == "" { - return errors.New("the migration name cannot be empty") + return errors.MigrationNameIsRequired } return nil @@ -56,7 +57,7 @@ func (r *MigrateMakeCommand) Handle(ctx console.Context) error { } } - migrationDriver, err := GetDriver(r.config) + migrationDriver, err := GetDriver(r.config, r.schema) if err != nil { return err } diff --git a/database/console/migration/migrate_make_command_test.go b/database/console/migration/migrate_make_command_test.go index fb22a4823..9624b5b03 100644 --- a/database/console/migration/migrate_make_command_test.go +++ b/database/console/migration/migrate_make_command_test.go @@ -11,6 +11,7 @@ import ( contractsmigration "github.com/goravel/framework/contracts/database/migration" mocksconfig "github.com/goravel/framework/mocks/config" mocksconsole "github.com/goravel/framework/mocks/console" + mocksmigration "github.com/goravel/framework/mocks/database/migration" "github.com/goravel/framework/support/carbon" "github.com/goravel/framework/support/file" ) @@ -19,6 +20,7 @@ func TestMigrateMakeCommand(t *testing.T) { var ( mockConfig *mocksconfig.Config mockContext *mocksconsole.Context + mockSchema *mocksmigration.Schema ) now := carbon.Now() @@ -27,6 +29,7 @@ func TestMigrateMakeCommand(t *testing.T) { beforeEach := func() { mockConfig = mocksconfig.NewConfig(t) mockContext = mocksconsole.NewContext(t) + mockSchema = mocksmigration.NewSchema(t) } tests := []struct { @@ -38,8 +41,8 @@ func TestMigrateMakeCommand(t *testing.T) { { 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() + mockContext.EXPECT().Argument(0).Return("").Once() + mockContext.EXPECT().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"), @@ -47,8 +50,9 @@ func TestMigrateMakeCommand(t *testing.T) { { name: "default driver", setup: func() { - mockContext.On("Argument", 0).Return("create_users_table").Once() - mockConfig.On("GetString", "database.migrations.driver").Return(contractsmigration.DriverDefault).Once() + mockContext.EXPECT().Argument(0).Return("create_users_table").Once() + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverDefault).Once() + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() }, assert: func() { migration := fmt.Sprintf("database/migrations/%s_%s.go", now.ToShortDateTimeString(), "create_users_table") @@ -59,11 +63,12 @@ func TestMigrateMakeCommand(t *testing.T) { { name: "sql driver", setup: func() { - mockContext.On("Argument", 0).Return("create_users_table").Once() - mockConfig.On("GetString", "database.default").Return("postgres").Once() - mockConfig.On("GetString", "database.migrations.driver").Return(contractsmigration.DriverSql).Once() - mockConfig.On("GetString", "database.connections.postgres.driver").Return("postgres").Once() - mockConfig.On("GetString", "database.connections.postgres.charset").Return("utf8mb4").Once() + mockContext.EXPECT().Argument(0).Return("create_users_table").Once() + mockConfig.EXPECT().GetString("database.default").Return("postgres").Once() + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql).Once() + mockConfig.EXPECT().GetString("database.connections.postgres.driver").Return("postgres").Once() + mockConfig.EXPECT().GetString("database.connections.postgres.charset").Return("utf8mb4").Once() + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() }, assert: func() { up := fmt.Sprintf("database/migrations/%s_%s.%s.sql", now.ToShortDateTimeString(), "create_users_table", "up") @@ -80,7 +85,7 @@ func TestMigrateMakeCommand(t *testing.T) { beforeEach() test.setup() - migrateMakeCommand := NewMigrateMakeCommand(mockConfig) + migrateMakeCommand := NewMigrateMakeCommand(mockConfig, mockSchema) err := migrateMakeCommand.Handle(mockContext) assert.Equal(t, test.expectErr, err) @@ -88,5 +93,8 @@ func TestMigrateMakeCommand(t *testing.T) { }) } - defer assert.Nil(t, file.Remove("database")) + defer func() { + assert.Nil(t, file.Remove("database")) + carbon.UnsetTestNow() + }() } diff --git a/database/console/migration/migrate_refresh_command.go b/database/console/migration/migrate_refresh_command.go index 05794ccc1..af7fba033 100644 --- a/database/console/migration/migrate_refresh_command.go +++ b/database/console/migration/migrate_refresh_command.go @@ -1,7 +1,6 @@ package migration import ( - "errors" "strconv" "strings" @@ -10,6 +9,7 @@ import ( "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support/color" ) diff --git a/database/console/migration/migrate_refresh_command_test.go b/database/console/migration/migrate_refresh_command_test.go index d271ae319..370bdd062 100644 --- a/database/console/migration/migrate_refresh_command_test.go +++ b/database/console/migration/migrate_refresh_command_test.go @@ -1,12 +1,17 @@ package migration import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + contractsmigration "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/migration" mocksconsole "github.com/goravel/framework/mocks/console" + mocksmigration "github.com/goravel/framework/mocks/database/migration" "github.com/goravel/framework/support/env" "github.com/goravel/framework/support/file" ) @@ -19,52 +24,61 @@ func TestMigrateRefreshCommand(t *testing.T) { testQueries := gorm.NewTestQueries().Queries() for driver, testQuery := range testQueries { query := testQuery.Query() + mockConfig := testQuery.MockConfig() - createMigrations(driver) + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations") + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql) + mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.charset", testQuery.Docker().Driver().String())).Return("utf8bm4") + + mockSchema := mocksmigration.NewSchema(t) + migration.CreateTestMigrations(driver) mockArtisan := mocksconsole.NewArtisan(t) mockContext := mocksconsole.NewContext(t) - mockContext.On("Option", "step").Return("").Once() + mockContext.EXPECT().Option("step").Return("").Once() - migrateCommand := NewMigrateCommand(mockConfig) + migrateCommand := NewMigrateCommand(mockConfig, mockSchema) + require.NotNil(t, migrateCommand) assert.Nil(t, migrateCommand.Handle(mockContext)) // Test MigrateRefreshCommand without --seed flag - mockContext.On("OptionBool", "seed").Return(false).Once() + mockContext.EXPECT().OptionBool("seed").Return(false).Once() migrateRefreshCommand := NewMigrateRefreshCommand(mockConfig, mockArtisan) assert.Nil(t, migrateRefreshCommand.Handle(mockContext)) - var agent Agent + var agent migration.Agent err := query.Where("name", "goravel").First(&agent) assert.Nil(t, err) assert.True(t, agent.ID > 0) - mockArtisan = &mocksconsole.Artisan{} - mockContext = &mocksconsole.Context{} - mockContext.On("Option", "step").Return("5").Once() + mockArtisan = mocksconsole.NewArtisan(t) + mockContext = mocksconsole.NewContext(t) + mockContext.EXPECT().Option("step").Return("5").Once() + mockSchema = mocksmigration.NewSchema(t) - migrateCommand = NewMigrateCommand(mockConfig) + migrateCommand = NewMigrateCommand(mockConfig, mockSchema) + require.NotNil(t, migrateCommand) assert.Nil(t, migrateCommand.Handle(mockContext)) // Test MigrateRefreshCommand with --seed flag and --seeder specified - mockContext.On("OptionBool", "seed").Return(true).Once() - mockContext.On("OptionSlice", "seeder").Return([]string{"UserSeeder"}).Once() - mockArtisan.On("Call", "db:seed --seeder UserSeeder").Return(nil).Once() + mockContext.EXPECT().OptionBool("seed").Return(true).Once() + mockContext.EXPECT().OptionSlice("seeder").Return([]string{"UserSeeder"}).Once() + mockArtisan.EXPECT().Call("db:seed --seeder UserSeeder").Once() migrateRefreshCommand = NewMigrateRefreshCommand(mockConfig, mockArtisan) assert.Nil(t, migrateRefreshCommand.Handle(mockContext)) - mockArtisan = &mocksconsole.Artisan{} - mockContext = &mocksconsole.Context{} + mockArtisan = mocksconsole.NewArtisan(t) + mockContext = mocksconsole.NewContext(t) // Test MigrateRefreshCommand with --seed flag and no --seeder specified - mockContext.On("Option", "step").Return("").Once() - mockContext.On("OptionBool", "seed").Return(true).Once() - mockContext.On("OptionSlice", "seeder").Return([]string{}).Once() - mockArtisan.On("Call", "db:seed").Return(nil).Once() + mockContext.EXPECT().Option("step").Return("").Once() + mockContext.EXPECT().OptionBool("seed").Return(true).Once() + mockContext.EXPECT().OptionSlice("seeder").Return([]string{}).Once() + mockArtisan.EXPECT().Call("db:seed").Once() migrateRefreshCommand = NewMigrateRefreshCommand(mockConfig, mockArtisan) assert.Nil(t, migrateRefreshCommand.Handle(mockContext)) - var agent1 Agent + var agent1 migration.Agent err = query.Where("name", "goravel").First(&agent1) assert.Nil(t, err) assert.True(t, agent1.ID > 0) diff --git a/database/console/migration/migrate_reset_command.go b/database/console/migration/migrate_reset_command.go index cf5959252..72c2a9d55 100644 --- a/database/console/migration/migrate_reset_command.go +++ b/database/console/migration/migrate_reset_command.go @@ -1,13 +1,12 @@ package migration import ( - "errors" - "github.com/golang-migrate/migrate/v4" "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support/color" ) diff --git a/database/console/migration/migrate_reset_command_test.go b/database/console/migration/migrate_reset_command_test.go index 47478d9d2..baa6c63ee 100644 --- a/database/console/migration/migrate_reset_command_test.go +++ b/database/console/migration/migrate_reset_command_test.go @@ -1,12 +1,17 @@ package migration import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + contractsmigration "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/migration" consolemocks "github.com/goravel/framework/mocks/console" + mocksmigration "github.com/goravel/framework/mocks/database/migration" "github.com/goravel/framework/support/env" "github.com/goravel/framework/support/file" ) @@ -19,18 +24,25 @@ func TestMigrateResetCommand(t *testing.T) { testQueries := gorm.NewTestQueries().Queries() for driver, testQuery := range testQueries { query := testQuery.Query() + mockConfig := testQuery.MockConfig() - createMigrations(driver) + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql).Once() + mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.charset", testQuery.Docker().Driver().String())).Return("utf8bm4").Once() + + migration.CreateTestMigrations(driver) mockContext := consolemocks.NewContext(t) + mockSchema := mocksmigration.NewSchema(t) - migrateCommand := NewMigrateCommand(mockConfig) + migrateCommand := NewMigrateCommand(mockConfig, mockSchema) + require.NotNil(t, migrateCommand) assert.Nil(t, migrateCommand.Handle(mockContext)) migrateResetCommand := NewMigrateResetCommand(mockConfig) assert.Nil(t, migrateResetCommand.Handle(mockContext)) - var agent Agent + var agent migration.Agent err := query.Where("name", "goravel").FirstOrFail(&agent) assert.Error(t, err) } diff --git a/database/console/migration/migrate_rollback_command.go b/database/console/migration/migrate_rollback_command.go index c5f817f03..89bbe38cb 100644 --- a/database/console/migration/migrate_rollback_command.go +++ b/database/console/migration/migrate_rollback_command.go @@ -1,7 +1,6 @@ package migration import ( - "errors" "strconv" _ "github.com/go-sql-driver/mysql" @@ -11,6 +10,7 @@ import ( "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support/color" ) diff --git a/database/console/migration/migrate_rollback_command_test.go b/database/console/migration/migrate_rollback_command_test.go index 5e3164256..b03207b59 100644 --- a/database/console/migration/migrate_rollback_command_test.go +++ b/database/console/migration/migrate_rollback_command_test.go @@ -1,12 +1,17 @@ package migration import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + contractsmigration "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/migration" mocksconsole "github.com/goravel/framework/mocks/console" + mocksmigration "github.com/goravel/framework/mocks/database/migration" "github.com/goravel/framework/support/env" "github.com/goravel/framework/support/file" ) @@ -19,16 +24,24 @@ func TestMigrateRollbackCommand(t *testing.T) { testQueries := gorm.NewTestQueries().Queries() for driver, testQuery := range testQueries { query := testQuery.Query() + mockConfig := testQuery.MockConfig() - createMigrations(driver) + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql).Once() + mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.charset", testQuery.Docker().Driver().String())).Return("utf8bm4").Once() + + migration.CreateTestMigrations(driver) mockContext := mocksconsole.NewContext(t) - mockContext.On("Option", "step").Return("1").Once() + mockContext.EXPECT().Option("step").Return("1").Once() + + mockSchema := mocksmigration.NewSchema(t) - migrateCommand := NewMigrateCommand(mockConfig) + migrateCommand := NewMigrateCommand(mockConfig, mockSchema) + require.NotNil(t, migrateCommand) assert.Nil(t, migrateCommand.Handle(mockContext)) - var agent Agent + var agent migration.Agent err := query.Where("name", "goravel").FirstOrFail(&agent) assert.Nil(t, err) assert.True(t, agent.ID > 0) @@ -36,7 +49,7 @@ func TestMigrateRollbackCommand(t *testing.T) { migrateRollbackCommand := NewMigrateRollbackCommand(mockConfig) assert.Nil(t, migrateRollbackCommand.Handle(mockContext)) - var agent1 Agent + var agent1 migration.Agent err = query.Where("name", "goravel").FirstOrFail(&agent1) assert.Error(t, err) } diff --git a/database/console/migration/migrate_status_command_test.go b/database/console/migration/migrate_status_command_test.go index 1ffd573f7..214eca19a 100644 --- a/database/console/migration/migrate_status_command_test.go +++ b/database/console/migration/migrate_status_command_test.go @@ -1,12 +1,17 @@ package migration import ( + "fmt" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + contractsmigration "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/migration" consolemocks "github.com/goravel/framework/mocks/console" + mocksmigration "github.com/goravel/framework/mocks/database/migration" "github.com/goravel/framework/support/env" "github.com/goravel/framework/support/file" ) @@ -19,12 +24,19 @@ func TestMigrateStatusCommand(t *testing.T) { testQueries := gorm.NewTestQueries().Queries() for driver, testQuery := range testQueries { query := testQuery.Query() + mockConfig := testQuery.MockConfig() - createMigrations(driver) + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() + mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql).Once() + mockConfig.EXPECT().GetString(fmt.Sprintf("database.connections.%s.charset", testQuery.Docker().Driver().String())).Return("utf8bm4").Once() + + migration.CreateTestMigrations(driver) mockContext := consolemocks.NewContext(t) + mockSchema := mocksmigration.NewSchema(t) - migrateCommand := NewMigrateCommand(mockConfig) + migrateCommand := NewMigrateCommand(mockConfig, mockSchema) + require.NotNil(t, migrateCommand) assert.Nil(t, migrateCommand.Handle(mockContext)) migrateStatusCommand := NewMigrateStatusCommand(mockConfig) diff --git a/database/console/migration/utils.go b/database/console/migration/utils.go index 3e2c6e527..7fb4e6d33 100644 --- a/database/console/migration/utils.go +++ b/database/console/migration/utils.go @@ -1,26 +1,23 @@ package migration import ( - "fmt" - "github.com/goravel/framework/contracts/config" contractsmigration "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/database/migration" + "github.com/goravel/framework/errors" ) -func GetDriver(config config.Config) (contractsmigration.Driver, error) { +func GetDriver(config config.Config, schema contractsmigration.Schema) (contractsmigration.Driver, error) { driver := config.GetString("database.migrations.driver") switch driver { case contractsmigration.DriverDefault: - return migration.NewDefaultDriver(), nil - case contractsmigration.DriverSql: - connection := config.GetString("database.default") - dbDriver := config.GetString(fmt.Sprintf("database.connections.%s.driver", connection)) - charset := config.GetString(fmt.Sprintf("database.connections.%s.charset", connection)) + table := config.GetString("database.migrations.table") - return migration.NewSqlDriver(dbDriver, charset), nil + return migration.NewDefaultDriver(schema, table), nil + case contractsmigration.DriverSql: + return migration.NewSqlDriver(config), nil default: - return nil, fmt.Errorf("unsupported migration driver: %s", driver) + return nil, errors.MigrationUnsupportedDriver.Args(driver).SetModule(errors.ModuleMigration) } } diff --git a/database/console/migration/utils_test.go b/database/console/migration/utils_test.go index 36d8e5641..f3fffa45a 100644 --- a/database/console/migration/utils_test.go +++ b/database/console/migration/utils_test.go @@ -7,11 +7,16 @@ import ( contractsmigration "github.com/goravel/framework/contracts/database/migration" "github.com/goravel/framework/database/migration" + "github.com/goravel/framework/errors" mocksconfig "github.com/goravel/framework/mocks/config" + mocksmigration "github.com/goravel/framework/mocks/database/migration" ) func TestGetDriver(t *testing.T) { - var mockConfig *mocksconfig.Config + var ( + mockConfig *mocksconfig.Config + mockSchema *mocksmigration.Schema + ) tests := []struct { name string @@ -23,34 +28,37 @@ func TestGetDriver(t *testing.T) { name: "default driver", setup: func() { mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverDefault).Once() + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() }, - expectDriver: migration.NewDefaultDriver(), + expectDriver: &migration.DefaultDriver{}, }, { name: "sql driver", setup: func() { mockConfig.EXPECT().GetString("database.migrations.driver").Return(contractsmigration.DriverSql).Once() + mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() mockConfig.EXPECT().GetString("database.default").Return("postgres").Once() mockConfig.EXPECT().GetString("database.connections.postgres.driver").Return("postgres").Once() mockConfig.EXPECT().GetString("database.connections.postgres.charset").Return("utf8mb4").Once() }, - expectDriver: migration.NewSqlDriver("postgres", "utf8mb4"), + expectDriver: &migration.SqlDriver{}, }, { name: "unsupported driver", setup: func() { mockConfig.EXPECT().GetString("database.migrations.driver").Return("unsupported").Once() }, - expectError: "unsupported migration driver: unsupported", + expectError: errors.MigrationUnsupportedDriver.Args("unsupported").SetModule(errors.ModuleMigration).Error(), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { mockConfig = mocksconfig.NewConfig(t) + mockSchema = mocksmigration.NewSchema(t) test.setup() - driver, err := GetDriver(mockConfig) + driver, err := GetDriver(mockConfig, mockSchema) if test.expectError != "" { assert.EqualError(t, err, test.expectError) assert.Nil(t, driver) diff --git a/database/console/seed_command.go b/database/console/seed_command.go index 9685e89f7..93aa2220f 100644 --- a/database/console/seed_command.go +++ b/database/console/seed_command.go @@ -1,22 +1,20 @@ package console import ( - "errors" - "fmt" - "github.com/goravel/framework/contracts/config" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" - "github.com/goravel/framework/contracts/database/seeder" + contractsseeder "github.com/goravel/framework/contracts/database/seeder" + "github.com/goravel/framework/errors" "github.com/goravel/framework/support/color" ) type SeedCommand struct { config config.Config - seeder seeder.Facade + seeder contractsseeder.Facade } -func NewSeedCommand(config config.Config, seeder seeder.Facade) *SeedCommand { +func NewSeedCommand(config config.Config, seeder contractsseeder.Facade) *SeedCommand { return &SeedCommand{ config: config, seeder: seeder, @@ -85,19 +83,19 @@ func (receiver *SeedCommand) ConfirmToProceed(force bool) error { return nil } - return errors.New("application in production use --force to run this command") + return errors.DBForceIsRequiredInProduction } // GetSeeders returns a seeder instances -func (receiver *SeedCommand) GetSeeders(names []string) ([]seeder.Seeder, error) { +func (receiver *SeedCommand) GetSeeders(names []string) ([]contractsseeder.Seeder, error) { if len(names) == 0 { return receiver.seeder.GetSeeders(), nil } - var seeders []seeder.Seeder + var seeders []contractsseeder.Seeder for _, name := range names { seeder := receiver.seeder.GetSeeder(name) if seeder == nil { - return nil, fmt.Errorf("no seeder of %s found", name) + return nil, errors.DBSeederNotFound.Args(name) } seeders = append(seeders, seeder) } diff --git a/database/console/seed_command_test.go b/database/console/seed_command_test.go index 38efc81b7..8716d8b4a 100644 --- a/database/console/seed_command_test.go +++ b/database/console/seed_command_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/goravel/framework/contracts/database/seeder" + "github.com/goravel/framework/errors" configmocks "github.com/goravel/framework/mocks/config" consolemocks "github.com/goravel/framework/mocks/console" seedermocks "github.com/goravel/framework/mocks/database/seeder" @@ -51,7 +52,7 @@ func (s *SeedCommandTestSuite) TestConfirmToProceed() { s.mockContext.On("OptionBool", "force").Return(false).Once() err = s.seedCommand.ConfirmToProceed(false) - s.EqualError(err, "application in production use --force to run this command") + s.ErrorIs(err, errors.DBForceIsRequiredInProduction) } func (s *SeedCommandTestSuite) TestGetSeeders() { diff --git a/database/gorm/query_test.go b/database/gorm/query_test.go index 961e461d8..1509770dc 100644 --- a/database/gorm/query_test.go +++ b/database/gorm/query_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" _ "gorm.io/driver/postgres" @@ -42,7 +43,12 @@ func (s *QueryTestSuite) SetupSuite() { testQueries := NewTestQueries() s.queries = testQueries.Queries() + for _, query := range s.queries { + query.CreateTable() + } + s.additionalQuery = testQueries.QueryOfAdditional() + s.additionalQuery.CreateTable() } func (s *QueryTestSuite) SetupTest() {} @@ -3621,20 +3627,40 @@ func TestReadWriteSeparate(t *testing.T) { for drive, db := range dbs { t.Run(drive.String(), func(t *testing.T) { + var ( + mixQuery contractsorm.Query + err error + ) + if drive == database.DriverSqlite { + mixQuery, err = db["write"].QueryOfReadWrite(TestReadWriteConfig{ + ReadDatabase: db["read"].Docker().Config().Database, + }) + } else { + mixQuery, err = db["write"].QueryOfReadWrite(TestReadWriteConfig{ + ReadPort: db["read"].Docker().Config().Port, + WritePort: db["write"].Docker().Config().Port, + }) + } + + require.NoError(t, err) + + db["read"].CreateTable(TestTableUsers) + db["write"].CreateTable(TestTableUsers) + user := User{Name: "user"} - assert.Nil(t, db["mix"].Create(&user)) + assert.Nil(t, mixQuery.Create(&user)) assert.True(t, user.ID > 0) var user2 User - assert.Nil(t, db["mix"].Find(&user2, user.ID)) + assert.Nil(t, mixQuery.Find(&user2, user.ID)) assert.True(t, user2.ID == 0) var user3 User - assert.Nil(t, db["read"].Find(&user3, user.ID)) + assert.Nil(t, db["read"].Query().Find(&user3, user.ID)) assert.True(t, user3.ID == 0) var user4 User - assert.Nil(t, db["write"].Find(&user4, user.ID)) + assert.Nil(t, db["write"].Query().Find(&user4, user.ID)) assert.True(t, user4.ID > 0) }) } @@ -3649,6 +3675,8 @@ func TestTablePrefixAndSingular(t *testing.T) { for drive, db := range dbs { t.Run(drive.String(), func(t *testing.T) { + db.CreateTable(TestTableGoravelUser) + user := User{Name: "user"} assert.Nil(t, db.Query().Create(&user)) assert.True(t, user.ID > 0) diff --git a/database/gorm/test_utils.go b/database/gorm/test_utils.go index c29882a6d..c8284ec97 100644 --- a/database/gorm/test_utils.go +++ b/database/gorm/test_utils.go @@ -53,118 +53,63 @@ type TestQueries struct { } func NewTestQueries() *TestQueries { - if supportdocker.TestModel == supportdocker.TestModelMinimum { - return &TestQueries{ - sqliteDockers: supportdocker.Sqlites(2), - postgresDockers: supportdocker.Postgreses(2), - } + testQueries := &TestQueries{ + sqliteDockers: supportdocker.Sqlites(2), + postgresDockers: supportdocker.Postgreses(2), } - return &TestQueries{ - mysqlDockers: supportdocker.Mysqls(2), - postgresDockers: supportdocker.Postgreses(2), - sqliteDockers: supportdocker.Sqlites(2), - sqlserverDockers: supportdocker.Sqlservers(2), + if supportdocker.TestModel == supportdocker.TestModelMinimum { + return testQueries } + + testQueries.mysqlDockers = supportdocker.Mysqls(2) + testQueries.sqlserverDockers = supportdocker.Sqlservers(2) + + return testQueries } func (r *TestQueries) Queries() map[contractsdatabase.Driver]*TestQuery { return r.queries(false) } -func (r *TestQueries) QueriesOfReadWrite() map[contractsdatabase.Driver]map[string]orm.Query { +func (r *TestQueries) QueriesOfReadWrite() map[contractsdatabase.Driver]map[string]*TestQuery { readPostgresQuery := NewTestQuery(r.postgresDockers[0]) - readPostgresQuery.CreateTable(TestTableUsers) - writePostgresQuery := NewTestQuery(r.postgresDockers[1]) - writePostgresQuery.CreateTable(TestTableUsers) - - postgresQuery, err := writePostgresQuery.QueryOfReadWrite(TestReadWriteConfig{ - ReadPort: readPostgresQuery.Docker().Config().Port, - WritePort: writePostgresQuery.Docker().Config().Port, - }) - if err != nil { - panic(err) - } readSqliteQuery := NewTestQuery(r.sqliteDockers[0]) - readSqliteQuery.CreateTable(TestTableUsers) - writeSqliteQuery := NewTestQuery(r.sqliteDockers[1]) - writeSqliteQuery.CreateTable(TestTableUsers) - sqliteQuery, err := writeSqliteQuery.QueryOfReadWrite(TestReadWriteConfig{ - ReadDatabase: readSqliteQuery.Docker().Config().Database, - }) - if err != nil { - panic(err) + queries := map[contractsdatabase.Driver]map[string]*TestQuery{ + contractsdatabase.DriverPostgres: { + "read": readPostgresQuery, + "write": writePostgresQuery, + }, + contractsdatabase.DriverSqlite: { + "read": readSqliteQuery, + "write": writeSqliteQuery, + }, } if supportdocker.TestModel == supportdocker.TestModelMinimum { - return map[contractsdatabase.Driver]map[string]orm.Query{ - contractsdatabase.DriverPostgres: { - "mix": postgresQuery, - "read": readPostgresQuery.Query(), - "write": writePostgresQuery.Query(), - }, - contractsdatabase.DriverSqlite: { - "mix": sqliteQuery, - "read": readSqliteQuery.Query(), - "write": writeSqliteQuery.Query(), - }, - } + return queries } readMysqlQuery := NewTestQuery(r.mysqlDockers[0]) - readMysqlQuery.CreateTable(TestTableUsers) - writeMysqlQuery := NewTestQuery(r.mysqlDockers[1]) - writeMysqlQuery.CreateTable(TestTableUsers) - - mysqlQuery, err := writeMysqlQuery.QueryOfReadWrite(TestReadWriteConfig{ - ReadPort: readMysqlQuery.Docker().Config().Port, - WritePort: writeMysqlQuery.Docker().Config().Port, - }) - if err != nil { - panic(err) - } readSqlserverQuery := NewTestQuery(r.sqlserverDockers[0]) - readSqlserverQuery.CreateTable(TestTableUsers) - writeSqlserverQuery := NewTestQuery(r.sqlserverDockers[1]) - writeSqlserverQuery.CreateTable(TestTableUsers) - sqlserverQuery, err := writeSqlserverQuery.QueryOfReadWrite(TestReadWriteConfig{ - ReadPort: readSqlserverQuery.Docker().Config().Port, - WritePort: writeSqlserverQuery.Docker().Config().Port, - }) - if err != nil { - panic(err) + queries[contractsdatabase.DriverMysql] = map[string]*TestQuery{ + "read": readMysqlQuery, + "write": writeMysqlQuery, } - - return map[contractsdatabase.Driver]map[string]orm.Query{ - contractsdatabase.DriverMysql: { - "mix": mysqlQuery, - "read": readMysqlQuery.Query(), - "write": writeMysqlQuery.Query(), - }, - contractsdatabase.DriverPostgres: { - "mix": postgresQuery, - "read": readPostgresQuery.Query(), - "write": writePostgresQuery.Query(), - }, - contractsdatabase.DriverSqlite: { - "mix": sqliteQuery, - "read": readSqliteQuery.Query(), - "write": writeSqliteQuery.Query(), - }, - contractsdatabase.DriverSqlserver: { - "mix": sqlserverQuery, - "read": readSqlserverQuery.Query(), - "write": writeSqlserverQuery.Query(), - }, + queries[contractsdatabase.DriverSqlserver] = map[string]*TestQuery{ + "read": readSqlserverQuery, + "write": writeSqlserverQuery, } + + return queries } func (r *TestQueries) QueriesWithPrefixAndSingular() map[contractsdatabase.Driver]*TestQuery { @@ -173,7 +118,6 @@ func (r *TestQueries) QueriesWithPrefixAndSingular() map[contractsdatabase.Drive func (r *TestQueries) QueryOfAdditional() *TestQuery { postgresQuery := NewTestQuery(r.postgresDockers[1]) - postgresQuery.CreateTable() return postgresQuery } @@ -193,7 +137,6 @@ func (r *TestQueries) queries(withPrefixAndSingular bool) map[contractsdatabase. for driver, docker := range driverToDocker { query := NewTestQuery(docker, withPrefixAndSingular) - query.CreateTable() driverToTestQuery[driver] = query } diff --git a/database/migration/default_creator.go b/database/migration/default_creator.go index 25558fcb4..b00935f06 100644 --- a/database/migration/default_creator.go +++ b/database/migration/default_creator.go @@ -31,9 +31,10 @@ func (r *DefaultCreator) GetStub(table string, create bool) string { } // PopulateStub Populate the place-holders in the migration stub. -func (r *DefaultCreator) PopulateStub(stub, name string) string { - stub = strings.ReplaceAll(stub, "DummyMigration", str.Of(name).Prepend("m_").Studly().String()) - stub = strings.ReplaceAll(stub, "DummyName", name) +func (r *DefaultCreator) PopulateStub(stub, signature, table string) string { + stub = strings.ReplaceAll(stub, "DummyMigration", str.Of(signature).Prepend("m_").Studly().String()) + stub = strings.ReplaceAll(stub, "DummySignature", signature) + stub = strings.ReplaceAll(stub, "DummyTable", table) return stub } diff --git a/database/migration/default_creator_test.go b/database/migration/default_creator_test.go new file mode 100644 index 000000000..820a8c086 --- /dev/null +++ b/database/migration/default_creator_test.go @@ -0,0 +1,135 @@ +package migration + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/goravel/framework/support/carbon" +) + +type DefaultCreatorSuite struct { + suite.Suite + defaultCreator *DefaultCreator +} + +func TestDefaultCreatorSuite(t *testing.T) { + suite.Run(t, &DefaultCreatorSuite{}) +} + +func (s *DefaultCreatorSuite) SetupTest() { + s.defaultCreator = NewDefaultCreator() +} + +func (s *DefaultCreatorSuite) TestPopulateStub() { + tests := []struct { + name string + stub string + signature string + table string + expected string + }{ + { + name: "Empty stub", + stub: Stubs{}.Empty(), + signature: "202410131203_create_users_table", + table: "users", + expected: `package migrations + +type M202410131203CreateUsersTable struct { +} + +// Signature The unique signature for the migration. +func (r *M202410131203CreateUsersTable) Signature() string { + return "202410131203_create_users_table" +} + +// Up Run the migrations. +func (r *M202410131203CreateUsersTable) Up() { + +} + +// Down Reverse the migrations. +func (r *M202410131203CreateUsersTable) Down() { + +} +`, + }, + { + name: "Create stub", + stub: Stubs{}.Create(), + signature: "202410131203_create_users_table", + table: "users", + expected: `package migrations + +import "github.com/goravel/framework/contracts/database/migration" + +type M202410131203CreateUsersTable struct { +} + +// Signature The unique signature for the migration. +func (r *M202410131203CreateUsersTable) Signature() string { + return "202410131203_create_users_table" +} + +// Up Run the migrations. +func (r *M202410131203CreateUsersTable) Up() { + facades.Schema.Create("users", func(table migration.Blueprint) { + table.BigIncrements("id") + table.Timestamps() + }) +} + +// Down Reverse the migrations. +func (r *M202410131203CreateUsersTable) Down() { + facades.Schema.DropIfExists("users") +} +`, + }, + { + name: "Update stub", + stub: Stubs{}.Update(), + signature: "202410131203_create_users_table", + expected: `package migrations + +import "github.com/goravel/framework/contracts/database/migration" + +type M202410131203CreateUsersTable struct { +} + +// Signature The unique signature for the migration. +func (r *M202410131203CreateUsersTable) Signature() string { + return "202410131203_create_users_table" +} + +// Up Run the migrations. +func (r *M202410131203CreateUsersTable) Up() { + facades.Schema.Table("users", func(table migration.Blueprint) { + + }) +} + +// Down Reverse the migrations. +func (r *M202410131203CreateUsersTable) Down() { + +} +`, + table: "users", + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + actual := s.defaultCreator.PopulateStub(test.stub, test.signature, test.table) + s.Equal(test.expected, actual) + }) + } +} + +func (s *DefaultCreatorSuite) TestGetFileName() { + now := carbon.FromDateTime(2024, 8, 17, 21, 45, 1) + carbon.SetTestNow(now) + + actual := s.defaultCreator.GetFileName("create_users_table") + s.Contains(actual, "20240817214501_create_users_table") +} diff --git a/database/migration/default_driver.go b/database/migration/default_driver.go index 42434a0f3..71404dc38 100644 --- a/database/migration/default_driver.go +++ b/database/migration/default_driver.go @@ -1,33 +1,108 @@ package migration import ( + "slices" + + "github.com/goravel/framework/contracts/database/migration" + "github.com/goravel/framework/support/color" "github.com/goravel/framework/support/file" ) type DefaultDriver struct { + creator *DefaultCreator + repository migration.Repository + schema migration.Schema } -func NewDefaultDriver() *DefaultDriver { - return &DefaultDriver{} +func NewDefaultDriver(schema migration.Schema, table string) *DefaultDriver { + return &DefaultDriver{ + creator: NewDefaultCreator(), + repository: NewRepository(schema, table), + schema: schema, + } } func (r *DefaultDriver) Create(name string) error { - creator := NewDefaultCreator() table, create := TableGuesser{}.Guess(name) - stub := creator.GetStub(table, create) + stub := r.creator.GetStub(table, create) // Prepend timestamp to the file name. - fileName := creator.GetFileName(name) + fileName := r.creator.GetFileName(name) // Create the up.sql file. - if err := file.Create(creator.GetPath(fileName), creator.PopulateStub(stub, fileName)); err != nil { + if err := file.Create(r.creator.GetPath(fileName), r.creator.PopulateStub(stub, fileName, table)); err != nil { return err } return nil } -func (r *DefaultDriver) Run(paths []string) error { +func (r *DefaultDriver) Run() error { + r.prepareDatabase() + + ran, err := r.repository.GetRan() + if err != nil { + return err + } + + pendingMigrations := r.pendingMigrations(r.schema.Migrations(), ran) + + return r.runPending(pendingMigrations) +} + +func (r *DefaultDriver) pendingMigrations(migrations []migration.Migration, ran []string) []migration.Migration { + var pendingMigrations []migration.Migration + for _, migration := range migrations { + if !slices.Contains(ran, migration.Signature()) { + pendingMigrations = append(pendingMigrations, migration) + } + } + + return pendingMigrations +} + +func (r *DefaultDriver) prepareDatabase() { + if r.repository.RepositoryExists() { + return + } + + r.repository.CreateRepository() +} + +func (r *DefaultDriver) runPending(migrations []migration.Migration) error { + if len(migrations) == 0 { + color.Infoln("Nothing to migrate") + + return nil + } + + batch, err := r.repository.GetNextBatchNumber() + if err != nil { + return err + } + + color.Infoln("Running migration") + + for _, migration := range migrations { + color.Infoln("Running:", migration.Signature()) + + if err := r.runUp(migration, batch); err != nil { + return err + } + } + return nil } + +func (r *DefaultDriver) runUp(file migration.Migration, batch int) error { + if connectionMigration, ok := file.(migration.Connection); ok { + previousConnection := r.schema.GetConnection() + r.schema.SetConnection(connectionMigration.Connection()) + defer r.schema.SetConnection(previousConnection) + } + + file.Up() + + return r.repository.Log(file.Signature(), batch) +} diff --git a/database/migration/default_driver_test.go b/database/migration/default_driver_test.go new file mode 100644 index 000000000..abecf6584 --- /dev/null +++ b/database/migration/default_driver_test.go @@ -0,0 +1,267 @@ +package migration + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/goravel/framework/contracts/database/migration" + mocksmigration "github.com/goravel/framework/mocks/database/migration" + "github.com/goravel/framework/support/carbon" + "github.com/goravel/framework/support/file" +) + +type DefaultDriverSuite struct { + suite.Suite + value int + mockRepository *mocksmigration.Repository + mockSchema *mocksmigration.Schema + driver *DefaultDriver +} + +func TestDefaultDriverSuite(t *testing.T) { + suite.Run(t, &DefaultDriverSuite{}) +} + +func (s *DefaultDriverSuite) SetupTest() { + s.value = 0 + s.mockRepository = mocksmigration.NewRepository(s.T()) + s.mockSchema = mocksmigration.NewSchema(s.T()) + + s.driver = &DefaultDriver{ + creator: NewDefaultCreator(), + repository: s.mockRepository, + schema: s.mockSchema, + } +} + +func (s *DefaultDriverSuite) TestCreate() { + now := carbon.FromDateTime(2024, 8, 17, 21, 45, 1) + carbon.SetTestNow(now) + + pwd, err := os.Getwd() + s.NoError(err) + + path := filepath.Join(pwd, "database", "migrations") + name := "create_users_table" + + s.NoError(s.driver.Create(name)) + + migrationFile := filepath.Join(path, "20240817214501_"+name+".go") + s.True(file.Exists(migrationFile)) + + defer func() { + carbon.UnsetTestNow() + s.NoError(file.Remove("database")) + }() +} + +func (s *DefaultDriverSuite) TestRun() { + tests := []struct { + name string + setup func() + expectError string + }{ + { + name: "Happy path", + setup: func() { + s.mockRepository.EXPECT().RepositoryExists().Return(true).Once() + s.mockRepository.EXPECT().GetRan().Return([]string{"20240817214501_create_agents_table"}, nil).Once() + s.mockSchema.EXPECT().Migrations().Return([]migration.Migration{ + &TestMigration{suite: s}, + &TestConnectionMigration{suite: s}, + }).Once() + s.mockRepository.EXPECT().GetNextBatchNumber().Return(1, nil).Once() + s.mockRepository.EXPECT().Log("20240817214501_create_users_table", 1).Return(nil).Once() + }, + }, + { + name: "Sad path - Log returns error", + setup: func() { + s.mockRepository.EXPECT().RepositoryExists().Return(true).Once() + s.mockRepository.EXPECT().GetRan().Return([]string{"20240817214501_create_agents_table"}, nil).Once() + s.mockSchema.EXPECT().Migrations().Return([]migration.Migration{ + &TestMigration{suite: s}, + &TestConnectionMigration{suite: s}, + }).Once() + s.mockRepository.EXPECT().GetNextBatchNumber().Return(1, nil).Once() + s.mockRepository.EXPECT().Log("20240817214501_create_users_table", 1).Return(errors.New("error")).Once() + }, + expectError: "error", + }, + { + name: "Sad path - GetNextBatchNumber returns error", + setup: func() { + s.mockRepository.EXPECT().RepositoryExists().Return(true).Once() + s.mockRepository.EXPECT().GetRan().Return([]string{"20240817214501_create_agents_table"}, nil).Once() + s.mockSchema.EXPECT().Migrations().Return([]migration.Migration{ + &TestMigration{suite: s}, + &TestConnectionMigration{suite: s}, + }).Once() + s.mockRepository.EXPECT().GetNextBatchNumber().Return(0, errors.New("error")).Once() + }, + expectError: "error", + }, + { + name: "Sad path - GetRan returns error", + setup: func() { + s.mockRepository.EXPECT().RepositoryExists().Return(true).Once() + s.mockRepository.EXPECT().GetRan().Return(nil, errors.New("error")).Once() + }, + expectError: "error", + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + test.setup() + + err := s.driver.Run() + if test.expectError == "" { + s.Nil(err) + } else { + s.EqualError(err, test.expectError) + } + }) + } +} + +func (s *DefaultDriverSuite) TestPendingMigrations() { + migrations := []migration.Migration{ + &TestMigration{suite: s}, + &TestConnectionMigration{suite: s}, + } + ran := []string{ + "20240817214501_create_users_table", + } + + pendingMigrations := s.driver.pendingMigrations(migrations, ran) + s.Len(pendingMigrations, 1) + s.Equal(&TestConnectionMigration{suite: s}, pendingMigrations[0]) +} + +func (s *DefaultDriverSuite) TestPrepareDatabase() { + s.mockRepository.EXPECT().RepositoryExists().Return(true).Once() + s.driver.prepareDatabase() + + s.mockRepository.EXPECT().RepositoryExists().Return(false).Once() + s.mockRepository.EXPECT().CreateRepository().Once() + s.driver.prepareDatabase() +} + +func (s *DefaultDriverSuite) TestRunPending() { + tests := []struct { + name string + migrations []migration.Migration + setup func() + expectError string + }{ + { + name: "Happy path", + migrations: []migration.Migration{ + &TestMigration{suite: s}, + }, + setup: func() { + s.mockRepository.EXPECT().GetNextBatchNumber().Return(1, nil).Once() + s.mockRepository.EXPECT().Log("20240817214501_create_users_table", 1).Return(nil).Once() + }, + }, + { + name: "Happy path - no migrations", + migrations: []migration.Migration{}, + setup: func() {}, + }, + { + name: "Sad path - GetNextBatchNumber returns error", + migrations: []migration.Migration{ + &TestMigration{suite: s}, + }, + setup: func() { + s.mockRepository.EXPECT().GetNextBatchNumber().Return(0, errors.New("error")).Once() + }, + expectError: "error", + }, + { + name: "Sad path - runUp returns error", + migrations: []migration.Migration{ + &TestMigration{suite: s}, + }, + setup: func() { + s.mockRepository.EXPECT().GetNextBatchNumber().Return(1, nil).Once() + s.mockRepository.EXPECT().Log("20240817214501_create_users_table", 1).Return(errors.New("error")).Once() + }, + expectError: "error", + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + test.setup() + + err := s.driver.runPending(test.migrations) + if test.expectError == "" { + s.Nil(err) + } else { + s.EqualError(err, test.expectError) + } + }) + } +} + +func (s *DefaultDriverSuite) TestRunUp() { + batch := 1 + s.mockRepository.EXPECT().Log("20240817214501_create_users_table", batch).Return(nil).Once() + s.NoError(s.driver.runUp(&TestMigration{ + suite: s, + }, batch)) + s.Equal(1, s.value) + + previousConnection := "postgres" + s.mockSchema.EXPECT().GetConnection().Return(previousConnection).Once() + s.mockSchema.EXPECT().SetConnection("mysql").Once() + s.mockSchema.EXPECT().SetConnection(previousConnection).Once() + s.mockRepository.EXPECT().Log("20240817214501_create_agents_table", batch).Return(nil).Once() + s.NoError(s.driver.runUp(&TestConnectionMigration{ + suite: s, + }, batch)) + s.Equal(2, s.value) +} + +type TestMigration struct { + suite *DefaultDriverSuite +} + +func (s *TestMigration) Signature() string { + return "20240817214501_create_users_table" +} + +func (s *TestMigration) Up() { + s.suite.value++ +} + +func (s *TestMigration) Down() { + +} + +type TestConnectionMigration struct { + suite *DefaultDriverSuite +} + +func (s *TestConnectionMigration) Signature() string { + return "20240817214501_create_agents_table" +} + +func (s *TestConnectionMigration) Connection() string { + return "mysql" +} + +func (s *TestConnectionMigration) Up() { + s.suite.value++ +} + +func (s *TestConnectionMigration) Down() { + +} diff --git a/database/migration/driver_test.go b/database/migration/driver_test.go deleted file mode 100644 index a469df51e..000000000 --- a/database/migration/driver_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package migration - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/suite" - - "github.com/goravel/framework/contracts/database/migration" - "github.com/goravel/framework/support/carbon" - "github.com/goravel/framework/support/file" -) - -type DriverSuite struct { - suite.Suite - drivers map[string]migration.Driver -} - -func TestDriverSuite(t *testing.T) { - suite.Run(t, &DriverSuite{}) -} - -func (s *DriverSuite) SetupTest() { - s.drivers = map[string]migration.Driver{ - migration.DriverDefault: NewDefaultDriver(), - migration.DriverSql: NewSqlDriver("postgres", "utf8mb4"), - } -} - -func (s *DriverSuite) TestCreate() { - now := carbon.FromDateTime(2024, 8, 17, 21, 45, 1) - carbon.SetTestNow(now) - - pwd, _ := os.Getwd() - path := filepath.Join(pwd, "database", "migrations") - name := "create_users_table" - - for driverName, driver := range s.drivers { - s.Run(driverName, func() { - s.NoError(driver.Create(name)) - - if driverName == migration.DriverDefault { - migrationFile := filepath.Join(path, "20240817214501_"+name+".go") - s.True(file.Exists(migrationFile)) - } - - if driverName == migration.DriverSql { - upFile := filepath.Join(path, "20240817214501_"+name+".up.sql") - downFile := filepath.Join(path, "20240817214501_"+name+".down.sql") - - s.True(file.Exists(upFile)) - s.True(file.Exists(downFile)) - } - }) - } - - defer func() { - carbon.UnsetTestNow() - s.NoError(file.Remove("database")) - }() -} diff --git a/database/migration/repository.go b/database/migration/repository.go index 5ea99b1b6..b87612af7 100644 --- a/database/migration/repository.go +++ b/database/migration/repository.go @@ -2,25 +2,22 @@ package migration import ( "github.com/goravel/framework/contracts/database/migration" - "github.com/goravel/framework/contracts/database/orm" ) type Repository struct { - query orm.Query schema migration.Schema table string } -func NewRepository(query orm.Query, schema migration.Schema, table string) *Repository { +func NewRepository(schema migration.Schema, table string) *Repository { return &Repository{ - query: query, schema: schema, table: table, } } -func (r *Repository) CreateRepository() error { - return r.schema.Create(r.table, func(table migration.Blueprint) { +func (r *Repository) CreateRepository() { + r.schema.Create(r.table, func(table migration.Blueprint) { table.ID() table.String("migration") table.Integer("batch") @@ -28,13 +25,13 @@ func (r *Repository) CreateRepository() error { } func (r *Repository) Delete(migration string) error { - _, err := r.query.Table(r.table).Where("migration", migration).Delete() + _, err := r.schema.Orm().Query().Table(r.table).Where("migration", migration).Delete() return err } -func (r *Repository) DeleteRepository() error { - return r.schema.DropIfExists(r.table) +func (r *Repository) DeleteRepository() { + r.schema.DropIfExists(r.table) } func (r *Repository) GetLast() ([]migration.File, error) { @@ -44,7 +41,7 @@ func (r *Repository) GetLast() ([]migration.File, error) { return nil, err } - if err := r.query.Table(r.table).Where("batch", lastBatchNumber).OrderByDesc("migration").Get(&files); err != nil { + if err := r.schema.Orm().Query().Table(r.table).Where("batch", lastBatchNumber).OrderByDesc("migration").Get(&files); err != nil { return nil, err } @@ -53,7 +50,7 @@ func (r *Repository) GetLast() ([]migration.File, error) { func (r *Repository) GetMigrations(steps int) ([]migration.File, error) { var files []migration.File - if err := r.query.Table(r.table).Where("batch >= 1").OrderByDesc("batch").OrderByDesc("migration").Limit(steps).Get(&files); err != nil { + if err := r.schema.Orm().Query().Table(r.table).Where("batch >= 1").OrderByDesc("batch").OrderByDesc("migration").Limit(steps).Get(&files); err != nil { return nil, err } @@ -62,7 +59,7 @@ func (r *Repository) GetMigrations(steps int) ([]migration.File, error) { func (r *Repository) GetMigrationsByBatch(batch int) ([]migration.File, error) { var files []migration.File - if err := r.query.Table(r.table).Where("batch", batch).OrderByDesc("migration").Get(&files); err != nil { + if err := r.schema.Orm().Query().Table(r.table).Where("batch", batch).OrderByDesc("migration").Get(&files); err != nil { return nil, err } @@ -80,7 +77,7 @@ func (r *Repository) GetNextBatchNumber() (int, error) { func (r *Repository) GetRan() ([]string, error) { var migrations []string - if err := r.query.Table(r.table).OrderBy("batch").OrderBy("migration").Pluck("migration", &migrations); err != nil { + if err := r.schema.Orm().Query().Table(r.table).OrderBy("batch").OrderBy("migration").Pluck("migration", &migrations); err != nil { return nil, err } @@ -88,7 +85,7 @@ func (r *Repository) GetRan() ([]string, error) { } func (r *Repository) Log(file string, batch int) error { - return r.query.Table(r.table).Create(map[string]any{ + return r.schema.Orm().Query().Table(r.table).Create(map[string]any{ "migration": file, "batch": batch, }) @@ -100,7 +97,7 @@ func (r *Repository) RepositoryExists() bool { func (r *Repository) getLastBatchNumber() (int, error) { var batch int - if err := r.query.Table(r.table).OrderByDesc("batch").Limit(1).Pluck("batch", &batch); err != nil { + if err := r.schema.Orm().Query().Table(r.table).OrderByDesc("batch").Limit(1).Pluck("batch", &batch); err != nil { return 0, err } diff --git a/database/migration/repository_test.go b/database/migration/repository_test.go index 3e346d958..48253dfb7 100644 --- a/database/migration/repository_test.go +++ b/database/migration/repository_test.go @@ -37,24 +37,19 @@ func (s *RepositoryTestSuite) TestCreate_Delete_Exists() { for driver, testQuery := range s.driverToTestQuery { s.Run(driver.String(), func() { repository, mockOrm := s.initRepository(testQuery) + mockTransaction(mockOrm, testQuery) - mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once() - mockOrm.EXPECT().Query().Return(repository.query).Once() + repository.CreateRepository() - err := repository.CreateRepository() - s.NoError(err) - - mockOrm.EXPECT().Query().Return(repository.query).Once() + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() s.True(repository.RepositoryExists()) - mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once() - mockOrm.EXPECT().Query().Return(repository.query).Once() + mockTransaction(mockOrm, testQuery) - err = repository.DeleteRepository() - s.NoError(err) + repository.DeleteRepository() - mockOrm.EXPECT().Query().Return(repository.query).Once() + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() s.False(repository.RepositoryExists()) }) @@ -66,37 +61,51 @@ func (s *RepositoryTestSuite) TestRecord() { s.Run(driver.String(), func() { repository, mockOrm := s.initRepository(testQuery) - mockOrm.EXPECT().Query().Return(repository.query).Once() + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() if !repository.RepositoryExists() { - mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once() - mockOrm.EXPECT().Query().Return(repository.query).Once() + mockTransaction(mockOrm, testQuery) - s.NoError(repository.CreateRepository()) + repository.CreateRepository() } + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + err := repository.Log("migration1", 1) s.NoError(err) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + err = repository.Log("migration2", 1) s.NoError(err) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + err = repository.Log("migration3", 2) s.NoError(err) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + lastBatchNumber, err := repository.getLastBatchNumber() s.NoError(err) s.Equal(2, lastBatchNumber) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + nextBatchNumber, err := repository.GetNextBatchNumber() s.NoError(err) s.Equal(3, nextBatchNumber) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + ranMigrations, err := repository.GetRan() s.NoError(err) s.ElementsMatch([]string{"migration1", "migration2", "migration3"}, ranMigrations) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + migrations, err := repository.GetMigrations(2) + s.NoError(err) s.Len(migrations, 2) s.Equal("migration3", migrations[0].Migration) @@ -104,7 +113,10 @@ func (s *RepositoryTestSuite) TestRecord() { s.Equal("migration2", migrations[1].Migration) s.Equal(1, migrations[1].Batch) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + migrations, err = repository.GetMigrationsByBatch(1) + s.NoError(err) s.Len(migrations, 2) s.Equal("migration2", migrations[0].Migration) @@ -112,15 +124,22 @@ func (s *RepositoryTestSuite) TestRecord() { s.Equal("migration1", migrations[1].Migration) s.Equal(1, migrations[1].Batch) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Twice() + migrations, err = repository.GetLast() + s.NoError(err) s.Len(migrations, 1) s.Equal("migration3", migrations[0].Migration) s.Equal(2, migrations[0].Batch) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + err = repository.Delete("migration1") s.NoError(err) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + ranMigrations, err = repository.GetRan() s.NoError(err) s.ElementsMatch([]string{"migration2", "migration3"}, ranMigrations) @@ -131,5 +150,5 @@ func (s *RepositoryTestSuite) TestRecord() { func (s *RepositoryTestSuite) initRepository(testQuery *gorm.TestQuery) (*Repository, *mocksorm.Orm) { schema, mockOrm := initSchema(s.T(), testQuery) - return NewRepository(testQuery.Query(), schema, "migrations"), mockOrm + return NewRepository(schema, "migrations"), mockOrm } diff --git a/database/migration/schema.go b/database/migration/schema.go index 2ae3eacfc..696d5c5f9 100644 --- a/database/migration/schema.go +++ b/database/migration/schema.go @@ -2,6 +2,7 @@ package migration import ( "fmt" + "os" "github.com/goravel/framework/contracts/config" contractsdatabase "github.com/goravel/framework/contracts/database" @@ -10,13 +11,13 @@ import ( "github.com/goravel/framework/contracts/log" "github.com/goravel/framework/database/migration/grammars" "github.com/goravel/framework/errors" + "github.com/goravel/framework/support/color" ) var _ migration.Schema = (*Schema)(nil) type Schema struct { config config.Config - connection string grammar migration.Grammar log log.Log migrations []migration.Migration @@ -24,40 +25,48 @@ type Schema struct { prefix string } -func NewSchema(config config.Config, connection string, log log.Log, orm contractsorm.Orm) *Schema { - driver := config.GetString(fmt.Sprintf("database.connections.%s.driver", connection)) - prefix := config.GetString(fmt.Sprintf("database.connections.%s.prefix", connection)) +func NewSchema(config config.Config, log log.Log, orm contractsorm.Orm, migrations []migration.Migration) *Schema { + driver := contractsdatabase.Driver(config.GetString(fmt.Sprintf("database.connections.%s.driver", orm.Name()))) + prefix := config.GetString(fmt.Sprintf("database.connections.%s.prefix", orm.Name())) grammar := getGrammar(driver) return &Schema{ config: config, - connection: connection, grammar: grammar, log: log, + migrations: migrations, orm: orm, prefix: prefix, } } func (r *Schema) Connection(name string) migration.Schema { - return NewSchema(r.config, name, r.log, r.orm) + return NewSchema(r.config, r.log, r.orm.Connection(name), r.migrations) } -func (r *Schema) Create(table string, callback func(table migration.Blueprint)) error { +func (r *Schema) Create(table string, callback func(table migration.Blueprint)) { blueprint := r.createBlueprint(table) blueprint.Create() callback(blueprint) - // TODO catch error and rollback - return r.build(blueprint) + if err := r.build(blueprint); err != nil { + color.Red().Printf("failed to create %s table: %v\n", table, err) + os.Exit(1) + } } -func (r *Schema) DropIfExists(table string) error { +func (r *Schema) DropIfExists(table string) { blueprint := r.createBlueprint(table) blueprint.DropIfExists() - // TODO catch error when run migrate command - return r.build(blueprint) + if err := r.build(blueprint); err != nil { + color.Red().Printf("failed to drop %s table: %v\n", table, err) + os.Exit(1) + } +} + +func (r *Schema) GetConnection() string { + return r.orm.Name() } func (r *Schema) GetTables() ([]migration.Table, error) { @@ -75,7 +84,7 @@ func (r *Schema) HasTable(name string) bool { tables, err := r.GetTables() if err != nil { - r.log.Errorf(errors.SchemaFailedToGetTables.Args(r.connection, err).Error()) + r.log.Errorf(errors.SchemaFailedToGetTables.Args(r.orm.Name(), err).Error()) return false } @@ -88,42 +97,58 @@ func (r *Schema) HasTable(name string) bool { return false } +func (r *Schema) Migrations() []migration.Migration { + return r.migrations +} + +func (r *Schema) Orm() contractsorm.Orm { + return r.orm +} + func (r *Schema) Register(migrations []migration.Migration) { r.migrations = migrations } +func (r *Schema) SetConnection(name string) { + r.orm = r.orm.Connection(name) +} + func (r *Schema) Sql(sql string) { - // TODO catch error and rollback, optimize test - _, _ = r.orm.Connection(r.connection).Query().Exec(sql) + if _, err := r.orm.Query().Exec(sql); err != nil { + r.log.Fatalf("failed to execute sql: %v", err) + } } -func (r *Schema) Table(table string, callback func(table migration.Blueprint)) error { +func (r *Schema) Table(table string, callback func(table migration.Blueprint)) { blueprint := r.createBlueprint(table) callback(blueprint) - // TODO catch error and rollback - return r.build(blueprint) + if err := r.build(blueprint); err != nil { + r.log.Fatalf("failed to modify %s table: %v", table, err) + } } func (r *Schema) build(blueprint migration.Blueprint) error { - return blueprint.Build(r.orm.Connection(r.connection).Query(), r.grammar) + return r.orm.Transaction(func(tx contractsorm.Query) error { + return blueprint.Build(tx, r.grammar) + }) } func (r *Schema) createBlueprint(table string) migration.Blueprint { return NewBlueprint(r.prefix, table) } -func getGrammar(driver string) migration.Grammar { +func getGrammar(driver contractsdatabase.Driver) migration.Grammar { switch driver { - case contractsdatabase.DriverMysql.String(): + case contractsdatabase.DriverMysql: // TODO Optimize here when implementing Mysql driver return nil - case contractsdatabase.DriverPostgres.String(): + case contractsdatabase.DriverPostgres: return grammars.NewPostgres() - case contractsdatabase.DriverSqlserver.String(): + case contractsdatabase.DriverSqlserver: // TODO Optimize here when implementing Mysql driver return nil - case contractsdatabase.DriverSqlite.String(): + case contractsdatabase.DriverSqlite: // TODO Optimize here when implementing Mysql driver return nil default: diff --git a/database/migration/schema_test.go b/database/migration/schema_test.go index c1b44cc3c..715c6f16d 100644 --- a/database/migration/schema_test.go +++ b/database/migration/schema_test.go @@ -34,26 +34,28 @@ func (s *SchemaSuite) SetupTest() { } } -func (s *SchemaSuite) TestDropIfExists() { +func (s *SchemaSuite) TestCreate_DropIfExists_HasTable() { for driver, testQuery := range s.driverToTestQuery { s.Run(driver.String(), func() { schema, mockOrm := initSchema(s.T(), testQuery) - table := "drop_if_exists" + mockTransaction(mockOrm, testQuery) + + schema.DropIfExists(table) - mockOrm.EXPECT().Connection(schema.connection).Return(mockOrm).Twice() - mockOrm.EXPECT().Query().Return(testQuery.Query()).Twice() - s.NoError(schema.DropIfExists(table)) - s.NoError(schema.Create(table, func(table migration.Blueprint) { + mockTransaction(mockOrm, testQuery) + + schema.Create(table, func(table migration.Blueprint) { table.String("name") - })) + }) mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + s.True(schema.HasTable(table)) - mockOrm.EXPECT().Connection(schema.connection).Return(mockOrm).Once() - mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() - s.NoError(schema.DropIfExists(table)) + mockTransaction(mockOrm, testQuery) + + schema.DropIfExists(table) mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() s.False(schema.HasTable(table)) @@ -61,23 +63,25 @@ func (s *SchemaSuite) TestDropIfExists() { } } -func (s *SchemaSuite) TestTable() { +func (s *SchemaSuite) TestTable_GetTables() { for driver, testQuery := range s.driverToTestQuery { s.Run(driver.String(), func() { schema, mockOrm := initSchema(s.T(), testQuery) + mockTransaction(mockOrm, testQuery) - mockOrm.EXPECT().Connection(schema.connection).Return(mockOrm).Once() - mockOrm.EXPECT().Query().Return(testQuery.Query()).Times(3) - - err := schema.Create("changes", func(table migration.Blueprint) { + schema.Create("changes", func(table migration.Blueprint) { table.String("name") }) - s.NoError(err) + + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + s.True(schema.HasTable("changes")) + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + tables, err := schema.GetTables() s.NoError(err) - s.Greater(len(tables), 0) + s.Len(tables, 1) // Open this after implementing other methods //s.Require().True(schema.HasColumn("changes", "name")) @@ -128,9 +132,33 @@ func (s *SchemaSuite) TestTable() { } } +func (s *SchemaSuite) TestSql() { + for driver, testQuery := range s.driverToTestQuery { + s.Run(driver.String(), func() { + schema, mockOrm := initSchema(s.T(), testQuery) + mockTransaction(mockOrm, testQuery) + + schema.Create("sql", func(table migration.Blueprint) { + table.String("name") + }) + + mockOrm.EXPECT().Query().Return(testQuery.Query()).Once() + + schema.Sql("insert into goravel_sql (name) values ('goravel');") + + var count int64 + err := testQuery.Query().Table("sql").Where("name", "goravel").Count(&count) + + s.NoError(err) + s.Equal(int64(1), count) + }) + } +} + func initSchema(t *testing.T, testQuery *gorm.TestQuery) (*Schema, *mocksorm.Orm) { mockOrm := mocksorm.NewOrm(t) - schema := NewSchema(testQuery.MockConfig(), testQuery.Docker().Driver().String(), nil, mockOrm) + mockOrm.EXPECT().Name().Return(testQuery.Docker().Driver().String()).Twice() + schema := NewSchema(testQuery.MockConfig(), nil, mockOrm, nil) return schema, mockOrm } diff --git a/database/migration/sql_creator.go b/database/migration/sql_creator.go index f2e12f1f8..2934c584a 100644 --- a/database/migration/sql_creator.go +++ b/database/migration/sql_creator.go @@ -11,11 +11,11 @@ import ( ) type SqlCreator struct { - driver string + driver database.Driver charset string } -func NewSqlCreator(driver, charset string) *SqlCreator { +func NewSqlCreator(driver database.Driver, charset string) *SqlCreator { return &SqlCreator{ driver: driver, charset: charset, @@ -35,7 +35,7 @@ func (r *SqlCreator) GetStub(table string, create bool) (string, string) { return "", "" } - switch database.Driver(r.driver) { + switch r.driver { case database.DriverPostgres: if create { return PostgresStubs{}.CreateUp(), PostgresStubs{}.CreateDown() diff --git a/database/migration/sql_creator_test.go b/database/migration/sql_creator_test.go index 86ef62b6a..d81095e89 100644 --- a/database/migration/sql_creator_test.go +++ b/database/migration/sql_creator_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/suite" + "github.com/goravel/framework/contracts/database" "github.com/goravel/framework/support/carbon" "github.com/goravel/framework/support/str" ) @@ -40,7 +41,7 @@ func (s *SqlCreatorSuite) TestGetPath() { func (s *SqlCreatorSuite) TestPopulateStub() { tests := []struct { name string - driver string + driver database.Driver table string create bool expectUp string diff --git a/database/migration/sql_driver.go b/database/migration/sql_driver.go index 7a60399c4..4a2859407 100644 --- a/database/migration/sql_driver.go +++ b/database/migration/sql_driver.go @@ -1,17 +1,43 @@ package migration import ( + "database/sql" + "fmt" + + "github.com/golang-migrate/migrate/v4" + migratedatabase "github.com/golang-migrate/migrate/v4/database" + "github.com/golang-migrate/migrate/v4/database/mysql" + "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/database/sqlserver" + _ "github.com/golang-migrate/migrate/v4/source/file" + + "github.com/goravel/framework/contracts/config" + "github.com/goravel/framework/contracts/database" + "github.com/goravel/framework/database/console/driver" + databasedb "github.com/goravel/framework/database/db" + "github.com/goravel/framework/errors" + "github.com/goravel/framework/support" + "github.com/goravel/framework/support/color" "github.com/goravel/framework/support/file" ) // TODO Remove in v1.16 type SqlDriver struct { - creator *SqlCreator + configBuilder *databasedb.ConfigBuilder + creator *SqlCreator + table string } -func NewSqlDriver(driver, charset string) *SqlDriver { +func NewSqlDriver(config config.Config) *SqlDriver { + connection := config.GetString("database.default") + charset := config.GetString(fmt.Sprintf("database.connections.%s.charset", connection)) + driver := database.Driver(config.GetString(fmt.Sprintf("database.connections.%s.driver", connection))) + table := config.GetString("database.migrations.table") + return &SqlDriver{ - creator: NewSqlCreator(driver, charset), + configBuilder: databasedb.NewConfigBuilder(config, connection), + creator: NewSqlCreator(driver, charset), + table: table, } } @@ -33,6 +59,91 @@ func (r *SqlDriver) Create(name string) error { return nil } -func (r *SqlDriver) Run(paths []string) error { +func (r *SqlDriver) Run() error { + migrator, err := r.getMigrator() + if err != nil { + return err + } + + if err = migrator.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + color.Red().Println("Migration failed:", err.Error()) + } + return nil } + +func (r *SqlDriver) getMigrator() (*migrate.Migrate, error) { + path := "file://./database/migrations" + if support.RelativePath != "" { + path = fmt.Sprintf("file://%s/database/migrations", support.RelativePath) + } + + writeConfigs := r.configBuilder.Writes() + if len(writeConfigs) == 0 { + return nil, errors.OrmDatabaseConfigNotFound + } + + writeConfig := writeConfigs[0] + dsn := databasedb.Dsn(writeConfigs[0]) + if dsn == "" { + return nil, errors.OrmFailedToGenerateDNS.Args(writeConfig.Connection) + } + + var ( + databaseName string + db *sql.DB + dbDriver migratedatabase.Driver + err error + ) + + switch writeConfig.Driver { + case database.DriverMysql: + databaseName = "mysql" + db, err = sql.Open(databaseName, dsn) + if err != nil { + return nil, err + } + + dbDriver, err = mysql.WithInstance(db, &mysql.Config{ + MigrationsTable: r.table, + }) + case database.DriverPostgres: + databaseName = "postgres" + db, err = sql.Open(databaseName, dsn) + if err != nil { + return nil, err + } + + dbDriver, err = postgres.WithInstance(db, &postgres.Config{ + MigrationsTable: r.table, + }) + case database.DriverSqlite: + databaseName = "sqlite3" + db, err = sql.Open("sqlite", dsn) + if err != nil { + return nil, err + } + + dbDriver, err = driver.WithInstance(db, &driver.Config{ + MigrationsTable: r.table, + }) + case database.DriverSqlserver: + databaseName = "sqlserver" + db, err = sql.Open(databaseName, dsn) + if err != nil { + return nil, err + } + + dbDriver, err = sqlserver.WithInstance(db, &sqlserver.Config{ + MigrationsTable: r.table, + }) + default: + err = errors.OrmDriverNotSupported.Args(writeConfig.Connection) + } + + if err != nil { + return nil, err + } + + return migrate.NewWithDatabaseInstance(path, databaseName, dbDriver) +} diff --git a/database/migration/sql_driver_test.go b/database/migration/sql_driver_test.go new file mode 100644 index 000000000..c3300f114 --- /dev/null +++ b/database/migration/sql_driver_test.go @@ -0,0 +1,96 @@ +package migration + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/suite" + + contractsdatabase "github.com/goravel/framework/contracts/database" + databasedb "github.com/goravel/framework/database/db" + "github.com/goravel/framework/database/gorm" + mocksconfig "github.com/goravel/framework/mocks/config" + "github.com/goravel/framework/support/carbon" + "github.com/goravel/framework/support/env" + "github.com/goravel/framework/support/file" +) + +type SqlDriverSuite struct { + suite.Suite + mockConfig *mocksconfig.Config + driverToTestQuery map[contractsdatabase.Driver]*gorm.TestQuery +} + +func TestSqlDriverSuite(t *testing.T) { + if env.IsWindows() { + t.Skip("Skipping tests of using docker") + } + + suite.Run(t, &SqlDriverSuite{}) +} + +func (s *SqlDriverSuite) SetupSuite() { + s.driverToTestQuery = gorm.NewTestQueries().Queries() +} + +func (s *SqlDriverSuite) SetupTest() { + +} + +func (s *SqlDriverSuite) TearDownTest() { + s.NoError(file.Remove("database")) +} + +func (s *SqlDriverSuite) TestCreate() { + now := carbon.FromDateTime(2024, 8, 17, 21, 45, 1) + carbon.SetTestNow(now) + + pwd, err := os.Getwd() + s.NoError(err) + + path := filepath.Join(pwd, "database", "migrations") + name := "create_users_table" + + s.mockConfig = mocksconfig.NewConfig(s.T()) + s.mockConfig.EXPECT().GetString("database.default").Return("postgres").Once() + s.mockConfig.EXPECT().GetString("database.connections.postgres.driver").Return("postgres").Once() + s.mockConfig.EXPECT().GetString("database.connections.postgres.charset").Return("utf8mb4").Once() + s.mockConfig.EXPECT().GetString("database.migrations.table").Return("migrations").Once() + + driver := NewSqlDriver(s.mockConfig) + + s.NoError(driver.Create(name)) + + upFile := filepath.Join(path, "20240817214501_"+name+".up.sql") + downFile := filepath.Join(path, "20240817214501_"+name+".down.sql") + + s.True(file.Exists(upFile)) + s.True(file.Exists(downFile)) + + defer carbon.UnsetTestNow() +} + +func (s *SqlDriverSuite) TestRun() { + testQueries := gorm.NewTestQueries().Queries() + for driver, testQuery := range testQueries { + query := testQuery.Query() + mockConfig := testQuery.MockConfig() + CreateTestMigrations(driver) + + sqlDriver := &SqlDriver{ + configBuilder: databasedb.NewConfigBuilder(mockConfig, driver.String()), + creator: NewSqlCreator(driver, "utf8mb4"), + table: "migrations", + } + err := sqlDriver.Run() + s.NoError(err) + + var agent Agent + s.NoError(query.Where("name", "goravel").First(&agent)) + s.True(agent.ID > 0) + + err = sqlDriver.Run() + s.NoError(err) + } +} diff --git a/database/migration/stubs.go b/database/migration/stubs.go index 91007d851..ab94d9918 100644 --- a/database/migration/stubs.go +++ b/database/migration/stubs.go @@ -11,12 +11,7 @@ type DummyMigration struct { // Signature The unique signature for the migration. func (r *DummyMigration) Signature() string { - return "DummyName" -} - -// Connection The database connection that should be used by the migration. -func (r *DummyMigration) Connection() string { - return "" + return "DummySignature" } // Up Run the migrations. @@ -31,55 +26,52 @@ func (r *DummyMigration) Down() { ` } -// TODO add the facades.Schema().Create() method func (receiver Stubs) Create() string { return `package migrations +import "github.com/goravel/framework/contracts/database/migration" + type DummyMigration struct { } // Signature The unique signature for the migration. func (r *DummyMigration) Signature() string { - return "DummyName" -} - -// Connection The database connection that should be used by the migration. -func (r *DummyMigration) Connection() string { - return "" + return "DummySignature" } // Up Run the migrations. func (r *DummyMigration) Up() { - + facades.Schema.Create("DummyTable", func(table migration.Blueprint) { + table.BigIncrements("id") + table.Timestamps() + }) } // Down Reverse the migrations. func (r *DummyMigration) Down() { - + facades.Schema.DropIfExists("DummyTable") } ` } -// TODO add the facades.Schema().Table() method func (receiver Stubs) Update() string { return `package migrations +import "github.com/goravel/framework/contracts/database/migration" + type DummyMigration struct { } // Signature The unique signature for the migration. func (r *DummyMigration) Signature() string { - return "DummyName" -} - -// Connection The database connection that should be used by the migration. -func (r *DummyMigration) Connection() string { - return "" + return "DummySignature" } // Up Run the migrations. func (r *DummyMigration) Up() { + facades.Schema.Table("DummyTable", func(table migration.Blueprint) { + }) } // Down Reverse the migrations. diff --git a/database/console/migration/test_utils.go b/database/migration/test_utils.go similarity index 83% rename from database/console/migration/test_utils.go rename to database/migration/test_utils.go index dd9f35430..3b1510bd6 100644 --- a/database/console/migration/test_utils.go +++ b/database/migration/test_utils.go @@ -1,11 +1,22 @@ package migration import ( + "github.com/stretchr/testify/mock" + "github.com/goravel/framework/contracts/database" + contractsorm "github.com/goravel/framework/contracts/database/orm" + "github.com/goravel/framework/database/gorm" + "github.com/goravel/framework/database/orm" + mocksorm "github.com/goravel/framework/mocks/database/orm" "github.com/goravel/framework/support/file" ) -func createMigrations(driver database.Driver) { +type Agent struct { + orm.Model + Name string +} + +func CreateTestMigrations(driver database.Driver) { switch driver { case database.DriverPostgres: createPostgresMigrations() @@ -110,3 +121,9 @@ INSERT INTO agents (name, created_at, updated_at) VALUES ('goravel', '2023-03-11 panic(err) } } + +func mockTransaction(mockOrm *mocksorm.Orm, testQuery *gorm.TestQuery) { + mockOrm.EXPECT().Transaction(mock.Anything).RunAndReturn(func(txFunc func(contractsorm.Query) error) error { + return txFunc(testQuery.Query()) + }).Once() +} diff --git a/database/orm.go b/database/orm.go index 941492b40..2df35cca0 100644 --- a/database/orm.go +++ b/database/orm.go @@ -84,14 +84,14 @@ func (r *Orm) DB() (*sql.DB, error) { return query.Instance().DB() } -func (r *Orm) Query() contractsorm.Query { - return r.query -} - func (r *Orm) Factory() contractsorm.Factory { return NewFactoryImpl(r.Query()) } +func (r *Orm) Name() string { + return r.connection +} + func (r *Orm) Observe(model any, observer contractsorm.Observer) { orm.Observers = append(orm.Observers, orm.Observer{ Model: model, @@ -99,6 +99,10 @@ func (r *Orm) Observe(model any, observer contractsorm.Observer) { }) } +func (r *Orm) Query() contractsorm.Query { + return r.query +} + func (r *Orm) Refresh() { r.refresh(BindingOrm) } diff --git a/database/orm_test.go b/database/orm_test.go index 52a188325..f480044b0 100644 --- a/database/orm_test.go +++ b/database/orm_test.go @@ -41,6 +41,9 @@ func TestOrmSuite(t *testing.T) { func (s *OrmSuite) SetupSuite() { s.testQueries = gorm.NewTestQueries().Queries() + for _, testQuery := range s.testQueries { + testQuery.CreateTable() + } } func (s *OrmSuite) SetupTest() { diff --git a/database/service_provider.go b/database/service_provider.go index 18d84f40d..d835a5871 100644 --- a/database/service_provider.go +++ b/database/service_provider.go @@ -32,7 +32,12 @@ func (r *ServiceProvider) Register(app foundation.Application) { } connection := config.GetString("database.default") - return BuildOrm(ctx, config, connection, log, app.Refresh) + orm, err := BuildOrm(ctx, config, connection, log, app.Refresh) + if err != nil { + return nil, errors.OrmInitConnection.Args(connection, err).SetModule(errors.ModuleOrm) + } + + return orm, nil }) app.Singleton(BindingSchema, func(app foundation.Application) (any, error) { config := app.MakeConfig() @@ -50,9 +55,7 @@ func (r *ServiceProvider) Register(app foundation.Application) { return nil, errors.OrmFacadeNotSet.SetModule(errors.ModuleSchema) } - connection := config.GetString("database.default") - - return migration.NewSchema(config, connection, log, orm), nil + return migration.NewSchema(config, log, orm, nil), nil }) app.Singleton(BindingSeeder, func(app foundation.Application) (any, error) { return NewSeederFacade(), nil @@ -64,16 +67,19 @@ func (r *ServiceProvider) Boot(app foundation.Application) { } func (r *ServiceProvider) registerCommands(app foundation.Application) { - if artisanFacade := app.MakeArtisan(); artisanFacade != nil { - config := app.MakeConfig() - seeder := app.MakeSeeder() - artisanFacade.Register([]contractsconsole.Command{ - consolemigration.NewMigrateMakeCommand(config), - consolemigration.NewMigrateCommand(config), + artisan := app.MakeArtisan() + config := app.MakeConfig() + schema := app.MakeSchema() + seeder := app.MakeSeeder() + + if artisan != nil && config != nil && schema != nil && seeder != nil { + artisan.Register([]contractsconsole.Command{ + consolemigration.NewMigrateMakeCommand(config, schema), + consolemigration.NewMigrateCommand(config, schema), consolemigration.NewMigrateRollbackCommand(config), consolemigration.NewMigrateResetCommand(config), - consolemigration.NewMigrateRefreshCommand(config, artisanFacade), - consolemigration.NewMigrateFreshCommand(config, artisanFacade), + consolemigration.NewMigrateRefreshCommand(config, artisan), + consolemigration.NewMigrateFreshCommand(config, artisan), consolemigration.NewMigrateStatusCommand(config), console.NewModelMakeCommand(), console.NewObserverMakeCommand(), diff --git a/errors/list.go b/errors/list.go index a9336daa1..64ebf10eb 100644 --- a/errors/list.go +++ b/errors/list.go @@ -42,6 +42,9 @@ var ( CryptMissingIVKey = New("decrypt payload error: missing iv key") CryptMissingValueKey = New("decrypt payload error: missing value key") + DBForceIsRequiredInProduction = New("application in production use --force to run this command") + DBSeederNotFound = New("not found %s seeder") + EventListenerNotBind = New("event %v doesn't bind listeners") FilesystemDefaultDiskNotSet = New("please set default disk") @@ -62,11 +65,15 @@ var ( LogDriverNotSupported = New("invalid driver: %s, only support stack, single, daily, custom").SetModule(ModuleLog) LogEmptyLogFilePath = New("empty log file path").SetModule(ModuleLog) + MigrationNameIsRequired = New("migration name cannot be empty") + MigrationUnsupportedDriver = New("unsupported migration driver: %s") + OrmDatabaseConfigNotFound = New("not found database configuration") OrmDriverNotSupported = New("invalid driver: %s, only support mysql, postgres, sqlite and sqlserver") OrmFailedToGenerateDNS = New("failed to generate DSN for connection: %s") OrmFactoryMissingAttributes = New("failed to get raw attributes") OrmFactoryMissingMethod = New("%s does not find factory method") + OrmInitConnection = New("init %s connection error: %v") OrmMissingWhereClause = New("WHERE conditions required") OrmNoDialectorsFound = New("no dialectors found") OrmQueryAssociationsConflict = New("cannot set orm.Associations and other fields at the same time") diff --git a/errors/modules.go b/errors/modules.go index 22376d95a..99f2b3eba 100644 --- a/errors/modules.go +++ b/errors/modules.go @@ -13,6 +13,7 @@ var ( ModuleLang = "lang" ModuleLog = "log" ModuleMail = "mail" + ModuleMigration = "migration" ModuleOrm = "orm" ModuleQueue = "queue" ModuleRoute = "route" diff --git a/mocks/database/migration/Driver.go b/mocks/database/migration/Driver.go index dd05cf498..d679bdc43 100644 --- a/mocks/database/migration/Driver.go +++ b/mocks/database/migration/Driver.go @@ -63,17 +63,17 @@ func (_c *Driver_Create_Call) RunAndReturn(run func(string) error) *Driver_Creat return _c } -// Run provides a mock function with given fields: paths -func (_m *Driver) Run(paths []string) error { - ret := _m.Called(paths) +// Run provides a mock function with given fields: +func (_m *Driver) Run() error { + ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Run") } var r0 error - if rf, ok := ret.Get(0).(func([]string) error); ok { - r0 = rf(paths) + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() } else { r0 = ret.Error(0) } @@ -87,14 +87,13 @@ type Driver_Run_Call struct { } // Run is a helper method to define mock.On call -// - paths []string -func (_e *Driver_Expecter) Run(paths interface{}) *Driver_Run_Call { - return &Driver_Run_Call{Call: _e.mock.On("Run", paths)} +func (_e *Driver_Expecter) Run() *Driver_Run_Call { + return &Driver_Run_Call{Call: _e.mock.On("Run")} } -func (_c *Driver_Run_Call) Run(run func(paths []string)) *Driver_Run_Call { +func (_c *Driver_Run_Call) Run(run func()) *Driver_Run_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].([]string)) + run() }) return _c } @@ -104,7 +103,7 @@ func (_c *Driver_Run_Call) Return(_a0 error) *Driver_Run_Call { return _c } -func (_c *Driver_Run_Call) RunAndReturn(run func([]string) error) *Driver_Run_Call { +func (_c *Driver_Run_Call) RunAndReturn(run func() error) *Driver_Run_Call { _c.Call.Return(run) return _c } diff --git a/mocks/database/migration/Repository.go b/mocks/database/migration/Repository.go index 7c3e518be..dde1940c4 100644 --- a/mocks/database/migration/Repository.go +++ b/mocks/database/migration/Repository.go @@ -21,21 +21,8 @@ func (_m *Repository) EXPECT() *Repository_Expecter { } // CreateRepository provides a mock function with given fields: -func (_m *Repository) CreateRepository() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for CreateRepository") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 +func (_m *Repository) CreateRepository() { + _m.Called() } // Repository_CreateRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateRepository' @@ -55,12 +42,12 @@ func (_c *Repository_CreateRepository_Call) Run(run func()) *Repository_CreateRe return _c } -func (_c *Repository_CreateRepository_Call) Return(_a0 error) *Repository_CreateRepository_Call { - _c.Call.Return(_a0) +func (_c *Repository_CreateRepository_Call) Return() *Repository_CreateRepository_Call { + _c.Call.Return() return _c } -func (_c *Repository_CreateRepository_Call) RunAndReturn(run func() error) *Repository_CreateRepository_Call { +func (_c *Repository_CreateRepository_Call) RunAndReturn(run func()) *Repository_CreateRepository_Call { _c.Call.Return(run) return _c } @@ -112,21 +99,8 @@ func (_c *Repository_Delete_Call) RunAndReturn(run func(string) error) *Reposito } // DeleteRepository provides a mock function with given fields: -func (_m *Repository) DeleteRepository() error { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for DeleteRepository") - } - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 +func (_m *Repository) DeleteRepository() { + _m.Called() } // Repository_DeleteRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteRepository' @@ -146,12 +120,12 @@ func (_c *Repository_DeleteRepository_Call) Run(run func()) *Repository_DeleteRe return _c } -func (_c *Repository_DeleteRepository_Call) Return(_a0 error) *Repository_DeleteRepository_Call { - _c.Call.Return(_a0) +func (_c *Repository_DeleteRepository_Call) Return() *Repository_DeleteRepository_Call { + _c.Call.Return() return _c } -func (_c *Repository_DeleteRepository_Call) RunAndReturn(run func() error) *Repository_DeleteRepository_Call { +func (_c *Repository_DeleteRepository_Call) RunAndReturn(run func()) *Repository_DeleteRepository_Call { _c.Call.Return(run) return _c } diff --git a/mocks/database/migration/Schema.go b/mocks/database/migration/Schema.go index 7614ef16a..652251348 100644 --- a/mocks/database/migration/Schema.go +++ b/mocks/database/migration/Schema.go @@ -5,6 +5,8 @@ package migration import ( migration "github.com/goravel/framework/contracts/database/migration" mock "github.com/stretchr/testify/mock" + + orm "github.com/goravel/framework/contracts/database/orm" ) // Schema is an autogenerated mock type for the Schema type @@ -69,21 +71,8 @@ func (_c *Schema_Connection_Call) RunAndReturn(run func(string) migration.Schema } // Create provides a mock function with given fields: table, callback -func (_m *Schema) Create(table string, callback func(migration.Blueprint)) error { - ret := _m.Called(table, callback) - - if len(ret) == 0 { - panic("no return value specified for Create") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, func(migration.Blueprint)) error); ok { - r0 = rf(table, callback) - } else { - r0 = ret.Error(0) - } - - return r0 +func (_m *Schema) Create(table string, callback func(migration.Blueprint)) { + _m.Called(table, callback) } // Schema_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' @@ -105,32 +94,19 @@ func (_c *Schema_Create_Call) Run(run func(table string, callback func(migration return _c } -func (_c *Schema_Create_Call) Return(_a0 error) *Schema_Create_Call { - _c.Call.Return(_a0) +func (_c *Schema_Create_Call) Return() *Schema_Create_Call { + _c.Call.Return() return _c } -func (_c *Schema_Create_Call) RunAndReturn(run func(string, func(migration.Blueprint)) error) *Schema_Create_Call { +func (_c *Schema_Create_Call) RunAndReturn(run func(string, func(migration.Blueprint))) *Schema_Create_Call { _c.Call.Return(run) return _c } // DropIfExists provides a mock function with given fields: table -func (_m *Schema) DropIfExists(table string) error { - ret := _m.Called(table) - - if len(ret) == 0 { - panic("no return value specified for DropIfExists") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string) error); ok { - r0 = rf(table) - } else { - r0 = ret.Error(0) - } - - return r0 +func (_m *Schema) DropIfExists(table string) { + _m.Called(table) } // Schema_DropIfExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DropIfExists' @@ -151,12 +127,57 @@ func (_c *Schema_DropIfExists_Call) Run(run func(table string)) *Schema_DropIfEx return _c } -func (_c *Schema_DropIfExists_Call) Return(_a0 error) *Schema_DropIfExists_Call { +func (_c *Schema_DropIfExists_Call) Return() *Schema_DropIfExists_Call { + _c.Call.Return() + return _c +} + +func (_c *Schema_DropIfExists_Call) RunAndReturn(run func(string)) *Schema_DropIfExists_Call { + _c.Call.Return(run) + return _c +} + +// GetConnection provides a mock function with given fields: +func (_m *Schema) GetConnection() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetConnection") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Schema_GetConnection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConnection' +type Schema_GetConnection_Call struct { + *mock.Call +} + +// GetConnection is a helper method to define mock.On call +func (_e *Schema_Expecter) GetConnection() *Schema_GetConnection_Call { + return &Schema_GetConnection_Call{Call: _e.mock.On("GetConnection")} +} + +func (_c *Schema_GetConnection_Call) Run(run func()) *Schema_GetConnection_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Schema_GetConnection_Call) Return(_a0 string) *Schema_GetConnection_Call { _c.Call.Return(_a0) return _c } -func (_c *Schema_DropIfExists_Call) RunAndReturn(run func(string) error) *Schema_DropIfExists_Call { +func (_c *Schema_GetConnection_Call) RunAndReturn(run func() string) *Schema_GetConnection_Call { _c.Call.Return(run) return _c } @@ -264,6 +285,100 @@ func (_c *Schema_HasTable_Call) RunAndReturn(run func(string) bool) *Schema_HasT return _c } +// Migrations provides a mock function with given fields: +func (_m *Schema) Migrations() []migration.Migration { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Migrations") + } + + var r0 []migration.Migration + if rf, ok := ret.Get(0).(func() []migration.Migration); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]migration.Migration) + } + } + + return r0 +} + +// Schema_Migrations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrations' +type Schema_Migrations_Call struct { + *mock.Call +} + +// Migrations is a helper method to define mock.On call +func (_e *Schema_Expecter) Migrations() *Schema_Migrations_Call { + return &Schema_Migrations_Call{Call: _e.mock.On("Migrations")} +} + +func (_c *Schema_Migrations_Call) Run(run func()) *Schema_Migrations_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Schema_Migrations_Call) Return(_a0 []migration.Migration) *Schema_Migrations_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Schema_Migrations_Call) RunAndReturn(run func() []migration.Migration) *Schema_Migrations_Call { + _c.Call.Return(run) + return _c +} + +// Orm provides a mock function with given fields: +func (_m *Schema) Orm() orm.Orm { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Orm") + } + + var r0 orm.Orm + if rf, ok := ret.Get(0).(func() orm.Orm); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(orm.Orm) + } + } + + return r0 +} + +// Schema_Orm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Orm' +type Schema_Orm_Call struct { + *mock.Call +} + +// Orm is a helper method to define mock.On call +func (_e *Schema_Expecter) Orm() *Schema_Orm_Call { + return &Schema_Orm_Call{Call: _e.mock.On("Orm")} +} + +func (_c *Schema_Orm_Call) Run(run func()) *Schema_Orm_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Schema_Orm_Call) Return(_a0 orm.Orm) *Schema_Orm_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Schema_Orm_Call) RunAndReturn(run func() orm.Orm) *Schema_Orm_Call { + _c.Call.Return(run) + return _c +} + // Register provides a mock function with given fields: _a0 func (_m *Schema) Register(_a0 []migration.Migration) { _m.Called(_a0) @@ -297,6 +412,39 @@ func (_c *Schema_Register_Call) RunAndReturn(run func([]migration.Migration)) *S return _c } +// SetConnection provides a mock function with given fields: name +func (_m *Schema) SetConnection(name string) { + _m.Called(name) +} + +// Schema_SetConnection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetConnection' +type Schema_SetConnection_Call struct { + *mock.Call +} + +// SetConnection is a helper method to define mock.On call +// - name string +func (_e *Schema_Expecter) SetConnection(name interface{}) *Schema_SetConnection_Call { + return &Schema_SetConnection_Call{Call: _e.mock.On("SetConnection", name)} +} + +func (_c *Schema_SetConnection_Call) Run(run func(name string)) *Schema_SetConnection_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Schema_SetConnection_Call) Return() *Schema_SetConnection_Call { + _c.Call.Return() + return _c +} + +func (_c *Schema_SetConnection_Call) RunAndReturn(run func(string)) *Schema_SetConnection_Call { + _c.Call.Return(run) + return _c +} + // Sql provides a mock function with given fields: sql func (_m *Schema) Sql(sql string) { _m.Called(sql) @@ -331,21 +479,8 @@ func (_c *Schema_Sql_Call) RunAndReturn(run func(string)) *Schema_Sql_Call { } // Table provides a mock function with given fields: table, callback -func (_m *Schema) Table(table string, callback func(migration.Blueprint)) error { - ret := _m.Called(table, callback) - - if len(ret) == 0 { - panic("no return value specified for Table") - } - - var r0 error - if rf, ok := ret.Get(0).(func(string, func(migration.Blueprint)) error); ok { - r0 = rf(table, callback) - } else { - r0 = ret.Error(0) - } - - return r0 +func (_m *Schema) Table(table string, callback func(migration.Blueprint)) { + _m.Called(table, callback) } // Schema_Table_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Table' @@ -367,12 +502,12 @@ func (_c *Schema_Table_Call) Run(run func(table string, callback func(migration. return _c } -func (_c *Schema_Table_Call) Return(_a0 error) *Schema_Table_Call { - _c.Call.Return(_a0) +func (_c *Schema_Table_Call) Return() *Schema_Table_Call { + _c.Call.Return() return _c } -func (_c *Schema_Table_Call) RunAndReturn(run func(string, func(migration.Blueprint)) error) *Schema_Table_Call { +func (_c *Schema_Table_Call) RunAndReturn(run func(string, func(migration.Blueprint))) *Schema_Table_Call { _c.Call.Return(run) return _c } diff --git a/mocks/database/orm/Orm.go b/mocks/database/orm/Orm.go index 0bfbe5d6e..24ca364e4 100644 --- a/mocks/database/orm/Orm.go +++ b/mocks/database/orm/Orm.go @@ -176,6 +176,51 @@ func (_c *Orm_Factory_Call) RunAndReturn(run func() orm.Factory) *Orm_Factory_Ca return _c } +// Name provides a mock function with given fields: +func (_m *Orm) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Orm_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type Orm_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *Orm_Expecter) Name() *Orm_Name_Call { + return &Orm_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *Orm_Name_Call) Run(run func()) *Orm_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Orm_Name_Call) Return(_a0 string) *Orm_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Orm_Name_Call) RunAndReturn(run func() string) *Orm_Name_Call { + _c.Call.Return(run) + return _c +} + // Observe provides a mock function with given fields: model, observer func (_m *Orm) Observe(model interface{}, observer orm.Observer) { _m.Called(model, observer)