Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add artisan db:show command #787

Merged
merged 2 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions database/console/show_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
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"
"github.com/goravel/framework/contracts/database/schema"
"github.com/goravel/framework/support/str"
)

type ShowCommand struct {
config config.Config
schema schema.Schema
}

type databaseInfo struct {
Name string
Version string
Database string
Host string
Port string
Username string
OpenConnections string
Tables []schema.Table `gorm:"-"`
Views []schema.View `gorm:"-"`
}

type queryResult struct{ Value string }

func NewShowCommand(config config.Config, schema schema.Schema) *ShowCommand {
return &ShowCommand{
config: config,
schema: schema,
}
}

// Signature The name and signature of the console command.
func (r *ShowCommand) Signature() string {
return "db:show"
}

// Description The console command description.
func (r *ShowCommand) Description() string {
return "Display information about the given database"

Check warning on line 48 in database/console/show_command.go

View check run for this annotation

Codecov / codecov/patch

database/console/show_command.go#L47-L48

Added lines #L47 - L48 were not covered by tests
}

// Extend The console command extend.
func (r *ShowCommand) Extend() command.Extend {
return command.Extend{
Category: "db",
Flags: []command.Flag{
&command.StringFlag{
Name: "database",
Aliases: []string{"d"},
Usage: "The database connection",
},
&command.BoolFlag{
Name: "views",
Aliases: []string{"v"},
Usage: "Show the database views",
},
},
}

Check warning on line 67 in database/console/show_command.go

View check run for this annotation

Codecov / codecov/patch

database/console/show_command.go#L52-L67

Added lines #L52 - L67 were not covered by tests
}

// Handle Execute the console command.
func (r *ShowCommand) Handle(ctx console.Context) error {
if got := ctx.Argument(0); len(got) > 0 {
ctx.Error(fmt.Sprintf("No arguments expected for '%s' command, got '%s'.", r.Signature(), got))
return nil
}
r.schema = r.schema.Connection(ctx.Option("database"))
connection := r.schema.GetConnection()
getConfigValue := func(k string) string {
return r.config.GetString("database.connections." + connection + "." + k)
}
info := databaseInfo{
Database: getConfigValue("database"),
Host: getConfigValue("host"),
Port: getConfigValue("port"),
Username: getConfigValue("username"),
}
var err error
info.Name, info.Version, info.OpenConnections, err = r.getDataBaseInfo()
if err != nil {
ctx.Error(err.Error())
return nil
}

Check warning on line 92 in database/console/show_command.go

View check run for this annotation

Codecov / codecov/patch

database/console/show_command.go#L90-L92

Added lines #L90 - L92 were not covered by tests
if info.Tables, err = r.schema.GetTables(); err != nil {
ctx.Error(err.Error())
return nil
}
if ctx.OptionBool("views") {
if info.Views, err = r.schema.GetViews(); err != nil {
ctx.Error(err.Error())
return nil
}
}
r.display(ctx, info)
return nil
}

func (r *ShowCommand) getDataBaseInfo() (name, version, openConnections string, err error) {
var (
drivers = map[database.Driver]struct {
name string
versionQuery string
openConnectionsQuery string
}{
database.DriverSqlite: {
name: "SQLite",
versionQuery: "SELECT sqlite_version() AS value;",
},
database.DriverMysql: {
name: "MySQL",
versionQuery: "SELECT VERSION() AS value;",
openConnectionsQuery: "SHOW status WHERE variable_name = 'threads_connected';",
},
database.DriverPostgres: {
name: "PostgresSQL",
versionQuery: "SELECT current_setting('server_version') AS value;",
openConnectionsQuery: "SELECT COUNT(*) AS value FROM pg_stat_activity;",
},
database.DriverSqlserver: {
name: "SQL Server",
versionQuery: "SELECT SERVERPROPERTY('productversion') AS value;",
openConnectionsQuery: "SELECT COUNT(*) Value FROM sys.dm_exec_sessions WHERE status = 'running';",
},
}
)
name = string(r.schema.Orm().Query().Driver())
if driver, ok := drivers[r.schema.Orm().Query().Driver()]; ok {
name = driver.name
var versionResult queryResult
if err = r.schema.Orm().Query().Raw(driver.versionQuery).Scan(&versionResult); err == nil {
version = versionResult.Value
if strings.Contains(version, "MariaDB") {
name = "MariaDB"
}
if len(driver.openConnectionsQuery) > 0 {
var openConnectionsResult queryResult
if err = r.schema.Orm().Query().Raw(driver.openConnectionsQuery).Scan(&openConnectionsResult); err == nil {
openConnections = openConnectionsResult.Value
}
}
}
}
return
}

func (r *ShowCommand) display(ctx console.Context, info databaseInfo) {
ctx.NewLine()
ctx.TwoColumnDetail(fmt.Sprintf("<fg=green;op=bold>%s</>", info.Name), info.Version)
ctx.TwoColumnDetail("Database", info.Database)
ctx.TwoColumnDetail("Host", info.Host)
ctx.TwoColumnDetail("Port", info.Port)
ctx.TwoColumnDetail("Username", info.Username)
ctx.TwoColumnDetail("Open Connections", info.OpenConnections)
ctx.TwoColumnDetail("Tables", fmt.Sprintf("%d", len(info.Tables)))
if size := func() (size int) {
for i := range info.Tables {
size += info.Tables[i].Size
}
return
}(); size > 0 {
ctx.TwoColumnDetail("Total Size", fmt.Sprintf("%.3fMiB", float64(size)/1024/1024))
}
ctx.NewLine()
if len(info.Tables) > 0 {
ctx.TwoColumnDetail("<fg=green;op=bold>Tables</>", "<fg=yellow;op=bold>Size (MiB)</>")
for i := range info.Tables {
ctx.TwoColumnDetail(info.Tables[i].Name, fmt.Sprintf("%.3f", float64(info.Tables[i].Size)/1024/1024))
}
ctx.NewLine()
}
if len(info.Views) > 0 {
ctx.TwoColumnDetail("<fg=green;op=bold>Views</>", "<fg=yellow;op=bold>Rows</>")
for i := range info.Views {
if !str.Of(info.Views[i].Name).StartsWith("pg_catalog", "information_schema", "spt_") {
var rows int64
if err := r.schema.Orm().Query().Table(info.Views[i].Name).Count(&rows); err != nil {
ctx.Error(err.Error())
return
}

Check warning on line 188 in database/console/show_command.go

View check run for this annotation

Codecov / codecov/patch

database/console/show_command.go#L186-L188

Added lines #L186 - L188 were not covered by tests
ctx.TwoColumnDetail(info.Views[i].Name, fmt.Sprintf("%d", rows))
}
}
ctx.NewLine()
}
}
175 changes: 175 additions & 0 deletions database/console/show_command_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package console

import (
"io"
"testing"

"github.com/stretchr/testify/assert"

"github.com/goravel/framework/contracts/database"
"github.com/goravel/framework/contracts/database/schema"
mocksconfig "github.com/goravel/framework/mocks/config"
mocksconsole "github.com/goravel/framework/mocks/console"
mocksorm "github.com/goravel/framework/mocks/database/orm"
mocksschema "github.com/goravel/framework/mocks/database/schema"
"github.com/goravel/framework/support/color"
)

func TestShowCommand(t *testing.T) {
var (
mockContext *mocksconsole.Context
mockConfig *mocksconfig.Config
mockSchema *mocksschema.Schema
mockOrm *mocksorm.Orm
mockQuery *mocksorm.Query
)

beforeEach := func() {
mockContext = mocksconsole.NewContext(t)
mockConfig = mocksconfig.NewConfig(t)
mockSchema = mocksschema.NewSchema(t)
mockOrm = mocksorm.NewOrm(t)
mockQuery = mocksorm.NewQuery(t)
}
successCaseExpected := [][2]string{
{"<fg=green;op=bold>MariaDB</>", "MariaDB"},
{"Database", "db"},
{"Host", "host"},
{"Port", "port"},
{"Username", "username"},
{"Open Connections", "2"},
{"Tables", "1"},
{"Total Size", "0.000MiB"},
{"<fg=green;op=bold>Tables</>", "<fg=yellow;op=bold>Size (MiB)</>"},
{"test", "0.000"},
{"<fg=green;op=bold>Views</>", "<fg=yellow;op=bold>Rows</>"},
{"test", "0"},
}
tests := []struct {
name string
setup func()
expected string
}{
{
name: "invalid argument",
setup: func() {
mockContext.EXPECT().Argument(0).Return("test").Once()
mockContext.EXPECT().Error("No arguments expected for 'db:show' command, got 'test'.").Run(func(message string) {
color.Errorln(message)
}).Once()
},
expected: "No arguments expected for 'db:show' command, got 'test'.",
},
{
name: "get tables failed",
setup: func() {
mockContext.EXPECT().Argument(0).Return("").Once()
mockContext.EXPECT().Option("database").Return("test").Once()
mockSchema.EXPECT().Connection("test").Return(mockSchema).Once()
mockSchema.EXPECT().GetConnection().Return("test").Once()
mockConfig.EXPECT().GetString("database.connections.test.database").Return("db").Once()
mockConfig.EXPECT().GetString("database.connections.test.host").Return("host").Once()
mockConfig.EXPECT().GetString("database.connections.test.port").Return("port").Once()
mockConfig.EXPECT().GetString("database.connections.test.username").Return("username").Once()
mockQuery.EXPECT().Driver().Return(database.DriverMysql).Twice()
mockOrm.EXPECT().Query().Return(mockQuery).Times(4)
mockSchema.EXPECT().Orm().Return(mockOrm).Times(4)
mockQuery.EXPECT().Raw("SELECT VERSION() AS value;").Return(mockQuery).Once()
mockQuery.EXPECT().Raw("SHOW status WHERE variable_name = 'threads_connected';").Return(mockQuery).Once()
mockQuery.EXPECT().Scan(&queryResult{}).Return(nil).Twice()
mockSchema.EXPECT().GetTables().Return(nil, assert.AnError).Once()
mockContext.EXPECT().Error(assert.AnError.Error()).Run(func(message string) {
color.Errorln(message)
}).Once()
},
expected: assert.AnError.Error(),
}, {
name: "get views failed",
setup: func() {
mockContext.EXPECT().Argument(0).Return("").Once()
mockContext.EXPECT().Option("database").Return("test").Once()
mockSchema.EXPECT().Connection("test").Return(mockSchema).Once()
mockSchema.EXPECT().GetConnection().Return("test").Once()
mockConfig.EXPECT().GetString("database.connections.test.database").Return("db").Once()
mockConfig.EXPECT().GetString("database.connections.test.host").Return("host").Once()
mockConfig.EXPECT().GetString("database.connections.test.port").Return("port").Once()
mockConfig.EXPECT().GetString("database.connections.test.username").Return("username").Once()
mockQuery.EXPECT().Driver().Return(database.DriverMysql).Twice()
mockOrm.EXPECT().Query().Return(mockQuery).Times(4)
mockSchema.EXPECT().Orm().Return(mockOrm).Times(4)
mockQuery.EXPECT().Raw("SELECT VERSION() AS value;").Return(mockQuery).Once()
mockQuery.EXPECT().Raw("SHOW status WHERE variable_name = 'threads_connected';").Return(mockQuery).Once()
mockQuery.EXPECT().Scan(&queryResult{}).Return(nil).Twice()
mockSchema.EXPECT().GetTables().Return(nil, nil).Once()
mockContext.EXPECT().OptionBool("views").Return(true).Once()
mockSchema.EXPECT().GetViews().Return(nil, assert.AnError)
mockContext.EXPECT().Error(assert.AnError.Error()).Run(func(message string) {
color.Errorln(message)
})
},
expected: assert.AnError.Error(),
}, {
name: "success",
almas1992 marked this conversation as resolved.
Show resolved Hide resolved
setup: func() {
mockContext.EXPECT().Argument(0).Return("").Once()
mockContext.EXPECT().Option("database").Return("test").Once()
mockSchema.EXPECT().Connection("test").Return(mockSchema).Once()
mockSchema.EXPECT().GetConnection().Return("test").Once()
mockConfig.EXPECT().GetString("database.connections.test.database").Return("db").Once()
mockConfig.EXPECT().GetString("database.connections.test.host").Return("host").Once()
mockConfig.EXPECT().GetString("database.connections.test.port").Return("port").Once()
mockConfig.EXPECT().GetString("database.connections.test.username").Return("username").Once()
mockQuery.EXPECT().Driver().Return(database.DriverMysql).Twice()
mockOrm.EXPECT().Query().Return(mockQuery).Times(5)
mockSchema.EXPECT().Orm().Return(mockOrm).Times(5)
mockQuery.EXPECT().Raw("SELECT VERSION() AS value;").Return(mockQuery).Once()
mockQuery.EXPECT().Raw("SHOW status WHERE variable_name = 'threads_connected';").Return(mockQuery).Once()
mockQuery.EXPECT().Scan(&queryResult{}).Run(func(dest interface{}) {
if d, ok := dest.(*queryResult); ok {
d.Value = "MariaDB"
}
}).Return(nil).Once()
mockQuery.EXPECT().Scan(&queryResult{}).Run(func(dest interface{}) {
if d, ok := dest.(*queryResult); ok {
d.Value = "2"
}
}).Return(nil).Once()
mockSchema.EXPECT().GetTables().Return([]schema.Table{
{Name: "test", Size: 100},
}, nil).Once()
mockContext.EXPECT().OptionBool("views").Return(true).Once()
mockSchema.EXPECT().GetViews().Return([]schema.View{
{Name: "test"},
}, nil).Once()
mockQuery.EXPECT().Table("test").Return(mockQuery).Once()
var rows int64
mockQuery.EXPECT().Count(&rows).Return(nil).Once()
mockContext.EXPECT().NewLine().Times(4)
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 := NewShowCommand(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 @@
console.NewSeedCommand(config, seeder),
console.NewSeederMakeCommand(),
console.NewFactoryMakeCommand(),
console.NewShowCommand(config, schema),

Check warning on line 115 in database/service_provider.go

View check run for this annotation

Codecov / codecov/patch

database/service_provider.go#L115

Added line #L115 was not covered by tests
console.NewWipeCommand(config, schema),
})
}
Expand Down
Loading