From 51df61c5171f0e2cda96dc7c10b5b12273a1eac5 Mon Sep 17 00:00:00 2001 From: ALMAS Date: Sat, 28 Dec 2024 17:36:11 +0800 Subject: [PATCH] feat: Add artisan db:table command (#789) * feat: Add artisan db:table command * chore: Improve unit test * chore: Add command option alias --------- Co-authored-by: Wenbo Han --- console/application.go | 1 + contracts/console/command/command.go | 5 +- database/console/table_command.go | 160 ++++++++++++++++++++ database/console/table_command_test.go | 196 +++++++++++++++++++++++++ database/service_provider.go | 1 + 5 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 database/console/table_command.go create mode 100644 database/console/table_command_test.go diff --git a/console/application.go b/console/application.go index e2ae91bb2..b345166a3 100644 --- a/console/application.go +++ b/console/application.go @@ -41,6 +41,7 @@ func (r *Application) Register(commands []console.Command) { return item.Handle(NewCliContext(ctx)) }, Category: item.Extend().Category, + ArgsUsage: item.Extend().ArgsUsage, Flags: flagsToCliFlags(item.Extend().Flags), OnUsageError: onUsageError, } diff --git a/contracts/console/command/command.go b/contracts/console/command/command.go index b911eb086..fde1b7338 100644 --- a/contracts/console/command/command.go +++ b/contracts/console/command/command.go @@ -13,8 +13,9 @@ const ( ) type Extend struct { - Category string - Flags []Flag + ArgsUsage string + Category string + Flags []Flag } type Flag interface { diff --git a/database/console/table_command.go b/database/console/table_command.go new file mode 100644 index 000000000..39df3cb38 --- /dev/null +++ b/database/console/table_command.go @@ -0,0 +1,160 @@ +package console + +import ( + "fmt" + "strings" + + "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/schema" +) + +type TableCommand struct { + config config.Config + schema schema.Schema +} + +func NewTableCommand(config config.Config, schema schema.Schema) *TableCommand { + return &TableCommand{ + config: config, + schema: schema, + } +} + +// Signature The name and signature of the console command. +func (r *TableCommand) Signature() string { + return "db:table" +} + +// Description The console command description. +func (r *TableCommand) Description() string { + return "Display information about the given database table" +} + +// Extend The console command extend. +func (r *TableCommand) Extend() command.Extend { + return command.Extend{ + Category: "db", + ArgsUsage: " [--] []", + Flags: []command.Flag{ + &command.StringFlag{ + Name: "database", + Aliases: []string{"d"}, + Usage: "The database connection", + }, + }, + } +} + +// Handle Execute the console command. +func (r *TableCommand) Handle(ctx console.Context) error { + ctx.NewLine() + r.schema = r.schema.Connection(ctx.Option("database")) + table := ctx.Argument(0) + tables, err := r.schema.GetTables() + if err != nil { + ctx.Error(fmt.Sprintf("Failed to get tables: %s", err.Error())) + return nil + } + if len(table) == 0 { + table, err = ctx.Choice("Which table would you like to inspect?", func() (choices []console.Choice) { + for i := range tables { + choices = append(choices, console.Choice{ + Key: tables[i].Name, + Value: tables[i].Name, + }) + } + return + }()) + if err != nil { + ctx.Line(err.Error()) + return nil + } + } + for i := range tables { + if tables[i].Name == table { + r.display(ctx, tables[i]) + return nil + } + } + if len(table) > 0 { + ctx.Warning(fmt.Sprintf("Table '%s' doesn't exist.", table)) + ctx.NewLine() + } + return nil +} + +func (r *TableCommand) display(ctx console.Context, table schema.Table) { + columns, err := r.schema.GetColumns(table.Name) + if err != nil { + ctx.Error(fmt.Sprintf("Failed to get columns: %s", err.Error())) + return + } + indexes, err := r.schema.GetIndexes(table.Name) + if err != nil { + ctx.Error(fmt.Sprintf("Failed to get indexes: %s", err.Error())) + return + } + foreignKeys, err := r.schema.GetForeignKeys(table.Name) + if err != nil { + ctx.Error(fmt.Sprintf("Failed to get foreign keys: %s", err.Error())) + return + } + ctx.TwoColumnDetail(fmt.Sprintf("%s", table.Name), "") + ctx.TwoColumnDetail("Columns", fmt.Sprintf("%d", len(columns))) + ctx.TwoColumnDetail("Size", fmt.Sprintf("%.3fMiB", float64(table.Size)/1024/1024)) + if len(columns) > 0 { + ctx.NewLine() + ctx.TwoColumnDetail("Column", "Type") + for i := range columns { + var ( + key = columns[i].Name + value = columns[i].TypeName + attributes []string + ) + if columns[i].Autoincrement { + attributes = append(attributes, "autoincrement") + } + attributes = append(attributes, columns[i].Type) + if columns[i].Nullable { + attributes = append(attributes, "nullable") + } + key = fmt.Sprintf("%s %s", key, strings.Join(attributes, ", ")) + if columns[i].Default != "" { + value = fmt.Sprintf("%s %s", columns[i].Default, value) + } + ctx.TwoColumnDetail(key, value) + } + } + if len(indexes) > 0 { + ctx.NewLine() + ctx.TwoColumnDetail("Index", "") + for i := range indexes { + var attributes []string + if len(indexes[i].Columns) > 1 { + attributes = append(attributes, "compound") + } + if indexes[i].Unique { + attributes = append(attributes, "unique") + } + if indexes[i].Primary { + attributes = append(attributes, "primary") + } + ctx.TwoColumnDetail(fmt.Sprintf("%s %s", indexes[i].Name, strings.Join(indexes[i].Columns, ", ")), strings.Join(attributes, ", ")) + } + } + if len(foreignKeys) > 0 { + ctx.NewLine() + ctx.TwoColumnDetail("Foreign Key", "On Update / On Delete") + for i := range foreignKeys { + key := fmt.Sprintf("%s %s references %s on %s", + foreignKeys[i].Name, + strings.Join(foreignKeys[i].Columns, ", "), + strings.Join(foreignKeys[i].ForeignColumns, ", "), + foreignKeys[i].ForeignTable) + ctx.TwoColumnDetail(key, fmt.Sprintf("%s / %s", foreignKeys[i].OnUpdate, foreignKeys[i].OnDelete)) + } + } + ctx.NewLine() +} diff --git a/database/console/table_command_test.go b/database/console/table_command_test.go new file mode 100644 index 000000000..1ec5ceabb --- /dev/null +++ b/database/console/table_command_test.go @@ -0,0 +1,196 @@ +package console + +import ( + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/goravel/framework/contracts/console" + "github.com/goravel/framework/contracts/database/schema" + mocksconfig "github.com/goravel/framework/mocks/config" + mocksconsole "github.com/goravel/framework/mocks/console" + mocksschema "github.com/goravel/framework/mocks/database/schema" + "github.com/goravel/framework/support/color" +) + +func TestTableCommand(t *testing.T) { + var ( + mockContext *mocksconsole.Context + mockConfig *mocksconfig.Config + mockSchema *mocksschema.Schema + ) + beforeEach := func() { + mockContext = mocksconsole.NewContext(t) + mockConfig = mocksconfig.NewConfig(t) + mockSchema = mocksschema.NewSchema(t) + } + successCaseExpected := [][2]string{ + {"test", ""}, + {"Columns", "1"}, + {"Size", "0.000MiB"}, + {"Column", "Type"}, + {"foo autoincrement, int, nullable", "bar int"}, + {"Index", ""}, + {"index_foo foo, bar", "compound, unique, primary"}, + {"Foreign Key", "On Update / On Delete"}, + {"fk_foo foo references baz on bar", "restrict / cascade"}, + } + tests := []struct { + name string + setup func() + expected string + }{ + { + name: "get tables failed", + setup: func() { + mockContext.EXPECT().NewLine().Once() + mockContext.EXPECT().Option("database").Return("").Once() + mockSchema.EXPECT().Connection("").Return(mockSchema).Once() + mockContext.EXPECT().Argument(0).Return("").Once() + mockSchema.EXPECT().GetTables().Return(nil, assert.AnError).Once() + mockContext.EXPECT().Error(fmt.Sprintf("Failed to get tables: %s", assert.AnError.Error())).Run(func(message string) { + color.Errorln(message) + }).Once() + }, + expected: assert.AnError.Error(), + }, + { + name: "table not found", + setup: func() { + mockContext.EXPECT().NewLine().Times(2) + mockContext.EXPECT().Option("database").Return("test").Once() + mockSchema.EXPECT().Connection("test").Return(mockSchema).Once() + mockContext.EXPECT().Argument(0).Return("test").Once() + mockSchema.EXPECT().GetTables().Return(nil, nil).Once() + mockContext.EXPECT().Warning("Table 'test' doesn't exist.").Run(func(message string) { + color.Warningln(message) + }).Once() + }, + expected: "Table 'test' doesn't exist", + }, + { + name: "choice table canceled", + setup: func() { + mockContext.EXPECT().NewLine().Times(1) + mockContext.EXPECT().Option("database").Return("test").Once() + mockSchema.EXPECT().Connection("test").Return(mockSchema).Once() + mockContext.EXPECT().Argument(0).Return("").Once() + mockSchema.EXPECT().GetTables().Return(nil, nil).Once() + mockContext.EXPECT().Choice("Which table would you like to inspect?", + []console.Choice(nil)).Return("", assert.AnError).Once() + mockContext.EXPECT().Line(assert.AnError.Error()).Run(func(message string) { + color.Default().Println(message) + }).Once() + }, + expected: assert.AnError.Error(), + }, + { + name: "get columns failed", + setup: func() { + mockContext.EXPECT().NewLine().Once() + mockContext.EXPECT().Option("database").Return("test").Once() + mockSchema.EXPECT().Connection("test").Return(mockSchema).Once() + mockContext.EXPECT().Argument(0).Return("").Once() + mockSchema.EXPECT().GetTables().Return([]schema.Table{{Name: "test"}}, nil).Once() + mockContext.EXPECT().Choice("Which table would you like to inspect?", + []console.Choice{{Key: "test", Value: "test"}}).Return("test", nil).Once() + mockSchema.EXPECT().GetColumns("test").Return(nil, assert.AnError).Once() + mockContext.EXPECT().Error(fmt.Sprintf("Failed to get columns: %s", assert.AnError.Error())).Run(func(message string) { + color.Errorln(message) + }).Once() + }, + expected: assert.AnError.Error(), + }, + { + name: "get indexes failed", + setup: func() { + mockContext.EXPECT().NewLine().Once() + mockContext.EXPECT().Option("database").Return("test").Once() + mockSchema.EXPECT().Connection("test").Return(mockSchema).Once() + mockContext.EXPECT().Argument(0).Return("").Once() + mockSchema.EXPECT().GetTables().Return([]schema.Table{{Name: "test"}}, nil).Once() + mockContext.EXPECT().Choice("Which table would you like to inspect?", + []console.Choice{{Key: "test", Value: "test"}}).Return("test", nil).Once() + mockSchema.EXPECT().GetColumns("test").Return(nil, nil).Once() + mockSchema.EXPECT().GetIndexes("test").Return(nil, assert.AnError).Once() + mockContext.EXPECT().Error(fmt.Sprintf("Failed to get indexes: %s", assert.AnError.Error())).Run(func(message string) { + color.Errorln(message) + }).Once() + }, + expected: assert.AnError.Error(), + }, + { + name: "get foreign keys failed", + setup: func() { + mockContext.EXPECT().NewLine().Once() + mockContext.EXPECT().Option("database").Return("test").Once() + mockSchema.EXPECT().Connection("test").Return(mockSchema).Once() + mockContext.EXPECT().Argument(0).Return("").Once() + mockSchema.EXPECT().GetTables().Return([]schema.Table{{Name: "test"}}, nil).Once() + mockContext.EXPECT().Choice("Which table would you like to inspect?", + []console.Choice{{Key: "test", Value: "test"}}).Return("test", nil).Once() + mockSchema.EXPECT().GetColumns("test").Return(nil, nil).Once() + mockSchema.EXPECT().GetIndexes("test").Return(nil, nil).Once() + mockSchema.EXPECT().GetForeignKeys("test").Return(nil, assert.AnError).Once() + mockContext.EXPECT().Error(fmt.Sprintf("Failed to get foreign keys: %s", assert.AnError.Error())).Run(func(message string) { + color.Errorln(message) + }).Once() + }, + expected: assert.AnError.Error(), + }, + { + name: "success", + setup: func() { + mockContext.EXPECT().NewLine().Times(5) + mockContext.EXPECT().Option("database").Return("test").Once() + mockSchema.EXPECT().Connection("test").Return(mockSchema).Once() + mockContext.EXPECT().Argument(0).Return("").Once() + mockSchema.EXPECT().GetTables().Return([]schema.Table{{Name: "test"}}, nil).Once() + mockContext.EXPECT().Choice("Which table would you like to inspect?", + []console.Choice{{Key: "test", Value: "test"}}).Return("test", nil).Once() + mockSchema.EXPECT().GetColumns("test").Return([]schema.Column{ + {Name: "foo", Type: "int", TypeName: "int", Autoincrement: true, Nullable: true, Default: "bar"}, + }, nil).Once() + mockSchema.EXPECT().GetIndexes("test").Return([]schema.Index{ + {Name: "index_foo", Columns: []string{"foo", "bar"}, Unique: true, Primary: true}, + }, nil).Once() + mockSchema.EXPECT().GetForeignKeys("test").Return([]schema.ForeignKey{ + { + Name: "fk_foo", + Columns: []string{"foo"}, + ForeignTable: "bar", + ForeignColumns: []string{"baz"}, + OnDelete: "cascade", + OnUpdate: "restrict", + }, + }, nil).Once() + for i := range successCaseExpected { + mockContext.EXPECT().TwoColumnDetail(successCaseExpected[i][0], successCaseExpected[i][1]).Run(func(first string, second string, filler ...rune) { + color.Default().Printf("%s %s\n", first, second) + }).Once() + } + }, + expected: func() string { + var result string + for i := range successCaseExpected { + result += color.Default().Sprintf("%s %s\n", successCaseExpected[i][0], successCaseExpected[i][1]) + } + return result + }(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + beforeEach() + test.setup() + command := NewTableCommand(mockConfig, mockSchema) + assert.Contains(t, color.CaptureOutput(func(_ io.Writer) { + assert.NoError(t, command.Handle(mockContext)) + }), test.expected) + }) + } + +} diff --git a/database/service_provider.go b/database/service_provider.go index 111943043..99b43d148 100644 --- a/database/service_provider.go +++ b/database/service_provider.go @@ -112,6 +112,7 @@ func (r *ServiceProvider) registerCommands(app foundation.Application) { console.NewSeedCommand(config, seeder), console.NewSeederMakeCommand(), console.NewFactoryMakeCommand(), + console.NewTableCommand(config, schema), console.NewShowCommand(config, schema), console.NewWipeCommand(config, schema), })