Skip to content

Commit

Permalink
feat: Add artisan db:table command (#789)
Browse files Browse the repository at this point in the history
* feat: Add artisan db:table command

* chore: Improve unit test

* chore: Add command option alias

---------

Co-authored-by: Wenbo Han <[email protected]>
  • Loading branch information
almas1992 and hwbrzzl authored Dec 28, 2024
1 parent bd23615 commit 51df61c
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 2 deletions.
1 change: 1 addition & 0 deletions console/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
5 changes: 3 additions & 2 deletions contracts/console/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ const (
)

type Extend struct {
Category string
Flags []Flag
ArgsUsage string
Category string
Flags []Flag
}

type Flag interface {
Expand Down
160 changes: 160 additions & 0 deletions database/console/table_command.go
Original file line number Diff line number Diff line change
@@ -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: " [--] [<table>]",
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("<fg=green;op=bold>%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("<fg=green;op=bold>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 <fg=gray>%s</>", key, strings.Join(attributes, ", "))
if columns[i].Default != "" {
value = fmt.Sprintf("<fg=gray>%s</> %s", columns[i].Default, value)
}
ctx.TwoColumnDetail(key, value)
}
}
if len(indexes) > 0 {
ctx.NewLine()
ctx.TwoColumnDetail("<fg=green;op=bold>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 <fg=gray>%s</>", indexes[i].Name, strings.Join(indexes[i].Columns, ", ")), strings.Join(attributes, ", "))
}
}
if len(foreignKeys) > 0 {
ctx.NewLine()
ctx.TwoColumnDetail("<fg=green;op=bold>Foreign Key</>", "On Update / On Delete")
for i := range foreignKeys {
key := fmt.Sprintf("%s <fg=gray>%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()
}
196 changes: 196 additions & 0 deletions database/console/table_command_test.go
Original file line number Diff line number Diff line change
@@ -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{
{"<fg=green;op=bold>test</>", ""},
{"Columns", "1"},
{"Size", "0.000MiB"},
{"<fg=green;op=bold>Column</>", "Type"},
{"foo <fg=gray>autoincrement, int, nullable</>", "<fg=gray>bar</> int"},
{"<fg=green;op=bold>Index</>", ""},
{"index_foo <fg=gray>foo, bar</>", "compound, unique, primary"},
{"<fg=green;op=bold>Foreign Key</>", "On Update / On Delete"},
{"fk_foo <fg=gray>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)
})
}

}
1 change: 1 addition & 0 deletions database/service_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})
Expand Down

0 comments on commit 51df61c

Please sign in to comment.