feat: simple populator

support varchar, bigint, int, timestamp
main
sudo pacman -Syu 2022-11-06 17:56:31 +07:00
parent 7adb819c11
commit cf9a9d7e66
No known key found for this signature in database
GPG Key ID: D6CB5C6C567C47B0
9 changed files with 286 additions and 7 deletions

15
README.md Normal file
View File

@ -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
```

View File

@ -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()
}

1
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

40
internal/cli/action.go Normal file
View File

@ -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...)
}
}

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}