From b9e8a45a7a04dd8d707349ba73afd1b372fdcb78 Mon Sep 17 00:00:00 2001 From: ginokent <29125616+ginokent@users.noreply.github.com> Date: Tue, 2 Jan 2024 06:17:09 +0900 Subject: [PATCH] feat: Add apply subcommand --- .envrc.sh | 3 +- README.md | 47 +++++++++++++--- internal/config/auto_approve.go | 20 +++++++ internal/config/config.go | 2 + internal/consts/cli.go | 3 ++ pkg/ddlctl/ddlctl.go | 15 +++++- pkg/ddlctl/ddlctl_apply.go | 96 +++++++++++++++++++++++++++++++++ pkg/ddlctl/ddlctl_diff.go | 19 +++++-- pkg/errors/errors.go | 3 +- 9 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 internal/config/auto_approve.go create mode 100644 pkg/ddlctl/ddlctl_apply.go diff --git a/.envrc.sh b/.envrc.sh index 903e1e7..4c74b46 100644 --- a/.envrc.sh +++ b/.envrc.sh @@ -2,8 +2,7 @@ # shellcheck disable=SC2148 # Define environment variables that are not referenced in the container. -REPO_ROOT=$(git rev-parse --show-toplevel) -export REPO_ROOT +export REPO_ROOT=$(git rev-parse --show-toplevel) export PATH="${REPO_ROOT}/.local/bin:${REPO_ROOT}/.bin:${PATH}" export DOCKER_BUILDKIT="1" export COMPOSE_DOCKER_CLI_BUILD="1" diff --git a/README.md b/README.md index a988d00..8669e1c 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ options: ```console $ ddlctl diff --help Usage: - ddlctl diff [options] --dialect --src --dst + ddlctl diff [options] --dialect Description: command "ddlctl diff" description @@ -186,20 +186,55 @@ options: show usage ``` +### `ddlctl apply` + +```console +$ ddlctl apply --help +Usage: + ddlctl apply [options] --dialect + +Description: + command "ddlctl apply" description + +options: + --lang (env: DDLCTL_LANGUAGE, default: go) + programming language to generate DDL + --dialect (env: DDLCTL_DIALECT, default: ) + SQL dialect to generate DDL + --column-tag-go (env: DDLCTL_COLUMN_TAG_GO, default: db) + column annotation key for Go struct tag + --ddl-tag-go (env: DDLCTL_DDL_TAG_GO, default: ddlctl) + DDL annotation key for Go struct tag + --pk-tag-go (env: DDLCTL_PK_TAG_GO, default: pk) + primary key annotation key for Go struct tag + --auto-approve (env: DDLCTL_AUTO_APPROVE, default: false) + auto approve + --help (default: false) + show usage +``` + ## TODO -- dialect - - `generate` subcommand +- `generate` subcommand + - dialect - [x] Support `mysql` - [x] Support `postgres` - [x] Support `cockroachdb` - [x] Support `spanner` - [ ] Support `sqlite3` - - `diff` subcommand + - lang + - [x] Support `go` +- `diff` subcommand + - dialect + - [ ] Support `mysql` + - [ ] Support `postgres` + - [x] Support `cockroachdb` + - [ ] Support `spanner` + - [ ] Support `sqlite3` +- `apply` subcommand + - dialect - [ ] Support `mysql` - [ ] Support `postgres` - [x] Support `cockroachdb` - [ ] Support `spanner` - [ ] Support `sqlite3` -- lang - - [x] Support `go` diff --git a/internal/config/auto_approve.go b/internal/config/auto_approve.go new file mode 100644 index 0000000..673c0c3 --- /dev/null +++ b/internal/config/auto_approve.go @@ -0,0 +1,20 @@ +package config + +import ( + "context" + + cliz "github.com/kunitsucom/util.go/exp/cli" + + "github.com/kunitsucom/ddlctl/internal/consts" +) + +func loadAutoApprove(_ context.Context, cmd *cliz.Command) bool { + v, _ := cmd.GetOptionBool(consts.OptionAutoApprove) + return v +} + +func AutoApprove() bool { + globalConfigMu.RLock() + defer globalConfigMu.RUnlock() + return globalConfig.AutoApprove +} diff --git a/internal/config/config.go b/internal/config/config.go index f5dac1c..5947a78 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -22,6 +22,7 @@ type config struct { Dialect string `json:"dialect"` Source string `json:"source"` Destination string `json:"destination"` + AutoApprove bool `json:"auto_approve"` // Golang ColumnTagGo string `json:"column_tag_go"` DDLTagGo string `json:"ddl_tag_go"` @@ -80,6 +81,7 @@ func load(ctx context.Context) (cfg *config, err error) { //nolint:unparam Dialect: loadDialect(ctx, cmd), Source: loadSource(ctx, cmd), Destination: loadDestination(ctx, cmd), + AutoApprove: loadAutoApprove(ctx, cmd), ColumnTagGo: loadColumnTagGo(ctx, cmd), DDLTagGo: loadDDLTagGo(ctx, cmd), PKTagGo: loadPKTagGo(ctx, cmd), diff --git a/internal/consts/cli.go b/internal/consts/cli.go index 79ff9cb..7ff67af 100644 --- a/internal/consts/cli.go +++ b/internal/consts/cli.go @@ -19,6 +19,9 @@ const ( OptionDestination = "dst" EnvKeyDestination = "DDLCTL_DESTINATION" + OptionAutoApprove = "auto-approve" + EnvKeyAutoApprove = "DDLCTL_AUTO_APPROVE" + // Golang OptionColumnTagGo = "column-tag-go" EnvKeyColumnTagGo = "DDLCTL_COLUMN_TAG_GO" diff --git a/pkg/ddlctl/ddlctl.go b/pkg/ddlctl/ddlctl.go index 4843ad2..089e733 100644 --- a/pkg/ddlctl/ddlctl.go +++ b/pkg/ddlctl/ddlctl.go @@ -90,10 +90,23 @@ func DDLCtl(ctx context.Context) error { }, { Name: "diff", - Usage: "ddlctl diff [options] --dialect --src --dst ", + Usage: "ddlctl diff [options] --dialect ", Options: opts, RunFunc: Diff, }, + { + Name: "apply", + Usage: "ddlctl apply [options] --dialect ", + Options: append(opts, + &cliz.BoolOption{ + Name: consts.OptionAutoApprove, + Environment: consts.EnvKeyAutoApprove, + Description: "auto approve", + Default: cliz.Default(false), + }, + ), + RunFunc: Apply, + }, }, Options: []cliz.Option{ &cliz.BoolOption{ diff --git a/pkg/ddlctl/ddlctl_apply.go b/pkg/ddlctl/ddlctl_apply.go new file mode 100644 index 0000000..76d828a --- /dev/null +++ b/pkg/ddlctl/ddlctl_apply.go @@ -0,0 +1,96 @@ +package ddlctl + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" + + sqlz "github.com/kunitsucom/util.go/database/sql" + errorz "github.com/kunitsucom/util.go/errors" + + "github.com/kunitsucom/ddlctl/internal/config" + "github.com/kunitsucom/ddlctl/internal/consts" + apperr "github.com/kunitsucom/ddlctl/pkg/errors" +) + +//nolint:cyclop,funlen +func Apply(ctx context.Context, args []string) error { + if _, err := config.Load(ctx); err != nil { + return errorz.Errorf("config.Load: %w", err) + } + + if len(args) != 2 { + return errorz.Errorf("args=%v: %w", args, apperr.ErrTwoArgumentsRequired) + } + + left, right, err := resolve(ctx, config.Dialect(), args[0], args[1]) + if err != nil { + return errorz.Errorf("resolve: %w", err) + } + + buf := new(strings.Builder) + if err := diff(buf, left, right); err != nil { + return errorz.Errorf("diff: %w", err) + } + + msg := ` + +ddlctl will exec the following DDL queries: + +-- 8< -- + +` + buf.String() + ` + +-- >8 -- + +Do you want to apply these DDL? + ddlctl will exec the DDL queries described above. + Only 'yes' will be accepted to approve. + +Enter a value: ` + + if _, err := os.Stdout.WriteString(msg); err != nil { + return errorz.Errorf("os.Stdout.WriteString: %w", err) + } + + if config.AutoApprove() { + if _, err := os.Stdout.WriteString(fmt.Sprintf("yes (via --%s option)\n", consts.OptionAutoApprove)); err != nil { + return errorz.Errorf("os.Stdout.WriteString: %w", err) + } + } else { + if err := prompt(); err != nil { + return errorz.Errorf("prompt: %w", err) + } + } + + os.Stdout.WriteString("\nexecuting...\n") + + db, err := sqlz.OpenContext(ctx, _postgres, args[0]) + if err != nil { + return errorz.Errorf("sqlz.OpenContext: %w", err) + } + defer db.Close() + + if _, err := db.ExecContext(ctx, buf.String()); err != nil { + return errorz.Errorf("db.ExecContext: %w", err) + } + + os.Stdout.WriteString("done\n") + + return nil +} + +func prompt() error { + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + userInput := scanner.Text() + + switch userInput { + case "yes", "YES": + return nil + default: + return errorz.Errorf("userInput=%s: %w", userInput, apperr.ErrUserCanceled) + } +} diff --git a/pkg/ddlctl/ddlctl_diff.go b/pkg/ddlctl/ddlctl_diff.go index fcc7352..eab7ad9 100644 --- a/pkg/ddlctl/ddlctl_diff.go +++ b/pkg/ddlctl/ddlctl_diff.go @@ -1,7 +1,9 @@ package ddlctl import ( + "bytes" "context" + "io" "os" "regexp" "strings" @@ -28,15 +30,24 @@ func Diff(ctx context.Context, args []string) error { return errorz.Errorf("config.Load: %w", err) } + if len(args) != 2 { + return errorz.Errorf("args=%v: %w", args, apperr.ErrTwoArgumentsRequired) + } + left, right, err := resolve(ctx, config.Dialect(), args[0], args[1]) if err != nil { return errorz.Errorf("resolve: %w", err) } - if err := diff(left, right); err != nil { + buf := bytes.NewBuffer(nil) + if err := diff(buf, left, right); err != nil { return errorz.Errorf("diff: %w", err) } + if _, err := io.Copy(os.Stdout, buf); err != nil { + return errorz.Errorf("io.Copy: %w", err) + } + return nil } @@ -134,7 +145,7 @@ func generateDDLForDiff(ctx context.Context, src string) (string, error) { } //nolint:cyclop -func diff(src, dst string) error { +func diff(out io.Writer, src, dst string) error { logs.Debug.Printf("src: %q", src) logs.Debug.Printf("dst: %q", dst) @@ -172,7 +183,9 @@ func diff(src, dst string) error { return errorz.Errorf("pgddl.Diff: %w", err) } - os.Stdout.WriteString(result.String()) + if _, err := io.WriteString(out, result.String()); err != nil { + return errorz.Errorf("io.WriteString: %w", err) + } return nil case "": diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index d1593ba..a8a43f2 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -4,9 +4,10 @@ import "errors" var ( ErrNotSupported = errors.New("not supported") + ErrUserCanceled = errors.New("user canceled") ErrDialectIsEmpty = errors.New("dialect is empty") ErrDDLTagGoAnnotationNotFoundInSource = errors.New("ddl-tag-go annotation not found in source") - ErrDiffRequiresTwoArguments = errors.New("diff requires two arguments") + ErrTwoArgumentsRequired = errors.New("two arguments required") ErrBothArgumentsIsDSN = errors.New("both arguments is dsn") ErrBothArgumentsAreNotDSNOrSQLFile = errors.New("both arguments are not dsn or sql file") )