Skip to content

Commit

Permalink
feat: [#280] Manage migrations table
Browse files Browse the repository at this point in the history
  • Loading branch information
hwbrzzl committed Oct 3, 2024
1 parent aeaf75d commit f77315b
Show file tree
Hide file tree
Showing 14 changed files with 1,050 additions and 183 deletions.
2 changes: 2 additions & 0 deletions contracts/database/migration/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type Grammar interface {
CompileCreate(blueprint Blueprint, query orm.Query) string
// CompileDropIfExists Compile a drop table (if exists) command.
CompileDropIfExists(blueprint Blueprint) string
// CompileTables Compile the query to determine the tables.
CompileTables(database string) string
// GetAttributeCommands Get the commands for the schema build.
GetAttributeCommands() []string
// GetModifiers Get the column modifiers.
Expand Down
48 changes: 26 additions & 22 deletions contracts/database/migration/repository.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
package migration

type File struct {
ID uint
Migration string
Batch int
}

type Repository interface {
//// CreateRepository Create the migration repository data store.
//CreateRepository()
//// Delete Remove a migration from the log.
//Delete(migration string)
//// DeleteRepository Delete the migration repository data store.
//DeleteRepository()
//// GetLast Get the last migration batch.
//GetLast()
//// GetMigrationBatches Get the completed migrations with their batch numbers.
//GetMigrationBatches()
//// GetMigrations Get the list of migrations.
//GetMigrations(steps int)
//// GetMigrationsByBatch Get the list of the migrations by batch.
//GetMigrationsByBatch(batch int)
//// GetNextBatchNumber Get the next migration batch number.
//GetNextBatchNumber()
//// GetRan Get the completed migrations.
//GetRan()
//// Log that a migration was run.
//Log(file, batch string)
//// RepositoryExists Determine if the migration repository exists.
//RepositoryExists()
// CreateRepository Create the migration repository data store.
CreateRepository() error
// Delete Remove a migration from the log.
Delete(migration string) error
// DeleteRepository Delete the migration repository data store.
DeleteRepository() error
// GetLast Get the last migration batch.
GetLast() ([]File, error)
// GetMigrations Get the list of migrations.
GetMigrations(steps int) ([]File, error)
// GetMigrationsByBatch Get the list of the migrations by batch.
GetMigrationsByBatch(batch int) ([]File, error)
// GetNextBatchNumber Get the next migration batch number.
GetNextBatchNumber() int
// GetRan Get the completed migrations.
GetRan() ([]string, error)
// Log that a migration was run.
Log(file string, batch int) error
// RepositoryExists Determine if the migration repository exists.
RepositoryExists() bool
}
14 changes: 12 additions & 2 deletions contracts/database/migration/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package migration

type Schema interface {
// Create a new table on the schema.
Create(table string, callback func(table Blueprint))
Create(table string, callback func(table Blueprint)) error
// Connection Get the connection for the schema.
Connection(name string) Schema
// DropIfExists Drop a table from the schema if exists.
DropIfExists(table string)
DropIfExists(table string) error
// GetTables Get the tables that belong to the database.
GetTables() ([]Table, error)
// HasTable Determine if the given table exists.
HasTable(table string) bool
// Register migrations.
Register([]Migration)
// Sql Execute a sql directly.
Expand Down Expand Up @@ -40,3 +44,9 @@ type Command struct {
References []string
Value string
}

type Table struct {
Comment string
Name string
Size int
}
8 changes: 5 additions & 3 deletions database/migration/blueprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ type Blueprint struct {
table string
}

func NewBlueprint(prefix, schema string) *Blueprint {
return &Blueprint{
func NewBlueprint(table, prefix string) *Blueprint {
blueprint := &Blueprint{
prefix: prefix,
schema: schema,
table: table,
}

return blueprint
}

func (r *Blueprint) BigIncrements(column string) migration.ColumnDefinition {
Expand Down
7 changes: 7 additions & 0 deletions database/migration/grammars/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ func (r *Postgres) CompileDropIfExists(blueprint migration.Blueprint) string {
return fmt.Sprintf("drop table if exists %s", blueprint.GetTableName())
}

func (r *Postgres) CompileTables(database string) string {
return "select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, " +
"obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " +
"where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " +
"order by c.relname"
}

func (r *Postgres) GetAttributeCommands() []string {
return r.attributeCommands
}
Expand Down
98 changes: 98 additions & 0 deletions database/migration/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 {
return &Repository{
query: query,
schema: schema,
table: table,
}
}

func (r *Repository) CreateRepository() error {
return r.schema.Create(r.table, func(table migration.Blueprint) {
table.ID()
table.String("migration")
table.Integer("batch")
})
}

func (r *Repository) Delete(migration string) error {
_, err := r.query.Table(r.table).Where("migration", migration).Delete()

return err
}

func (r *Repository) DeleteRepository() error {
return r.schema.DropIfExists(r.table)
}

func (r *Repository) GetLast() ([]migration.File, error) {
var files []migration.File
if err := r.query.Table(r.table).Where("batch", r.getLastBatchNumber()).OrderByDesc("migration").Get(&files); err != nil {
return nil, err
}

return files, nil
}

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 {
return nil, err
}

return files, nil
}

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 {
return nil, err
}

return files, nil
}

func (r *Repository) GetNextBatchNumber() int {
return r.getLastBatchNumber() + 1
}

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 {
return nil, err
}

return migrations, nil
}

func (r *Repository) Log(file string, batch int) error {
return r.query.Table(r.table).Create(map[string]any{
"migration": file,
"batch": batch,
})
}

func (r *Repository) RepositoryExists() bool {
return r.schema.HasTable(r.table)
}

func (r *Repository) getLastBatchNumber() int {
var batch int
if err := r.query.Table(r.table).OrderBy("batch", "desc").Pluck("batch", &batch); err != nil {
return 0
}

return batch
}
133 changes: 133 additions & 0 deletions database/migration/repository_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package migration

import (
"testing"

"github.com/stretchr/testify/suite"

"github.com/goravel/framework/contracts/database"
"github.com/goravel/framework/contracts/database/migration"
"github.com/goravel/framework/contracts/database/orm"
"github.com/goravel/framework/database/gorm"
mocksorm "github.com/goravel/framework/mocks/database/orm"
"github.com/goravel/framework/support/docker"
"github.com/goravel/framework/support/env"
)

type RepositoryTestSuite struct {
suite.Suite
driverToTestQuery map[database.Driver]*gorm.TestQuery
}

func TestRepositoryTestSuite(t *testing.T) {
if env.IsWindows() {
t.Skip("Skipping tests of using docker")
}

suite.Run(t, &RepositoryTestSuite{})
}

func (s *RepositoryTestSuite) SetupTest() {
postgresDocker := docker.Postgres()
postgresQuery := gorm.NewTestQuery(postgresDocker)
s.driverToTestQuery = map[database.Driver]*gorm.TestQuery{
database.DriverPostgres: postgresQuery,
}
}

func (s *RepositoryTestSuite) TestCreate_Delete_Exists() {
for driver, query := range s.driverToTestQuery {
s.Run(driver.String(), func() {
repository, mockOrm := s.initRepository(s.T(), driver, query.Query())

mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once()
mockOrm.EXPECT().Query().Return(repository.query).Once()

err := repository.CreateRepository()
s.NoError(err)

mockOrm.EXPECT().Query().Return(repository.query).Once()

s.True(repository.RepositoryExists())

mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once()
mockOrm.EXPECT().Query().Return(repository.query).Once()

err = repository.DeleteRepository()
s.NoError(err)

mockOrm.EXPECT().Query().Return(repository.query).Once()

s.False(repository.RepositoryExists())
})
}
}

func (s *RepositoryTestSuite) TestRecord() {
for driver, query := range s.driverToTestQuery {
s.Run(driver.String(), func() {
repository, mockOrm := s.initRepository(s.T(), driver, query.Query())

mockOrm.EXPECT().Query().Return(repository.query).Once()

if !repository.RepositoryExists() {
mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once()
mockOrm.EXPECT().Query().Return(repository.query).Once()

s.NoError(repository.CreateRepository())
}

err := repository.Log("migration1", 1)
s.NoError(err)

err = repository.Log("migration2", 1)
s.NoError(err)

err = repository.Log("migration3", 2)
s.NoError(err)

lastBatchNumber := repository.getLastBatchNumber()
s.Equal(2, lastBatchNumber)

nextBatchNumber := repository.GetNextBatchNumber()
s.Equal(3, nextBatchNumber)

ranMigrations, err := repository.GetRan()
s.NoError(err)
s.ElementsMatch([]string{"migration1", "migration2", "migration3"}, ranMigrations)

migrations, err := repository.GetMigrations(2)
s.NoError(err)
s.ElementsMatch([]migration.File{
{Migration: "migration3", Batch: 2},
{Migration: "migration2", Batch: 1},
}, migrations)

migrations, err = repository.GetMigrationsByBatch(1)
s.NoError(err)
s.ElementsMatch([]migration.File{
{Migration: "migration2", Batch: 1},
{Migration: "migration1", Batch: 1},
}, migrations)

migrations, err = repository.GetLast()
s.NoError(err)
s.ElementsMatch([]migration.File{
{Migration: "migration3", Batch: 2},
}, migrations)

err = repository.Delete("migration1")
s.NoError(err)

ranMigrations, err = repository.GetRan()
s.NoError(err)
s.ElementsMatch([]string{"migration2", "migration3"}, ranMigrations)
})
}
}

func (s *RepositoryTestSuite) initRepository(t *testing.T, driver database.Driver, query orm.Query) (*Repository, *mocksorm.Orm) {
schema, _, _, mockOrm := initSchema(s.T(), driver)

return NewRepository(query, schema, schema.prefix+"migrations"), mockOrm
}
Loading

0 comments on commit f77315b

Please sign in to comment.