diff --git a/README.md b/README.md new file mode 100644 index 0000000..22a907c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# populatedb + +## Install + +```sh +go install github.com/haunt98/populatedb-go/cmd/populatedb@latest +``` + +## Run + +Example: + +```sh +populatedb g --dialect "mysql" --url "root:@tcp(localhost:4000)/production" --table "production_2022" --number 10000000 +``` diff --git a/cmd/populatedb/main.go b/cmd/populatedb/main.go index a3dd973..fadcb49 100644 --- a/cmd/populatedb/main.go +++ b/cmd/populatedb/main.go @@ -1,7 +1,8 @@ package main -import "fmt" +import "github.com/haunt98/populatedb-go/internal/cli" func main() { - fmt.Println("Hello, World!") + app := cli.NewApp() + app.Run() } diff --git a/go.mod b/go.mod index 8c473c9..1742da5 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/haunt98/populatedb-go go 1.18 require ( + github.com/brianvoe/gofakeit/v6 v6.19.0 github.com/go-sql-driver/mysql v1.6.0 github.com/k1LoW/tbls v1.56.6 github.com/make-go-great/color-go v0.4.1 diff --git a/go.sum b/go.sum index 88d5144..036ee7e 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/brianvoe/gofakeit/v6 v6.19.0 h1:g+yJ+meWVEsAmR+bV4mNM/eXI0N+0pZ3D+Mi+G5+YQo= +github.com/brianvoe/gofakeit/v6 v6.19.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/internal/cli/action.go b/internal/cli/action.go new file mode 100644 index 0000000..37f57cc --- /dev/null +++ b/internal/cli/action.go @@ -0,0 +1,40 @@ +package cli + +import ( + "log" + + "github.com/urfave/cli/v2" +) + +type action struct { + flags struct { + dialect string + url string + table string + numberRecord int + verbose bool + dryRun bool + } +} + +func (a *action) RunHelp(c *cli.Context) error { + return cli.ShowAppHelp(c) +} + +func (a *action) getFlags(c *cli.Context) { + a.flags.dialect = c.String(flagDialectName) + a.flags.url = c.String(flagURLName) + a.flags.table = c.String(flagTableName) + a.flags.numberRecord = c.Int(flagNumberRecordName) + + a.flags.verbose = c.Bool(flagVerboseName) + a.flags.dryRun = c.Bool(flagDryRunName) + + a.log("Flags %+v\n", a.flags) +} + +func (a *action) log(format string, v ...interface{}) { + if a.flags.verbose { + log.Printf(format, v...) + } +} diff --git a/internal/cli/action_generate.go b/internal/cli/action_generate.go new file mode 100644 index 0000000..4f09807 --- /dev/null +++ b/internal/cli/action_generate.go @@ -0,0 +1,28 @@ +package cli + +import ( + "fmt" + + "github.com/haunt98/populatedb-go/internal/populatedb" + "github.com/urfave/cli/v2" +) + +func (a *action) RunGenerate(c *cli.Context) error { + a.getFlags(c) + + populator, err := populatedb.NewPopulator( + a.flags.dialect, + a.flags.url, + a.flags.verbose, + a.flags.dryRun, + ) + if err != nil { + return fmt.Errorf("populatedb: failed to new populator: %w", err) + } + + if err := populator.Insert(c.Context, a.flags.table, a.flags.numberRecord); err != nil { + return fmt.Errorf("populatedb: failed to insert: %w", err) + } + + return nil +} diff --git a/internal/cli/app.go b/internal/cli/app.go index a09f7db..7dc38f1 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -20,6 +20,9 @@ const ( flagURLName = "url" flagURLUsage = "database url" + flagTableName = "table" + flagTableUsage = "table name to generate data" + flagNumberRecordName = "number" flagNumberRecordUsage = "number of record to generate" @@ -40,7 +43,52 @@ type App struct { } func NewApp() *App { - cliApp := &cli.App{} + a := &action{} + + cliApp := &cli.App{ + Name: name, + Usage: usage, + Commands: []*cli.Command{ + { + Name: commandGenerateName, + Aliases: commandGenerateAliases, + Usage: commandGenerateUsage, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: flagDialectName, + Usage: flagDialectUsage, + Required: true, + }, + &cli.StringFlag{ + Name: flagURLName, + Usage: flagURLUsage, + Required: true, + }, + &cli.StringFlag{ + Name: flagTableName, + Usage: flagTableUsage, + Required: true, + }, + &cli.IntFlag{ + Name: flagNumberRecordName, + Usage: flagNumberRecordUsage, + Required: true, + }, + &cli.BoolFlag{ + Name: flagVerboseName, + Aliases: flagVerboseAliases, + Usage: flagVerboseUsage, + }, + &cli.BoolFlag{ + Name: flagDryRunName, + Usage: flagDryRunUsage, + }, + }, + Action: a.RunGenerate, + }, + }, + Action: a.RunHelp, + } return &App{ cliApp: cliApp, diff --git a/internal/populatedb/database_type.go b/internal/populatedb/database_type.go new file mode 100644 index 0000000..9ccb3e4 --- /dev/null +++ b/internal/populatedb/database_type.go @@ -0,0 +1,73 @@ +package populatedb + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/brianvoe/gofakeit/v6" +) + +var ErrNotSupportDatabaseType = errors.New("not support database type") + +// varchar(123) +// timestamp +func ParseDatabaseType(databaseTypeStr string) (DatabaseType, error) { + switch { + case strings.HasPrefix(strings.ToLower(databaseTypeStr), "varchar"): + dtVarchar := DTVarchar{} + if _, err := fmt.Sscanf(databaseTypeStr, "varchar(%d)", &dtVarchar.Length); err != nil { + return nil, fmt.Errorf("fmt: failed to sscanf [%s]: %w", databaseTypeStr, err) + } + + return &dtVarchar, nil + case strings.HasPrefix(strings.ToLower(databaseTypeStr), "bigint"): + return &DTBigint{}, nil + case strings.HasPrefix(strings.ToLower(databaseTypeStr), "int"): + return &DTInt{}, nil + case strings.EqualFold(databaseTypeStr, "timestamp"): + return &DTTimestamp{}, nil + case strings.EqualFold(databaseTypeStr, "json"): + return &DTJSON{}, nil + default: + return nil, fmt.Errorf("not support database type [%s]: %w", databaseTypeStr, ErrNotSupportDatabaseType) + } +} + +type DatabaseType interface { + Generate() any +} + +type DTVarchar struct { + Length int +} + +func (dt *DTVarchar) Generate() any { + return gofakeit.LetterN(uint(dt.Length)) +} + +type DTTimestamp struct{} + +func (dt *DTTimestamp) Generate() any { + return time.Now() +} + +type DTBigint struct{} + +func (dt *DTBigint) Generate() any { + return gofakeit.Int64() +} + +type DTInt struct{} + +func (dt *DTInt) Generate() any { + return gofakeit.Int32() +} + +type DTJSON struct{} + +func (dt *DTJSON) Generate() any { + // TODO: need mock + return nil +} diff --git a/internal/populatedb/populatedb.go b/internal/populatedb/populatedb.go index 4f8a479..9fb0abe 100644 --- a/internal/populatedb/populatedb.go +++ b/internal/populatedb/populatedb.go @@ -1,10 +1,12 @@ package populatedb import ( + "context" "database/sql" "errors" "fmt" "net/url" + "strings" "time" "github.com/go-sql-driver/mysql" @@ -15,20 +17,35 @@ import ( const ( dialectMySQL = "mysql" + + stmtInsert = "INSERT INTO %s (%s) VALUES (%s);" ) -var ErrDialectNotSupport = errors.New("dialect not support ") +var ( + ErrNotSupportDialect = errors.New("not support dialect") + ErrTableNotExist = errors.New("table not exist") +) -type Populator interface{} +type Populator interface { + Insert(ctx context.Context, tableName string, numberRecord int) error +} type populator struct { db *sql.DB tblsSchema *tblsschema.Schema + tables map[string]*tblsschema.Table + verbose bool + dryRun bool } -func NewPopulator(dbDialect, dbURL string) (Populator, error) { +func NewPopulator( + dbDialect string, + dbURL string, + verbose bool, + dryRun bool, +) (Populator, error) { if dbDialect != dialectMySQL { - return nil, fmt.Errorf("not support [%s]: %w", dbDialect, ErrDialectNotSupport) + return nil, fmt.Errorf("not support [%s]: %w", dbDialect, ErrNotSupportDialect) } // https://go.dev/doc/tutorial/database-access @@ -62,8 +79,62 @@ func NewPopulator(dbDialect, dbURL string) (Populator, error) { return nil, fmt.Errorf("tbls: faield to analyze [%s]: %w", tblsURL, err) } + tables := make(map[string]*tblsschema.Table, len(tblsSchema.Tables)) + for _, table := range tblsSchema.Tables { + tables[table.Name] = table + } + return &populator{ db: db, tblsSchema: tblsSchema, + tables: tables, + verbose: verbose, + dryRun: dryRun, }, nil } + +func (p *populator) Insert(ctx context.Context, tableName string, numberRecord int) error { + table, ok := p.tables[tableName] + if !ok { + return fmt.Errorf("table [%s] not exist: %w", tableName, ErrTableNotExist) + } + + columnNames := make([]string, 0, len(table.Columns)) + questionMarks := make([]string, 0, len(table.Columns)) + argFns := make([]func() any, 0, len(table.Columns)) + for _, column := range table.Columns { + dt, err := ParseDatabaseType(column.Type) + if err != nil { + return fmt.Errorf("failed to parse database type [%s]: %w", column.Type, err) + } + + columnNames = append(columnNames, column.Name) + questionMarks = append(questionMarks, "?") + argFns = append(argFns, dt.Generate) + } + + queryInsert := fmt.Sprintf(stmtInsert, + tableName, + strings.Join(columnNames, ", "), + strings.Join(questionMarks, ", "), + ) + + for i := 0; i < numberRecord; i++ { + args := make([]any, 0, len(argFns)) + for _, argFn := range argFns { + args = append(args, argFn()) + } + + if p.verbose { + fmt.Println(i, queryInsert, args) + } + + if !p.dryRun { + if _, err := p.db.ExecContext(ctx, queryInsert, args...); err != nil { + return fmt.Errorf("database: failed to exec [%s]: %w", queryInsert, err) + } + } + } + + return nil +}