parent
7adb819c11
commit
cf9a9d7e66
|
@ -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
|
||||||
|
```
|
|
@ -1,7 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import "github.com/haunt98/populatedb-go/internal/cli"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println("Hello, World!")
|
app := cli.NewApp()
|
||||||
|
app.Run()
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module github.com/haunt98/populatedb-go
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/brianvoe/gofakeit/v6 v6.19.0
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/k1LoW/tbls v1.56.6
|
github.com/k1LoW/tbls v1.56.6
|
||||||
github.com/make-go-great/color-go v0.4.1
|
github.com/make-go-great/color-go v0.4.1
|
||||||
|
|
2
go.sum
2
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.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
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/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/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 v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
|
|
@ -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...)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -20,6 +20,9 @@ const (
|
||||||
flagURLName = "url"
|
flagURLName = "url"
|
||||||
flagURLUsage = "database url"
|
flagURLUsage = "database url"
|
||||||
|
|
||||||
|
flagTableName = "table"
|
||||||
|
flagTableUsage = "table name to generate data"
|
||||||
|
|
||||||
flagNumberRecordName = "number"
|
flagNumberRecordName = "number"
|
||||||
flagNumberRecordUsage = "number of record to generate"
|
flagNumberRecordUsage = "number of record to generate"
|
||||||
|
|
||||||
|
@ -40,7 +43,52 @@ type App struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp() *App {
|
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{
|
return &App{
|
||||||
cliApp: cliApp,
|
cliApp: cliApp,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
package populatedb
|
package populatedb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-sql-driver/mysql"
|
"github.com/go-sql-driver/mysql"
|
||||||
|
@ -15,20 +17,35 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dialectMySQL = "mysql"
|
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 {
|
type populator struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
tblsSchema *tblsschema.Schema
|
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 {
|
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
|
// 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)
|
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{
|
return &populator{
|
||||||
db: db,
|
db: db,
|
||||||
tblsSchema: tblsSchema,
|
tblsSchema: tblsSchema,
|
||||||
|
tables: tables,
|
||||||
|
verbose: verbose,
|
||||||
|
dryRun: dryRun,
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue