Skip to content

Commit 6b2ed2c

Browse files
authored
feat: [#280] Manage migrations table (#664)
* feat: [#280] Manage migrations table * optimize structure * chore: update mocks * fix test * add error * chore: update mocks --------- Co-authored-by: hwbrzzl <hwbrzzl@users.noreply.github.com>
1 parent a2c5e98 commit 6b2ed2c

16 files changed

Lines changed: 1153 additions & 240 deletions

File tree

contracts/database/migration/grammar.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ type Grammar interface {
99
CompileCreate(blueprint Blueprint, query orm.Query) string
1010
// CompileDropIfExists Compile a drop table (if exists) command.
1111
CompileDropIfExists(blueprint Blueprint) string
12+
// CompileTables Compile the query to determine the tables.
13+
CompileTables(database string) string
1214
// GetAttributeCommands Get the commands for the schema build.
1315
GetAttributeCommands() []string
1416
// GetModifiers Get the column modifiers.
Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,30 @@
11
package migration
22

3+
type File struct {
4+
ID uint
5+
Migration string
6+
Batch int
7+
}
8+
39
type Repository interface {
4-
//// CreateRepository Create the migration repository data store.
5-
//CreateRepository()
6-
//// Delete Remove a migration from the log.
7-
//Delete(migration string)
8-
//// DeleteRepository Delete the migration repository data store.
9-
//DeleteRepository()
10-
//// GetLast Get the last migration batch.
11-
//GetLast()
12-
//// GetMigrationBatches Get the completed migrations with their batch numbers.
13-
//GetMigrationBatches()
14-
//// GetMigrations Get the list of migrations.
15-
//GetMigrations(steps int)
16-
//// GetMigrationsByBatch Get the list of the migrations by batch.
17-
//GetMigrationsByBatch(batch int)
18-
//// GetNextBatchNumber Get the next migration batch number.
19-
//GetNextBatchNumber()
20-
//// GetRan Get the completed migrations.
21-
//GetRan()
22-
//// Log that a migration was run.
23-
//Log(file, batch string)
24-
//// RepositoryExists Determine if the migration repository exists.
25-
//RepositoryExists()
10+
// CreateRepository Create the migration repository data store.
11+
CreateRepository() error
12+
// Delete Remove a migration from the log.
13+
Delete(migration string) error
14+
// DeleteRepository Delete the migration repository data store.
15+
DeleteRepository() error
16+
// GetLast Get the last migration batch.
17+
GetLast() ([]File, error)
18+
// GetMigrations Get the list of migrations.
19+
GetMigrations(steps int) ([]File, error)
20+
// GetMigrationsByBatch Get the list of the migrations by batch.
21+
GetMigrationsByBatch(batch int) ([]File, error)
22+
// GetNextBatchNumber Get the next migration batch number.
23+
GetNextBatchNumber() (int, error)
24+
// GetRan Get the completed migrations.
25+
GetRan() ([]string, error)
26+
// Log that a migration was run.
27+
Log(file string, batch int) error
28+
// RepositoryExists Determine if the migration repository exists.
29+
RepositoryExists() bool
2630
}

contracts/database/migration/schema.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package migration
22

33
type Schema interface {
44
// Create a new table on the schema.
5-
Create(table string, callback func(table Blueprint))
5+
Create(table string, callback func(table Blueprint)) error
66
// Connection Get the connection for the schema.
77
Connection(name string) Schema
88
// DropIfExists Drop a table from the schema if exists.
9-
DropIfExists(table string)
9+
DropIfExists(table string) error
10+
// GetTables Get the tables that belong to the database.
11+
GetTables() ([]Table, error)
12+
// HasTable Determine if the given table exists.
13+
HasTable(table string) bool
1014
// Register migrations.
1115
Register([]Migration)
1216
// Sql Execute a sql directly.
@@ -18,14 +22,17 @@ type Schema interface {
1822
type Migration interface {
1923
// Signature Get the migration signature.
2024
Signature() string
21-
// Connection Get the connection for the migration.
22-
Connection() string
2325
// Up Run the migrations.
2426
Up()
2527
// Down Reverse the migrations.
2628
Down()
2729
}
2830

31+
type Connection interface {
32+
// Connection Get the connection for the migration.
33+
Connection() string
34+
}
35+
2936
type Command struct {
3037
Algorithm string
3138
Column ColumnDefinition
@@ -40,3 +47,9 @@ type Command struct {
4047
References []string
4148
Value string
4249
}
50+
51+
type Table struct {
52+
Comment string
53+
Name string
54+
Size int
55+
}

database/migration/blueprint.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ type Blueprint struct {
2121
columns []*ColumnDefinition
2222
commands []*migration.Command
2323
prefix string
24-
schema string
2524
table string
2625
}
2726

28-
func NewBlueprint(prefix, schema string) *Blueprint {
27+
func NewBlueprint(prefix, table string) *Blueprint {
2928
return &Blueprint{
3029
prefix: prefix,
31-
schema: schema,
30+
table: table,
3231
}
3332
}
3433

database/migration/grammars/postgres.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ func (r *Postgres) CompileDropIfExists(blueprint migration.Blueprint) string {
3737
return fmt.Sprintf("drop table if exists %s", blueprint.GetTableName())
3838
}
3939

40+
func (r *Postgres) CompileTables(database string) string {
41+
return "select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, " +
42+
"obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " +
43+
"where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " +
44+
"order by c.relname"
45+
}
46+
4047
func (r *Postgres) GetAttributeCommands() []string {
4148
return r.attributeCommands
4249
}

database/migration/repository.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package migration
2+
3+
import (
4+
"github.com/goravel/framework/contracts/database/migration"
5+
"github.com/goravel/framework/contracts/database/orm"
6+
)
7+
8+
type Repository struct {
9+
query orm.Query
10+
schema migration.Schema
11+
table string
12+
}
13+
14+
func NewRepository(query orm.Query, schema migration.Schema, table string) *Repository {
15+
return &Repository{
16+
query: query,
17+
schema: schema,
18+
table: table,
19+
}
20+
}
21+
22+
func (r *Repository) CreateRepository() error {
23+
return r.schema.Create(r.table, func(table migration.Blueprint) {
24+
table.ID()
25+
table.String("migration")
26+
table.Integer("batch")
27+
})
28+
}
29+
30+
func (r *Repository) Delete(migration string) error {
31+
_, err := r.query.Table(r.table).Where("migration", migration).Delete()
32+
33+
return err
34+
}
35+
36+
func (r *Repository) DeleteRepository() error {
37+
return r.schema.DropIfExists(r.table)
38+
}
39+
40+
func (r *Repository) GetLast() ([]migration.File, error) {
41+
var files []migration.File
42+
lastBatchNumber, err := r.getLastBatchNumber()
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
if err := r.query.Table(r.table).Where("batch", lastBatchNumber).OrderByDesc("migration").Get(&files); err != nil {
48+
return nil, err
49+
}
50+
51+
return files, nil
52+
}
53+
54+
func (r *Repository) GetMigrations(steps int) ([]migration.File, error) {
55+
var files []migration.File
56+
if err := r.query.Table(r.table).Where("batch >= 1").OrderByDesc("batch").OrderByDesc("migration").Limit(steps).Get(&files); err != nil {
57+
return nil, err
58+
}
59+
60+
return files, nil
61+
}
62+
63+
func (r *Repository) GetMigrationsByBatch(batch int) ([]migration.File, error) {
64+
var files []migration.File
65+
if err := r.query.Table(r.table).Where("batch", batch).OrderByDesc("migration").Get(&files); err != nil {
66+
return nil, err
67+
}
68+
69+
return files, nil
70+
}
71+
72+
func (r *Repository) GetNextBatchNumber() (int, error) {
73+
lastBatchNumber, err := r.getLastBatchNumber()
74+
if err != nil {
75+
return 0, err
76+
}
77+
78+
return lastBatchNumber + 1, nil
79+
}
80+
81+
func (r *Repository) GetRan() ([]string, error) {
82+
var migrations []string
83+
if err := r.query.Table(r.table).OrderBy("batch").OrderBy("migration").Pluck("migration", &migrations); err != nil {
84+
return nil, err
85+
}
86+
87+
return migrations, nil
88+
}
89+
90+
func (r *Repository) Log(file string, batch int) error {
91+
return r.query.Table(r.table).Create(map[string]any{
92+
"migration": file,
93+
"batch": batch,
94+
})
95+
}
96+
97+
func (r *Repository) RepositoryExists() bool {
98+
return r.schema.HasTable(r.table)
99+
}
100+
101+
func (r *Repository) getLastBatchNumber() (int, error) {
102+
var batch int
103+
if err := r.query.Table(r.table).OrderByDesc("batch").Limit(1).Pluck("batch", &batch); err != nil {
104+
return 0, err
105+
}
106+
107+
return batch, nil
108+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package migration
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/suite"
7+
8+
"github.com/goravel/framework/contracts/database"
9+
"github.com/goravel/framework/database/gorm"
10+
mocksorm "github.com/goravel/framework/mocks/database/orm"
11+
"github.com/goravel/framework/support/docker"
12+
"github.com/goravel/framework/support/env"
13+
)
14+
15+
type RepositoryTestSuite struct {
16+
suite.Suite
17+
driverToTestQuery map[database.Driver]*gorm.TestQuery
18+
}
19+
20+
func TestRepositoryTestSuite(t *testing.T) {
21+
if env.IsWindows() {
22+
t.Skip("Skipping tests of using docker")
23+
}
24+
25+
suite.Run(t, &RepositoryTestSuite{})
26+
}
27+
28+
func (s *RepositoryTestSuite) SetupTest() {
29+
postgresDocker := docker.Postgres()
30+
postgresQuery := gorm.NewTestQuery(postgresDocker, true)
31+
s.driverToTestQuery = map[database.Driver]*gorm.TestQuery{
32+
database.DriverPostgres: postgresQuery,
33+
}
34+
}
35+
36+
func (s *RepositoryTestSuite) TestCreate_Delete_Exists() {
37+
for driver, testQuery := range s.driverToTestQuery {
38+
s.Run(driver.String(), func() {
39+
repository, mockOrm := s.initRepository(testQuery)
40+
41+
mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once()
42+
mockOrm.EXPECT().Query().Return(repository.query).Once()
43+
44+
err := repository.CreateRepository()
45+
s.NoError(err)
46+
47+
mockOrm.EXPECT().Query().Return(repository.query).Once()
48+
49+
s.True(repository.RepositoryExists())
50+
51+
mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once()
52+
mockOrm.EXPECT().Query().Return(repository.query).Once()
53+
54+
err = repository.DeleteRepository()
55+
s.NoError(err)
56+
57+
mockOrm.EXPECT().Query().Return(repository.query).Once()
58+
59+
s.False(repository.RepositoryExists())
60+
})
61+
}
62+
}
63+
64+
func (s *RepositoryTestSuite) TestRecord() {
65+
for driver, testQuery := range s.driverToTestQuery {
66+
s.Run(driver.String(), func() {
67+
repository, mockOrm := s.initRepository(testQuery)
68+
69+
mockOrm.EXPECT().Query().Return(repository.query).Once()
70+
71+
if !repository.RepositoryExists() {
72+
mockOrm.EXPECT().Connection(driver.String()).Return(mockOrm).Once()
73+
mockOrm.EXPECT().Query().Return(repository.query).Once()
74+
75+
s.NoError(repository.CreateRepository())
76+
}
77+
78+
err := repository.Log("migration1", 1)
79+
s.NoError(err)
80+
81+
err = repository.Log("migration2", 1)
82+
s.NoError(err)
83+
84+
err = repository.Log("migration3", 2)
85+
s.NoError(err)
86+
87+
lastBatchNumber, err := repository.getLastBatchNumber()
88+
s.NoError(err)
89+
s.Equal(2, lastBatchNumber)
90+
91+
nextBatchNumber, err := repository.GetNextBatchNumber()
92+
s.NoError(err)
93+
s.Equal(3, nextBatchNumber)
94+
95+
ranMigrations, err := repository.GetRan()
96+
s.NoError(err)
97+
s.ElementsMatch([]string{"migration1", "migration2", "migration3"}, ranMigrations)
98+
99+
migrations, err := repository.GetMigrations(2)
100+
s.NoError(err)
101+
s.Len(migrations, 2)
102+
s.Equal("migration3", migrations[0].Migration)
103+
s.Equal(2, migrations[0].Batch)
104+
s.Equal("migration2", migrations[1].Migration)
105+
s.Equal(1, migrations[1].Batch)
106+
107+
migrations, err = repository.GetMigrationsByBatch(1)
108+
s.NoError(err)
109+
s.Len(migrations, 2)
110+
s.Equal("migration2", migrations[0].Migration)
111+
s.Equal(1, migrations[0].Batch)
112+
s.Equal("migration1", migrations[1].Migration)
113+
s.Equal(1, migrations[1].Batch)
114+
115+
migrations, err = repository.GetLast()
116+
s.NoError(err)
117+
s.Len(migrations, 1)
118+
s.Equal("migration3", migrations[0].Migration)
119+
s.Equal(2, migrations[0].Batch)
120+
121+
err = repository.Delete("migration1")
122+
s.NoError(err)
123+
124+
ranMigrations, err = repository.GetRan()
125+
s.NoError(err)
126+
s.ElementsMatch([]string{"migration2", "migration3"}, ranMigrations)
127+
})
128+
}
129+
}
130+
131+
func (s *RepositoryTestSuite) initRepository(testQuery *gorm.TestQuery) (*Repository, *mocksorm.Orm) {
132+
schema, mockOrm := initSchema(s.T(), testQuery)
133+
134+
return NewRepository(testQuery.Query(), schema, "migrations"), mockOrm
135+
}

0 commit comments

Comments
 (0)