diff --git a/.dockerignore b/.dockerignore index 3c64faf..3b58522 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,8 @@ ** !cmd -!console -!iofile -!migrator +!internal +!pkg +!fixtures !go.mod !go.sum !Makefile diff --git a/.env b/.env index 8ac31be..54d9e6c 100644 --- a/.env +++ b/.env @@ -1,19 +1,21 @@ +GO_IMAGE_VERSION="1.21" +GO_LINT_VERSION="1.55.2" POSTGRES_DB=docker POSTGRES_USER=docker POSTGRES_PASSWORD=docker POSTGRES_DSN=postgres://docker:docker@postgres:5432/docker?sslmode=disable -POSTGRES_MIGRATIONS_PATH=./db/postgresMigration/test_migrates +POSTGRES_MIGRATIONS_PATH=./fixtures/postgres CLICKHOUSE_DSN=clickhouse://default:@clickhouse:9000/docker?sslmode=disable&compress=true&debug=false -CLICKHOUSE_MIGRATIONS_PATH=./db/clickhouseMigration/test_migrates +CLICKHOUSE_MIGRATIONS_PATH=./fixtures/clickhouse CLICKHOUSE_CLUSTER_DSN1=clickhouse://default:@clickhouse1:9000/docker?sslmode=disable&compress=true&debug=false CLICKHOUSE_CLUSTER_DSN2=clickhouse://default:@clickhouse2:9000/docker?sslmode=disable&compress=true&debug=false CLICKHOUSE_CLUSTER_NAME=test_cluster -CLICKHOUSE_CLUSTER_MIGRATIONS_PATH=./db/clickhouseMigration/test_cluster_migrates +CLICKHOUSE_CLUSTER_MIGRATIONS_PATH=./fixtures/clickhouse_cluster MYSQL_DSN=mysql://docker:docker@tcp(mysql:3306)/docker MYSQL_ROOT_PASSWORD=docker-pw MYSQL_PASSWORD=docker MYSQL_USER=docker MYSQL_DATABASE=docker -MYSQL_MIGRATIONS_PATH=./db/mysqlMigration/test_migrates +MYSQL_MIGRATIONS_PATH=./fixtures//mysql diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..73d1542 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +**/mock* -diff +**/*.pb.go -diff \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31ef3cc..2f3a221 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,41 +10,53 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v1 - - - name: Cache go mods - uses: actions/cache@v2 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- + uses: actions/checkout@v3 +# +# - name: Cache go mods +# uses: actions/cache@v3 +# with: +# path: | +# ~/.cache/go-build +# ~/go/pkg/mod +# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - name: Pull containers run: docker-compose -f "docker-compose.yml" pull - - name: Cache containers - uses: satackey/action-docker-layer-caching@v0.0.11 - continue-on-error: true - - name: Start containers run: docker-compose -f "docker-compose.yml" up -d --build - - name: Sleep for 5 seconds - uses: jakejarvis/wait-action@master - with: - time: '5s' - - name: Run ps run: docker-compose -f "docker-compose.yml" ps - name: Run logs run: docker-compose -f "docker-compose.yml" logs - - name: Run tests - run: docker-compose -f "docker-compose.yml" exec -T app make test + - name: Run lint + run: docker-compose -f "docker-compose.yml" exec -T app make lint + + - name: Run unit tests + run: docker-compose -f "docker-compose.yml" exec -T app make test-unit + + - name: Sleep for 10 seconds + uses: jakejarvis/wait-action@master + with: + time: '10s' + + - name: Run integration tests + run: docker-compose -f "docker-compose.yml" exec -T app make test-integration + +# - name: Publish Test Report +# uses: mikepenz/action-junit-report@v4 +# if: success() || failure() +# with: +# report_paths: '**/.report/*.xml' + +# - name: Test & publish code coverage +# uses: paambaati/codeclimate-action@v5.0.0 +# with: +# coverageLocations: | +# ${{github.workspace}}/.report/*.json:cobertura - name: Stop containers if: always() diff --git a/.gitignore b/.gitignore index 322f178..fbaac04 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .idea .build -.coverage build.sh +.report diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..78d7c81 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,220 @@ +# $ golangci-lint run --config=~/.golangci.yml ./... > lint.txt + +run: + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 7m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + + # include test files or not, default is true + tests: false + + # list of build tags, all linters use it. Default is empty list. + # build-tags: + # - mytag + + # which dirs to skip: issues from them won't be reported; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but default dirs are skipped independently + # from this option's value (see skip-dirs-use-default). + # "/" will be replaced by current OS file path separator to properly work + # on Windows. + skip-dirs: + - doc + - docker + + # default is true. Enables skipping of directories: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + # "/" will be replaced by current OS file path separator to properly work + # on Windows. + skip-files: + - \.pb\.go$ + - \.pb\.gw\.go$ + + # - ".*\\.my\\.go$" + # - lib/bad.go + + # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + # modules-download-mode: readonly|vendor|mod + + # Allow multiple parallel golangci-lint instances running. + # If false (default) - golangci-lint acquires file lock on start. + allow-parallel-runners: true + +linters: + enable: + # Enabled by default linters: + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases + - gosimple # Linter for Go source code that specializes in simplifying code + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - ineffassign # Detects when assignments to existing variables are not used + - staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code + - unused # Checks Go code for unused constants, variables, functions and types + + # Disabled by default linters: + - bodyclose # checks whether HTTP response body is closed successfully + - durationcheck # : check for two durations multiplied together [fast: false, auto-fix: false] + - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. + - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error. + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - exportloopref # checks for pointers to enclosing loop variables + - forcetypeassert # finds forced type assertions + - goconst # Finds repeated strings that could be replaced by a constant + - gocritic # Provides diagnostics that check for bugs, performance and style issues. + - goimports # In addition to fixing imports, goimports also formats your code in the same style as gofmt. + - goerr113 # Golang linter to check the errors handling expressions + - gomnd # An analyzer to detect magic numbers. + - gosec # Inspects source code for security problems + - ireturn # Accept Interfaces, Return Concrete Types + - importas # Enforces consistent import aliases + - makezero # Finds slice declarations with non-zero initial length + - nakedret # Finds naked returns in functions greater than a specified function length + - nestif # Reports deeply nested if statements + - nilerr # Finds the code that returns nil even if it checks that the error is not nil. + - nolintlint # Reports ill-formed or insufficient nolint directives + - prealloc # Finds slice declarations that could potentially be pre-allocated + - predeclared # find code that shadows one of Go's predeclared identifiers + - promlinter # Check Prometheus metrics naming via promlint + - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. + - stylecheck # Stylecheck is a replacement for golint + - misspell # Finds commonly misspelled English words in comments + - unparam # Reports unused function parameters + - unconvert # Remove unnecessary type conversions + - whitespace # Tool for detection of leading and trailing whitespace + disable: + - godot # Check if comments end in a period + - asasalint # check for pass []any as any in variadic func(...any) + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers + - bidichk # Checks for dangerous unicode character sequences + - containedctx # containedctx is a linter that detects struct contained context.Context field + - contextcheck # check the function whether use a non-inherited context + - cyclop # checks function and package cyclomatic complexity + - decorder # check declaration order and count of types, constants, variables and functions + - depguard # Go linter that checks if package imports are in a list of acceptable packages + - dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) + - execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds + - exhaustive # check exhaustiveness of enum switch statements + - exhaustruct # Checks if all structure fields are initialized + - forbidigo # Forbids identifiers + - funlen # Tool for detection of long functions + - gci # Gci controls golang package import order and makes it always deterministic. + - gochecknoglobals # check that no global variables exist + - gochecknoinits # Checks that no init functions are present in Go code + - gocognit # Computes and checks the cognitive complexity of functions + - gocyclo # Computes and checks the cyclomatic complexity of functions + - godox # Tool for detection of FIXME, TODO and other comment keywords + - goheader # Checks is file header matches to pattern + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification + - gofumpt # Gofumpt checks whether code was gofumpt-ed. + - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. + - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. + - goprintffuncname # Checks that printf-like functions are named with f at the end + - grouper # An analyzer to analyze expression groups. + - interfacebloat # A linter that checks the number of methods inside an interface. + - lll # Reports long lines + - maintidx # maintidx measures the maintainability index of each function. + - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. + - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity + - noctx # noctx finds sending http request without context.Context + - nonamedreturns # Reports all named returns + - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL. + - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test + - reassign # Checks that package variables are not reassigned + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. + - tagliatelle # Checks the struct tags. + - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 + - testpackage # linter that makes you use a separate _test package + - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes + - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library. + - varnamelen # checks that the length of a variable's name matches its scope + - wastedassign # wastedassign finds wasted assignment statements. + - wrapcheck # Checks that errors returned from external packages are wrapped + - wsl # Whitespace Linter - Forces you to use empty lines! + +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions + # default is "colored-line-number" + format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: false + + # print linter name in the end of issue text, default is true + print-linter-name: true + + # make issues output unique by line, default is true + # uniq-by-line: true + + # add a prefix to the output file references; default is no prefix + # path-prefix: "" + + # sorts results by: filepath, line and column + sort-results: false + +# all available settings of specific linters +linters-settings: + govet: + check-shadowing: true + + golint: + min-confidence: 0 + + dupl: + threshold: 100 + + goconst: + min-len: 2 + min-occurrences: 2 + + errcheck: + exclude-functions: + - io/ioutil.ReadFile + - io.Copy(*bytes.Buffer) + - io.Copy(os.Stdout) + - (io.ReadCloser).Close + + gocritic: + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + disabled-checks: + - importShadow + - emptyStringTest + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + + revive: + rules: + - name: package-comments + severity: warning + disabled: true + +issues: + exclude-use-default: true + exclude-rules: + - linters: + - govet + text: "declaration of \"err\" shadows declaration" + - linters: + - goerr113 + text: "do not define dynamic errors, use wrapped static errors instead" diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 0000000..92bd468 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,2 @@ +disable-version-string: True +with-expecter: True \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ffc54f1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +ARG GO_IMAGE_VERSION +FROM golang:${GO_IMAGE_VERSION}-alpine +RUN apk add --no-cache \ + tzdata \ + ca-certificates \ + make \ + build-base \ + git \ + bash \ + file \ + jq + +RUN export BINDIR=/go/bin \ + && export PATH=$PATH:$GOPATH/bin + +#RUN go install github.com/boumenot/gocover-cobertura@latest \ +# && go install github.com/jstemmer/go-junit-report@latest + +ARG GO_LINT_VERSION +RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v${GO_LINT_VERSION} + +RUN go env -w GOFLAGS=-buildvcs=false && \ + go env -w CGO_ENABLED=0 diff --git a/Makefile b/Makefile index 73f97d1..a1ceb2d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ SHELL = /bin/bash -e +.DEFAULT_GOAL=help BASEDIR=$(shell pwd) GIT_BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) GIT_COMMIT ?= $(shell git rev-parse --short HEAD) @@ -6,11 +7,8 @@ GIT_TAG ?= $(shell git describe --tags --abbrev=0 HEAD 2>/dev/null || echo "v0.0 VERSION = $(shell echo "${GIT_TAG}" | grep -Eo "v([0-9]+\.[0-9]+\.[0-9]+)" | cut -dv -f2) export ${VERSION} LDFLAGS=-ldflags "-s -w -X main.Version=${GIT_TAG} -X main.GitCommit=${GIT_COMMIT}" -COVERAGE_DIR ?= .coverage +REPORT_DIR ?= .report BUILD_DIR ?= .build -SOURCE_FILES ?= ./... -TEST_PATTERN ?= . -TEST_OPTIONS ?= PKG_NAME = db-migrator APTLY_BASE_URL ?= APTLY_REPO_MASTER ?= xenial @@ -23,7 +21,10 @@ DOCKER_ID_USER = raoptimus DOCKER_PASS ?= "" DOCKER_IMAGE = "${PKG_NAME}" -help: +help: ## Show help message + @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +version: @echo "VERSION: ${VERSION}" @echo "GIT_BRANCH: ${GIT_BRANCH}" @echo "GIT_TAG: ${GIT_TAG}" @@ -50,16 +51,31 @@ build: help @file ${BUILD_DIR}/${PKG_NAME} @du -h ${BUILD_DIR}/${PKG_NAME} -test: - @[ -d ${COVERAGE_DIR} ] || mkdir -p ${COVERAGE_DIR} - @go test $(TEST_OPTIONS) \ +test-coverage: ## Coverage + @[ -d ${REPORT_DIR} ] || mkdir -p ${REPORT_DIR} + @-go test \ + -cover \ + -covermode=atomic \ + -coverprofile=${REPORT_DIR}/coverage.txt \ + ./... \ + -timeout=2m \ + -v + +test-unit: ## Run only Unit tests + @go test $$(go list ./... | grep -v mock) \ + -buildvcs=false \ + -failfast \ + -short \ + -timeout=2m \ + -v + +test-integration: ## Run Integration Tests only + @go test $$(go list ./... | grep -v mock) \ + -buildvcs=false \ -failfast \ - -race \ - -coverpkg=./... \ - -covermode=atomic \ - -coverprofile=${COVERAGE_DIR}/coverage.txt $(SOURCE_FILES) \ - -run $(TEST_PATTERN) \ - -timeout=2m + -run Integration \ + -tags=integration \ + -v build-deb: build @echo "deb package $(PKG_NAME) building..." @@ -83,3 +99,25 @@ publish-deb: @curl --fail --connect-timeout 5 -X POST ${APTLY_BASE_URL}/api/repos/${APTLY_REPO}/file/debian/${PACKAGE_FILE} @curl --fail --connect-timeout 5 -X PUT ${APTLY_BASE_URL}/api/publish/filesystem:ci:${APTLY_PREFIX}/${APTLY_DIST} +lint: ## Run linter + @[ -d ${REPORT_DIR} ] || mkdir -p ${REPORT_DIR} + golangci-lint run --timeout 5m + +install-mockery: + @mockery --version &> /dev/null || go install github.com/vektra/mockery/v2@latest + @mockery --version + +gen-mocks: install-mockery ## Run mockery + @bash -c 'for d in $$(find . ! -path "**/mock*" ! -path "**/.*" -name "**.go" -exec dirname {} \; | sort --unique); do \ + if [[ "$$d" == "." ]]; then continue; fi; \ + pkg=mock$$(basename -- "$${d/./''}"); \ + mockery --srcpkg=$${d} --outpkg=$${pkg} --output=$${d}/$${pkg} --all --with-expecter=true; \ + done; \ + ' + +gen-mocks-dry-run: install-mockery ## Run mockery --dry-run=true + @bash -c 'for d in $$(find . ! -path "**/mock*" ! -path "**/.*" -name "**.go" -exec dirname {} \; | sort --unique); do \ + if [[ "$$d" == "." ]]; then continue; fi; \ + mockery --srcpkg=$${d} --output=$${d}/mock$$(basename -- "$${d/./''}") --all --dry-run=true; \ + done; \ + ' diff --git a/cmd/db-migrator/main.go b/cmd/db-migrator/main.go index 1fb08d1..9d942a3 100644 --- a/cmd/db-migrator/main.go +++ b/cmd/db-migrator/main.go @@ -1,153 +1,179 @@ /** * This file is part of the raoptimus/db-migrator.go library * - * @copyright Copyright (c) Evgeniy Urvantsev + * @copyright Copyright (c) Evgeniy Urvantsev * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md * @link https://github.com/raoptimus/db-migrator.go */ + package main import ( "fmt" + "os" + _ "github.com/lib/pq" - "github.com/raoptimus/db-migrator.go/console" - "github.com/raoptimus/db-migrator.go/migrator" + "github.com/raoptimus/db-migrator.go/internal/migrator" + "github.com/raoptimus/db-migrator.go/pkg/console" "github.com/urfave/cli/v2" - "log" - "net/http" - _ "net/http/pprof" - "os" ) var ( - Version string - GitCommit string - controller *migrator.Service + Version string + GitCommit string + dbService *migrator.DBService ) func main() { + options := migrator.Options{} + app := cli.NewApp() app.Name = "DB Service" app.Usage = "up/down/redo command for migrates the different db" app.Version = fmt.Sprintf("v%s.rev[%s]", Version, GitCommit) - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "dsn", - EnvVars: []string{"DSN"}, - Aliases: []string{"d"}, - //Value: "postgres://docker:docker@postgres:5432/docker?sslmode=disable", - Value: "clickhouse://default:@clickhouse:9000/docker?sslmode=disable&compress=true&debug=false", - Usage: "DB connection string", - }, - &cli.StringFlag{ - Name: "migrationPath", - EnvVars: []string{"MIGRATION_PATH"}, - Aliases: []string{"p"}, - Value: "./migrator/db/clickhouseMigration/test_migrates", - //Value: "./migrator/db/postgresMigration/test_migrates", - Usage: "Directory for migrated files", - }, - &cli.StringFlag{ - Name: "migrationTable", - EnvVars: []string{"MIGRATION_TABLE"}, - Aliases: []string{"t"}, - Value: "migration", - Usage: "Table name for history of migrates", - }, - &cli.StringFlag{ - Name: "migrationClusterName", - EnvVars: []string{"MIGRATION_CLUSTER_NAME"}, - Aliases: []string{"cn"}, - Value: "", - Usage: "Cluster name for history of migrates", - }, - &cli.BoolFlag{ - Name: "compact", - EnvVars: []string{"COMPACT"}, - Aliases: []string{"c"}, - Usage: "Indicates whether the console output should be compacted.", - Value: false, - }, - &cli.BoolFlag{ - Name: "interactive", - EnvVars: []string{"INTERACTIVE"}, - Aliases: []string{"i"}, - Usage: "Whether to run the command interactively", - Value: true, - }, + app.Flags = flags(&options) + app.Commands = commands(&options) + app.Before = func(context *cli.Context) error { + dbService = migrator.New(&options) + return nil + } + + if err := app.Run(os.Args); err != nil { + console.Std.Fatal(err) } - app.Commands = []*cli.Command{ +} + +func commands(options *migrator.Options) []*cli.Command { + return []*cli.Command{ { - Name: "up", - Action: func(c *cli.Context) error { - return controller.Up(c.Args().Get(0)) + Name: "up", + Flags: addsFlags(options), + Action: func(ctx *cli.Context) error { + if a, err := dbService.Upgrade(); err != nil { + return err + } else { + return a.Run(ctx) + } }, }, { - Name: "down", - Action: func(c *cli.Context) error { - return controller.Down(c.Args().Get(0)) + Name: "down", + Flags: addsFlags(options), + Action: func(ctx *cli.Context) error { + if a, err := dbService.Downgrade(); err != nil { + return err + } else { + return a.Run(ctx) + } }, }, { - Name: "redo", - Action: func(c *cli.Context) error { - return controller.Redo(c.Args().Get(0)) + Name: "redo", + Flags: addsFlags(options), + Action: func(ctx *cli.Context) error { + if a, err := dbService.Redo(); err != nil { + return err + } else { + return a.Run(ctx) + } }, }, { Name: "create", - Action: func(c *cli.Context) error { - return controller.CreateMigration(c.Args().Get(0)) + Action: func(ctx *cli.Context) error { + return dbService.Create().Run(ctx) }, }, { - Name: "history", - Action: func(c *cli.Context) error { - return controller.History(c.Args().Get(0)) + Name: "history", + Flags: addsFlags(options), + Action: func(ctx *cli.Context) error { + if a, err := dbService.History(); err != nil { + return err + } else { + return a.Run(ctx) + } }, }, { - Name: "new", - Action: func(c *cli.Context) error { - return controller.HistoryNew(c.Args().Get(0)) + Name: "new", + Flags: addsFlags(options), + Action: func(ctx *cli.Context) error { + if a, err := dbService.HistoryNew(); err != nil { + return err + } else { + return a.Run(ctx) + } }, }, { - Name: "to", - Action: func(c *cli.Context) error { - return controller.To(c.Args().Get(0)) + Name: "to", + Flags: addsFlags(options), + Action: func(ctx *cli.Context) error { + if a, err := dbService.To(); err != nil { + return err + } else { + return a.Run(ctx) + } }, }, } - app.Before = before - app.Action = func(c *cli.Context) error { - return controller.Up(c.Args().Get(0)) - } +} - if err := app.Run(os.Args); err != nil { - log.Fatal(console.Red(err)) +func addsFlags(options *migrator.Options) []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "dsn", + EnvVars: []string{"DSN"}, + Aliases: []string{"d"}, + Usage: "DB connection string", + Destination: &options.DSN, + Required: true, + }, } } -func before(c *cli.Context) error { - fmt.Println(c.Command.Name) - - if c.Bool("debug") { - go func() { - fmt.Println(http.ListenAndServe(":6060", nil)) - }() +func flags(options *migrator.Options) []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "migrationPath", + EnvVars: []string{"MIGRATION_PATH"}, + Aliases: []string{"p"}, + Value: "./migrations", + Usage: "Directory for migrated files", + Destination: &options.Directory, + }, + &cli.StringFlag{ + Name: "migrationTable", + EnvVars: []string{"MIGRATION_TABLE"}, + Aliases: []string{"t"}, + Value: "migration", + Usage: "Table name for history of migrates", + Destination: &options.TableName, + }, + &cli.StringFlag{ + Name: "migrationClusterName", + EnvVars: []string{"MIGRATION_CLUSTER_NAME"}, + Aliases: []string{"cn"}, + Value: "", + Usage: "Cluster name for history of migrates", + Destination: &options.ClusterName, + }, + &cli.BoolFlag{ + Name: "compact", + EnvVars: []string{"COMPACT"}, + Aliases: []string{"c"}, + Usage: "Indicates whether the console output should be compacted.", + Value: false, + Destination: &options.Compact, + }, + &cli.BoolFlag{ + Name: "interactive", + EnvVars: []string{"INTERACTIVE"}, + Aliases: []string{"i"}, + Usage: "Whether to run the command interactively", + Value: true, + Destination: &options.Interactive, + }, } - - var err error - controller, err = migrator.New(migrator.Options{ - DSN: c.String("dsn"), - Directory: c.String("migrationPath"), - TableName: c.String("migrationTable"), - ClusterName: c.String("migrationClusterName"), - Compact: c.Bool("compact"), - Interactive: c.Bool("interactive"), - }) - - return err } diff --git a/console/output.go b/console/output.go deleted file mode 100644 index 2f3882d..0000000 --- a/console/output.go +++ /dev/null @@ -1,38 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package console - -import "fmt" - -var ( - Black = Color("\033[1;30m%s\033[0m") - Red = Color("\033[1;31m%s\033[0m") - Green = Color("\033[1;32m%s\033[0m") - Yellow = Color("\033[1;33m%s\033[0m") - Purple = Color("\033[1;34m%s\033[0m") - Magenta = Color("\033[1;35m%s\033[0m") - Teal = Color("\033[1;36m%s\033[0m") - White = Color("\033[1;37m%s\033[0m") -) - -func Color(colorString string) func(...interface{}) string { - sprint := func(args ...interface{}) string { - return fmt.Sprintf(colorString, - fmt.Sprint(args...)) - } - - return sprint -} - -func NumberPlural(c int, one, many string) string { - if c > 1 { - return many - } - - return one -} diff --git a/docker-compose.yml b/docker-compose.yml index 536a85f..96c1d69 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.7" services: clickhouse: - image: yandex/clickhouse-server:21.11.3.6-alpine + image: clickhouse/clickhouse-server:23.11-alpine volumes: - "./docker/volume/clickhouse/dump:/docker-entrypoint-initdb.d/" @@ -19,7 +19,12 @@ services: - .env app: - image: raoptimus/go-magick:latest + build: + context: ./ + dockerfile: Dockerfile + args: + GO_IMAGE_VERSION: ${GO_IMAGE_VERSION} + GO_LINT_VERSION: ${GO_LINT_VERSION} working_dir: "/usr/src/app" command: "sleep infinity" depends_on: @@ -28,31 +33,38 @@ services: - clickhouse2 - postgres - mysql + links: + - clickhouse + - clickhouse1 + - clickhouse2 + - postgres + - mysql volumes: - "./:/usr/src/app" - "~/.cache/go-build:/root/.cache/go-build" - - "~/go/pkg/mod:/go/pkg/mod" + - "~/go/pkg/mod:/root/go/pkg/mod" env_file: - .env - zookeeper: - image: zookeeper + clickhouse-keeper: + image: clickhouse/clickhouse-keeper:23.11-alpine + restart: on-failure + volumes: + - "./docker/volume/clickhouse-cluster/config/keeper1/keeper_config.xml:/etc/clickhouse-keeper/keeper_config.xml" clickhouse1: - image: yandex/clickhouse-server:21.11.3.6-alpine + image: clickhouse/clickhouse-server:23.11-alpine restart: on-failure volumes: - "./docker/volume/clickhouse-cluster/dump:/docker-entrypoint-initdb.d/" - - "./docker/volume/clickhouse-cluster/config/config_1.xml:/etc/clickhouse-server/config.xml" - - "./docker/volume/clickhouse-cluster/config/config_replica.xml:/etc/clickhouse-server/clickhouse_replication_config.xml" + - "./docker/volume/clickhouse-cluster/config/clickhouse1:/etc/clickhouse-server/config.d/" depends_on: - - zookeeper + - clickhouse-keeper clickhouse2: - image: yandex/clickhouse-server:21.11.3.6-alpine + image: clickhouse/clickhouse-server:23.11-alpine restart: on-failure volumes: - - "./docker/volume/clickhouse-cluster/config/config_2.xml:/etc/clickhouse-server/config.xml" - - "./docker/volume/clickhouse-cluster/config/config_replica.xml:/etc/clickhouse-server/clickhouse_replication_config.xml" + - "./docker/volume/clickhouse-cluster/config/clickhouse2:/etc/clickhouse-server/config.d/" depends_on: - - zookeeper + - clickhouse-keeper diff --git a/docker/volume/clickhouse-cluster/config/clickhouse1/config.xml b/docker/volume/clickhouse-cluster/config/clickhouse1/config.xml new file mode 100644 index 0000000..7e52396 --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse1/config.xml @@ -0,0 +1,16 @@ + + + debug + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + 1000M + 3 + + 0.0.0.0 + 8123 + 9000 + + + /clickhouse/task_queue/ddl + + diff --git a/docker/volume/clickhouse-cluster/config/clickhouse1/macros.xml b/docker/volume/clickhouse-cluster/config/clickhouse1/macros.xml new file mode 100644 index 0000000..72ab6ba --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse1/macros.xml @@ -0,0 +1,10 @@ + + + /clickhouse/task_queue/ddl + + + 01 + clickhouse1 + test_cluster + + diff --git a/docker/volume/clickhouse-cluster/config/clickhouse1/remote_servers.xml b/docker/volume/clickhouse-cluster/config/clickhouse1/remote_servers.xml new file mode 100644 index 0000000..96064ab --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse1/remote_servers.xml @@ -0,0 +1,17 @@ + + + + + true + + clickhouse1 + 9000 + + + clickhouse2 + 9000 + + + + + diff --git a/docker/volume/clickhouse-cluster/config/clickhouse1/use_keeper.xml b/docker/volume/clickhouse-cluster/config/clickhouse1/use_keeper.xml new file mode 100644 index 0000000..07e2ea4 --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse1/use_keeper.xml @@ -0,0 +1,8 @@ + + + + clickhouse-keeper + 9181 + + + diff --git a/docker/volume/clickhouse-cluster/config/clickhouse2/config.xml b/docker/volume/clickhouse-cluster/config/clickhouse2/config.xml new file mode 100644 index 0000000..7e52396 --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse2/config.xml @@ -0,0 +1,16 @@ + + + debug + /var/log/clickhouse-server/clickhouse-server.log + /var/log/clickhouse-server/clickhouse-server.err.log + 1000M + 3 + + 0.0.0.0 + 8123 + 9000 + + + /clickhouse/task_queue/ddl + + diff --git a/docker/volume/clickhouse-cluster/config/clickhouse2/macros.xml b/docker/volume/clickhouse-cluster/config/clickhouse2/macros.xml new file mode 100644 index 0000000..6f53565 --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse2/macros.xml @@ -0,0 +1,10 @@ + + + /clickhouse/task_queue/ddl + + + 01 + clickhouse2 + test_cluster + + diff --git a/docker/volume/clickhouse-cluster/config/clickhouse2/remote_servers.xml b/docker/volume/clickhouse-cluster/config/clickhouse2/remote_servers.xml new file mode 100644 index 0000000..96064ab --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse2/remote_servers.xml @@ -0,0 +1,17 @@ + + + + + true + + clickhouse1 + 9000 + + + clickhouse2 + 9000 + + + + + diff --git a/docker/volume/clickhouse-cluster/config/clickhouse2/use_keeper.xml b/docker/volume/clickhouse-cluster/config/clickhouse2/use_keeper.xml new file mode 100644 index 0000000..07e2ea4 --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/clickhouse2/use_keeper.xml @@ -0,0 +1,8 @@ + + + + clickhouse-keeper + 9181 + + + diff --git a/docker/volume/clickhouse-cluster/config/config_1.xml b/docker/volume/clickhouse-cluster/config/config_1.xml deleted file mode 100644 index bf4b8ea..0000000 --- a/docker/volume/clickhouse-cluster/config/config_1.xml +++ /dev/null @@ -1,621 +0,0 @@ - - - - - guest - guest - - - - debug - /var/log/clickhouse-server/clickhouse-server.log - /var/log/clickhouse-server/clickhouse-server.err.log - 100M - 10 - - - - - - - - - - - - - - false - - false - - - https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 - - - - 8123 - 9000 - 9004 - - - - - - - /etc/clickhouse-server/server.crt - /etc/clickhouse-server/server.key - - /etc/clickhouse-server/dhparam.pem - none - true - true - sslv2,sslv3 - true - - - - true - true - sslv2,sslv3 - true - - - - RejectCertificateHandler - - - - - - - - - 9009 - - - - - - - - 0.0.0.0 - - - - - - - - - - - - 4096 - 3 - - - 100 - - - 0 - - - - 10000 - - - 0.9 - - - 4194304 - - - 0 - - - - - - 8589934592 - - - 5368709120 - - - - /var/lib/clickhouse/ - - - /var/lib/clickhouse/tmp/ - - - - - - /var/lib/clickhouse/user_files/ - - - /var/lib/clickhouse/access/ - - - users.xml - - - default - - - - - - default - - - - - - - - - true - - - /etc/clickhouse-server/clickhouse_replication_config.xml - - - - - - - - - - - - - - - - - - - - - 01 - clickhouse1 - - - - 3600 - - - - 3600 - - - 60 - - - - - - - - - - - - - system - query_log
- - toYYYYMM(event_date) - - - - - 7500 -
- - - - system - trace_log
- - toYYYYMM(event_date) - 7500 -
- - - - system - query_thread_log
- toYYYYMM(event_date) - 7500 -
- - - - - - - - system - metric_log
- 7500 - 1000 -
- - - - system - asynchronous_metric_log
- - 60000 -
- - - - - - - - - - - - *_dictionary.xml - - - - - - - - - - /clickhouse/task_queue/ddl - - - - - - - - - - - - - - - - click_cost - any - - 0 - 3600 - - - 86400 - 60 - - - - max - - 0 - 60 - - - 3600 - 300 - - - 86400 - 3600 - - - - - - /var/lib/clickhouse/format_schemas/ - - - - - - - -
diff --git a/docker/volume/clickhouse-cluster/config/config_2.xml b/docker/volume/clickhouse-cluster/config/config_2.xml deleted file mode 100644 index 3135cf7..0000000 --- a/docker/volume/clickhouse-cluster/config/config_2.xml +++ /dev/null @@ -1,621 +0,0 @@ - - - - - guest - guest - - - - debug - /var/log/clickhouse-server/clickhouse-server.log - /var/log/clickhouse-server/clickhouse-server.err.log - 100M - 10 - - - - - - - - - - - - - - false - - false - - - https://6f33034cfe684dd7a3ab9875e57b1c8d@o388870.ingest.sentry.io/5226277 - - - - 8123 - 9000 - 9004 - - - - - - - /etc/clickhouse-server/server.crt - /etc/clickhouse-server/server.key - - /etc/clickhouse-server/dhparam.pem - none - true - true - sslv2,sslv3 - true - - - - true - true - sslv2,sslv3 - true - - - - RejectCertificateHandler - - - - - - - - - 9009 - - - - - - - - 0.0.0.0 - - - - - - - - - - - - 4096 - 3 - - - 100 - - - 0 - - - - 10000 - - - 0.9 - - - 4194304 - - - 0 - - - - - - 8589934592 - - - 5368709120 - - - - /var/lib/clickhouse/ - - - /var/lib/clickhouse/tmp/ - - - - - - /var/lib/clickhouse/user_files/ - - - /var/lib/clickhouse/access/ - - - users.xml - - - default - - - - - - default - - - - - - - - - true - - - /etc/clickhouse-server/clickhouse_replication_config.xml - - - - - - - - - - - - - - - - - - - - - 01 - clickhouse2 - - - - 3600 - - - - 3600 - - - 60 - - - - - - - - - - - - - system - query_log
- - toYYYYMM(event_date) - - - - - 7500 -
- - - - system - trace_log
- - toYYYYMM(event_date) - 7500 -
- - - - system - query_thread_log
- toYYYYMM(event_date) - 7500 -
- - - - - - - - system - metric_log
- 7500 - 1000 -
- - - - system - asynchronous_metric_log
- - 60000 -
- - - - - - - - - - - - *_dictionary.xml - - - - - - - - - - /clickhouse/task_queue/ddl - - - - - - - - - - - - - - - - click_cost - any - - 0 - 3600 - - - 86400 - 60 - - - - max - - 0 - 60 - - - 3600 - 300 - - - 86400 - 3600 - - - - - - /var/lib/clickhouse/format_schemas/ - - - - - - - -
diff --git a/docker/volume/clickhouse-cluster/config/config_replica.xml b/docker/volume/clickhouse-cluster/config/config_replica.xml deleted file mode 100644 index 782762a..0000000 --- a/docker/volume/clickhouse-cluster/config/config_replica.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - true - - clickhouse1 - 9000 - - - clickhouse2 - 9000 - - - - - - - - zookeeper - 2181 - - - diff --git a/docker/volume/clickhouse-cluster/config/keeper1/keeper_config.xml b/docker/volume/clickhouse-cluster/config/keeper1/keeper_config.xml new file mode 100644 index 0000000..e0c97dd --- /dev/null +++ b/docker/volume/clickhouse-cluster/config/keeper1/keeper_config.xml @@ -0,0 +1,28 @@ + + + information + /var/log/clickhouse-keeper/clickhouse-keeper.log + /var/log/clickhouse-keeper/clickhouse-keeper.err.log + 1000M + 3 + + 0.0.0.0 + + 9181 + 1 + /var/lib/clickhouse/coordination/log + /var/lib/clickhouse/coordination/snapshots + + 10000 + 30000 + information + + + + 1 + clickhouse-keeper + 9234 + + + + diff --git a/migrator/db/clickhouseMigration/test_migrates/200905_192800_create_test_table.down.sql b/fixtures/clickhouse/200905_192800_create_test_table.down.sql similarity index 100% rename from migrator/db/clickhouseMigration/test_migrates/200905_192800_create_test_table.down.sql rename to fixtures/clickhouse/200905_192800_create_test_table.down.sql diff --git a/migrator/db/clickhouseMigration/test_migrates/200905_192800_create_test_table.up.sql b/fixtures/clickhouse/200905_192800_create_test_table.up.sql similarity index 100% rename from migrator/db/clickhouseMigration/test_migrates/200905_192800_create_test_table.up.sql rename to fixtures/clickhouse/200905_192800_create_test_table.up.sql diff --git a/migrator/db/clickhouseMigration/test_migrates/200922_210000_add_column_to_test_table.down.sql b/fixtures/clickhouse/200922_210000_add_column_to_test_table.down.sql similarity index 100% rename from migrator/db/clickhouseMigration/test_migrates/200922_210000_add_column_to_test_table.down.sql rename to fixtures/clickhouse/200922_210000_add_column_to_test_table.down.sql diff --git a/migrator/db/clickhouseMigration/test_migrates/200922_210000_add_column_to_test_table.up.sql b/fixtures/clickhouse/200922_210000_add_column_to_test_table.up.sql similarity index 100% rename from migrator/db/clickhouseMigration/test_migrates/200922_210000_add_column_to_test_table.up.sql rename to fixtures/clickhouse/200922_210000_add_column_to_test_table.up.sql diff --git a/fixtures/clickhouse/210327_230201_broken_migration.safe.down.sql b/fixtures/clickhouse/210327_230201_broken_migration.safe.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/fixtures/clickhouse/210327_230201_broken_migration.safe.up.sql b/fixtures/clickhouse/210327_230201_broken_migration.safe.up.sql new file mode 100644 index 0000000..7cb3078 --- /dev/null +++ b/fixtures/clickhouse/210327_230201_broken_migration.safe.up.sql @@ -0,0 +1 @@ +insert into test2 values (time) values (now()); diff --git a/migrator/db/clickhouseMigration/test_cluster_migrates/200905_192800_create_test_table.down.sql b/fixtures/clickhouse_cluster/200905_192800_create_test_table.down.sql similarity index 100% rename from migrator/db/clickhouseMigration/test_cluster_migrates/200905_192800_create_test_table.down.sql rename to fixtures/clickhouse_cluster/200905_192800_create_test_table.down.sql diff --git a/migrator/db/clickhouseMigration/test_cluster_migrates/200905_192800_create_test_table.up.sql b/fixtures/clickhouse_cluster/200905_192800_create_test_table.up.sql similarity index 85% rename from migrator/db/clickhouseMigration/test_cluster_migrates/200905_192800_create_test_table.up.sql rename to fixtures/clickhouse_cluster/200905_192800_create_test_table.up.sql index 484be0d..0f31a7c 100644 --- a/migrator/db/clickhouseMigration/test_cluster_migrates/200905_192800_create_test_table.up.sql +++ b/fixtures/clickhouse_cluster/200905_192800_create_test_table.up.sql @@ -9,6 +9,3 @@ ENGINE = ReplicatedMergeTree ( ) PARTITION BY toYYYYMM(time) ORDER BY (time, value); - -ALTER TABLE test ADD COLUMN value2 UInt8; - diff --git a/fixtures/clickhouse_cluster/200922_210000_add_column_to_test_table.down.sql b/fixtures/clickhouse_cluster/200922_210000_add_column_to_test_table.down.sql new file mode 100644 index 0000000..39d14df --- /dev/null +++ b/fixtures/clickhouse_cluster/200922_210000_add_column_to_test_table.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE test DROP COLUMN text; + diff --git a/fixtures/clickhouse_cluster/200922_210000_add_column_to_test_table.up.sql b/fixtures/clickhouse_cluster/200922_210000_add_column_to_test_table.up.sql new file mode 100644 index 0000000..cd1ce2a --- /dev/null +++ b/fixtures/clickhouse_cluster/200922_210000_add_column_to_test_table.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE test ADD COLUMN text String; + diff --git a/fixtures/clickhouse_cluster/210327_230201_broken_migration.safe.down.sql b/fixtures/clickhouse_cluster/210327_230201_broken_migration.safe.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/fixtures/clickhouse_cluster/210327_230201_broken_migration.safe.up.sql b/fixtures/clickhouse_cluster/210327_230201_broken_migration.safe.up.sql new file mode 100644 index 0000000..7cb3078 --- /dev/null +++ b/fixtures/clickhouse_cluster/210327_230201_broken_migration.safe.up.sql @@ -0,0 +1 @@ +insert into test2 values (time) values (now()); diff --git a/migrator/db/mysqlMigration/test_migrates/200905_192800_create_test_table.safe.down.sql b/fixtures/mysql/200905_192800_create_test_table.safe.down.sql similarity index 100% rename from migrator/db/mysqlMigration/test_migrates/200905_192800_create_test_table.safe.down.sql rename to fixtures/mysql/200905_192800_create_test_table.safe.down.sql diff --git a/migrator/db/mysqlMigration/test_migrates/200905_192800_create_test_table.safe.up.sql b/fixtures/mysql/200905_192800_create_test_table.safe.up.sql similarity index 100% rename from migrator/db/mysqlMigration/test_migrates/200905_192800_create_test_table.safe.up.sql rename to fixtures/mysql/200905_192800_create_test_table.safe.up.sql diff --git a/fixtures/mysql/200905_202800_create_test2_table.down.sql b/fixtures/mysql/200905_202800_create_test2_table.down.sql new file mode 100644 index 0000000..815195d --- /dev/null +++ b/fixtures/mysql/200905_202800_create_test2_table.down.sql @@ -0,0 +1 @@ +DROP TABLE test2; diff --git a/fixtures/mysql/200905_202800_create_test2_table.up.sql b/fixtures/mysql/200905_202800_create_test2_table.up.sql new file mode 100644 index 0000000..d748813 --- /dev/null +++ b/fixtures/mysql/200905_202800_create_test2_table.up.sql @@ -0,0 +1,4 @@ +CREATE TABLE test2 ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/migrator/db/mysqlMigration/test_migrates/210327_230201_broken_migration.safe.down.sql b/fixtures/mysql/210327_230201_broken_migration.safe.down.sql similarity index 100% rename from migrator/db/mysqlMigration/test_migrates/210327_230201_broken_migration.safe.down.sql rename to fixtures/mysql/210327_230201_broken_migration.safe.down.sql diff --git a/migrator/db/mysqlMigration/test_migrates/210327_230201_broken_migration.safe.up.sql b/fixtures/mysql/210327_230201_broken_migration.safe.up.sql similarity index 100% rename from migrator/db/mysqlMigration/test_migrates/210327_230201_broken_migration.safe.up.sql rename to fixtures/mysql/210327_230201_broken_migration.safe.up.sql diff --git a/migrator/db/postgresMigration/test_migrates/200905_192800_create_test_table.safe.down.sql b/fixtures/postgres/200905_192800_create_test_table.safe.down.sql similarity index 100% rename from migrator/db/postgresMigration/test_migrates/200905_192800_create_test_table.safe.down.sql rename to fixtures/postgres/200905_192800_create_test_table.safe.down.sql diff --git a/migrator/db/postgresMigration/test_migrates/200905_192800_create_test_table.safe.up.sql b/fixtures/postgres/200905_192800_create_test_table.safe.up.sql similarity index 100% rename from migrator/db/postgresMigration/test_migrates/200905_192800_create_test_table.safe.up.sql rename to fixtures/postgres/200905_192800_create_test_table.safe.up.sql diff --git a/migrator/db/postgresMigration/test_migrates/200905_202800_create_test_table_trigger.down.sql b/fixtures/postgres/200905_202800_create_test_table_trigger.down.sql similarity index 100% rename from migrator/db/postgresMigration/test_migrates/200905_202800_create_test_table_trigger.down.sql rename to fixtures/postgres/200905_202800_create_test_table_trigger.down.sql diff --git a/migrator/db/postgresMigration/test_migrates/200905_202800_create_test_table_trigger.up.sql b/fixtures/postgres/200905_202800_create_test_table_trigger.up.sql similarity index 100% rename from migrator/db/postgresMigration/test_migrates/200905_202800_create_test_table_trigger.up.sql rename to fixtures/postgres/200905_202800_create_test_table_trigger.up.sql diff --git a/migrator/db/postgresMigration/test_migrates/210327_230201_broken_migration.safe.down.sql b/fixtures/postgres/210327_230201_broken_migration.safe.down.sql similarity index 100% rename from migrator/db/postgresMigration/test_migrates/210327_230201_broken_migration.safe.down.sql rename to fixtures/postgres/210327_230201_broken_migration.safe.down.sql diff --git a/migrator/db/postgresMigration/test_migrates/210327_230201_broken_migration.safe.up.sql b/fixtures/postgres/210327_230201_broken_migration.safe.up.sql similarity index 100% rename from migrator/db/postgresMigration/test_migrates/210327_230201_broken_migration.safe.up.sql rename to fixtures/postgres/210327_230201_broken_migration.safe.up.sql diff --git a/go.mod b/go.mod index 2a52d70..26afec0 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,18 @@ module github.com/raoptimus/db-migrator.go -go 1.18 +go 1.20 require ( github.com/ClickHouse/clickhouse-go v1.5.4 - github.com/go-sql-driver/mysql v1.6.0 - github.com/lib/pq v1.10.7 - github.com/stretchr/testify v1.8.0 - github.com/urfave/cli/v2 v2.20.2 + github.com/go-sql-driver/mysql v1.7.1 + github.com/lib/pq v1.10.9 + github.com/pkg/errors v0.9.1 + github.com/raoptimus/db-migrator.go/pkg/console v0.0.0-00010101000000-000000000000 + github.com/raoptimus/db-migrator.go/pkg/iohelp v0.0.0-00010101000000-000000000000 + github.com/raoptimus/db-migrator.go/pkg/sqlio v0.0.0-00010101000000-000000000000 + github.com/raoptimus/db-migrator.go/pkg/timex v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli/v2 v2.25.7 ) require ( @@ -16,6 +21,14 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/stretchr/objx v0.5.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace ( + github.com/raoptimus/db-migrator.go/pkg/console => ./pkg/console + github.com/raoptimus/db-migrator.go/pkg/iohelp => ./pkg/iohelp + github.com/raoptimus/db-migrator.go/pkg/sqlio => ./pkg/sqlio + github.com/raoptimus/db-migrator.go/pkg/timex => ./pkg/timex +) diff --git a/go.sum b/go.sum index ebc9d12..571d3ee 100644 --- a/go.sum +++ b/go.sum @@ -10,27 +10,34 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= -github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= +github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/urfave/cli/v2 v2.20.2 h1:dKA0LUjznZpwmmbrc0pOgcLTEilnHeM8Av9Yng77gHM= -github.com/urfave/cli/v2 v2.20.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/action/common.go b/internal/action/common.go new file mode 100644 index 0000000..0f51a4d --- /dev/null +++ b/internal/action/common.go @@ -0,0 +1,18 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +const ( + minLimit = 1 + + noNewMigrationsFound = "No new migrations found. Your system is up-to-date." + migratedUpSuccessfully = "Migrated up successfully" + migrationWas = "migration was" + migrationsWere = "migrations were" +) diff --git a/internal/action/common_test.go b/internal/action/common_test.go new file mode 100644 index 0000000..adf0ed8 --- /dev/null +++ b/internal/action/common_test.go @@ -0,0 +1,29 @@ +package action + +import ( + "flag" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +var time230527213123 = time.Date( + 2023, 05, 27, + 21, 31, 23, + 0, time.UTC) + +func flagSet(t *testing.T, argument string) *flag.FlagSet { + flagSet := flag.NewFlagSet("test", 0) + err := flagSet.Parse([]string{argument}) + assert.NoError(t, err) + + return flagSet +} + +func cliContext(t *testing.T, argument string) *cli.Context { + flagSet := flagSet(t, argument) + + return cli.NewContext(nil, flagSet, nil) +} diff --git a/internal/action/create.go b/internal/action/create.go new file mode 100644 index 0000000..e2199b5 --- /dev/null +++ b/internal/action/create.go @@ -0,0 +1,96 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "fmt" + "os" + "regexp" + + "github.com/pkg/errors" + "github.com/raoptimus/db-migrator.go/pkg/timex" + "github.com/urfave/cli/v2" +) + +const fileModeExecutable = 0o755 + +var regexpFileName = regexp.MustCompile(`^[\w\\]+$`) + +type Create struct { + time timex.Time + file File + console Console + fileNameBuilder FileNameBuilder + migrationDir string +} + +func NewCreate( + time timex.Time, + file File, + console Console, + fileNameBuilder FileNameBuilder, + migrationDir string, +) *Create { + return &Create{ + time: time, + file: file, + console: console, + fileNameBuilder: fileNameBuilder, + migrationDir: migrationDir, + } +} + +func (c *Create) Run(ctx *cli.Context) error { + migrationName := ctx.Args().Get(0) + if !regexpFileName.MatchString(migrationName) { + return ErrInvalidFileName + } + + prefix := c.time.Now().Format("060102_150405") + version := prefix + "_" + migrationName + fileNameUp, _ := c.fileNameBuilder.Up(version, true) + fileNameDown, _ := c.fileNameBuilder.Down(version, true) + + question := fmt.Sprintf( + "Create new migration files: \n'%s' and \n'%s'?\n", + fileNameUp, + fileNameDown, + ) + if !c.console.Confirm(question) { + return nil + } + + if err := c.createDirectory(c.migrationDir); err != nil { + return err + } + + if err := c.file.Create(fileNameUp); err != nil { + return err + } + + if err := c.file.Create(fileNameDown); err != nil { + return err + } + + c.console.SuccessLn("New migration created successfully.") + + return nil +} + +func (c *Create) createDirectory(path string) error { + if ok, err := c.file.Exists(path); err != nil || ok { + return err + } + + if err := os.Mkdir(path, fileModeExecutable); err != nil { + return errors.Wrapf(err, "creating directory %s", path) + } + + return nil +} diff --git a/internal/action/create_test.go b/internal/action/create_test.go new file mode 100644 index 0000000..0018eeb --- /dev/null +++ b/internal/action/create_test.go @@ -0,0 +1,82 @@ +package action + +import ( + "testing" + "time" + + "github.com/raoptimus/db-migrator.go/internal/action/mockaction" + "github.com/raoptimus/db-migrator.go/pkg/timex" + "github.com/stretchr/testify/assert" +) + +func TestCreate_Run_ExpectedArguments_NoError(t *testing.T) { + tests := []struct { + name string + now time.Time + version string + fileNameUp string + fileNameDown string + safely bool + }{ + { + name: "safely is true", + now: time230527213123, + version: "230527_213123_init", + fileNameUp: "230527_213123_init.safe.up.sql", + fileNameDown: "230527_213123_init.safe.down.sql", + safely: true, + }, + { + name: "safely is false", + now: time230527213123, + version: "230527_213123_init", + fileNameUp: "230527_213123_init.safe.up.sql", + fileNameDown: "230527_213123_init.safe.down.sql", + safely: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tm := timex.New(func() time.Time { + return time230527213123 + }) + + f := mockaction.NewFile(t) + f.EXPECT(). + Exists("/tmp"). + Return(true, nil). + Once() + f.EXPECT(). + Create(tt.fileNameUp). + Return(nil). + Once() + f.EXPECT(). + Create(tt.fileNameDown). + Return(nil). + Once() + + c := mockaction.NewConsole(t) + expectedQuestion := "Create new migration files: \n" + + "'" + tt.fileNameUp + "' and \n" + + "'" + tt.fileNameDown + "'?\n" + c.EXPECT(). + Confirm(expectedQuestion). + Return(true) + c.EXPECT(). + SuccessLn("New migration created successfully.") + + fb := mockaction.NewFileNameBuilder(t) + fb.EXPECT(). + Up(tt.version, true). + Return(tt.fileNameUp, tt.safely) + fb.EXPECT(). + Down(tt.version, true). + Return(tt.fileNameDown, tt.safely) + + ctx := cliContext(t, "init") + create := NewCreate(tm, f, c, fb, "/tmp") + err := create.Run(ctx) + assert.NoError(t, err) + }) + } +} diff --git a/internal/action/dependency.go b/internal/action/dependency.go new file mode 100644 index 0000000..1c76ac5 --- /dev/null +++ b/internal/action/dependency.go @@ -0,0 +1,62 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "context" + + "github.com/raoptimus/db-migrator.go/internal/dal/entity" +) + +//go:generate mockery --name=Console --outpkg=mockaction --output=./mockaction +type Console interface { + Confirm(s string) bool + Info(message string) + InfoLn(message string) + Infof(message string, a ...any) + Success(message string) + SuccessLn(message string) + Successf(message string, a ...any) + Warn(message string) + WarnLn(message string) + Warnf(message string, a ...any) + Error(message string) + ErrorLn(message string) + Errorf(message string, a ...any) + Fatal(err error) + NumberPlural(count int, one, many string) string +} + +//go:generate mockery --name=File --outpkg=mockaction --output=./mockaction +type File interface { + Create(filename string) error + Exists(path string) (bool, error) +} + +//go:generate mockery --name=FileNameBuilder --outpkg=mockaction --output=./mockaction +type FileNameBuilder interface { + // Up builds a file name for migration update + Up(version string, forceSafely bool) (fname string, safely bool) + // Down builds a file name for migration downgrade + Down(version string, forceSafely bool) (fname string, safely bool) +} + +//go:generate mockery --name=MigrationService --outpkg=mockaction --output=./mockaction +type MigrationService interface { + // Migrations returns an entities of migrations + Migrations(ctx context.Context, limit int) (entity.Migrations, error) + // NewMigrations returns an entities of new migrations + //todo: domain.Migrations + NewMigrations(ctx context.Context, limit int) (entity.Migrations, error) + // ApplyFile applies new migration + //todo: domain.Migration + ApplyFile(ctx context.Context, entity *entity.Migration, fileName string, safely bool) error + // RevertFile reverts the migration + RevertFile(ctx context.Context, entity *entity.Migration, fileName string, safely bool) error +} diff --git a/internal/action/downgrade.go b/internal/action/downgrade.go new file mode 100644 index 0000000..16b08b4 --- /dev/null +++ b/internal/action/downgrade.go @@ -0,0 +1,96 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "fmt" + + "github.com/raoptimus/db-migrator.go/internal/args" + "github.com/raoptimus/db-migrator.go/internal/console" + "github.com/urfave/cli/v2" +) + +type Downgrade struct { + service MigrationService + fileNameBuilder FileNameBuilder + interactive bool +} + +func NewDowngrade( + service MigrationService, + fileNameBuilder FileNameBuilder, + interactive bool, +) *Downgrade { + return &Downgrade{ + service: service, + fileNameBuilder: fileNameBuilder, + interactive: interactive, + } +} + +func (d *Downgrade) Run(ctx *cli.Context) error { + limit, err := args.ParseStepStringOrDefault(ctx.Args().Get(0), minLimit) + if err != nil { + return err + } + + migrations, err := d.service.Migrations(ctx.Context, limit) + if err != nil { + return err + } + + migrationsCount := migrations.Len() + if migrationsCount == 0 { + console.SuccessLn("No migration has been done before.") + return nil + } + + console.Warnf( + "Total %d %s to be reverted: \n", + migrationsCount, + console.NumberPlural(migrationsCount, "migration", "migrations"), + ) + + printMigrations(migrations, false) + + reverted := 0 + question := fmt.Sprintf("RevertFile the above %d %s?", + migrationsCount, + console.NumberPlural(migrationsCount, "migration", "migrations"), + ) + if d.interactive && !console.Confirm(question) { + return nil + } + + for i := range migrations { + migration := &migrations[i] + fileName, safely := d.fileNameBuilder.Down(migration.Version, false) + + if err := d.service.RevertFile(ctx.Context, migration, fileName, safely); err != nil { + console.Errorf( + "%d from %d %s reverted.\n"+ + "Migration failed. The rest of the migrations are canceled.\n", + reverted, + migrationsCount, + console.NumberPlural(reverted, migrationWas, migrationsWere), + ) + return err + } + + reverted++ + } + + console.Successf( + "%d %s reverted\n", + migrationsCount, + console.NumberPlural(migrationsCount, migrationWas, migrationsWere), + ) + console.SuccessLn("Migrated down successfully\n") + return nil +} diff --git a/internal/action/errors.go b/internal/action/errors.go new file mode 100644 index 0000000..b1edbf1 --- /dev/null +++ b/internal/action/errors.go @@ -0,0 +1,17 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "errors" +) + +var ( + ErrInvalidFileName = errors.New("the migration name should contain letters, digits, underscore and/or backslash characters only") +) diff --git a/internal/action/helper.go b/internal/action/helper.go new file mode 100644 index 0000000..5d7049c --- /dev/null +++ b/internal/action/helper.go @@ -0,0 +1,27 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "github.com/raoptimus/db-migrator.go/internal/console" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" +) + +func printMigrations(migrations entity.Migrations, withTime bool) { + for _, migration := range migrations { + //todo: check len of version name + if withTime { + console.Infof("\t(%s) %s\n", migration.ApplyTimeFormat(), migration.Version) + } else { + console.Infof("\t%s\n", migration.Version) + } + } + + console.InfoLn("") +} diff --git a/internal/action/history.go b/internal/action/history.go new file mode 100644 index 0000000..2da4e92 --- /dev/null +++ b/internal/action/history.go @@ -0,0 +1,64 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "github.com/raoptimus/db-migrator.go/internal/args" + "github.com/raoptimus/db-migrator.go/internal/console" + "github.com/urfave/cli/v2" +) + +const defaultGetHistoryLimit = 10 + +type History struct { + service MigrationService +} + +func NewHistory( + service MigrationService, +) *History { + return &History{ + service: service, + } +} + +func (h *History) Run(ctx *cli.Context) error { + limit, err := args.ParseStepStringOrDefault(ctx.Args().Get(0), defaultGetHistoryLimit) + if err != nil { + return err + } + + migrations, err := h.service.Migrations(ctx.Context, limit) + if err != nil { + return err + } + + migrationsCount := migrations.Len() + if migrationsCount == 0 { + console.SuccessLn("No migration has been done before.") + return nil + } + + if limit > 0 { + console.Warnf( + "Showing the last %d %s: \n", + migrationsCount, + console.NumberPlural(migrationsCount, "migration", "migrations"), + ) + } else { + console.Warnf( + "Total %d %s been applied before: \n", + migrationsCount, + console.NumberPlural(migrationsCount, "migration has", "migrations have"), + ) + } + + printMigrations(migrations, true) + return nil +} diff --git a/internal/action/history_new.go b/internal/action/history_new.go new file mode 100644 index 0000000..47df318 --- /dev/null +++ b/internal/action/history_new.go @@ -0,0 +1,62 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "github.com/raoptimus/db-migrator.go/internal/args" + "github.com/raoptimus/db-migrator.go/internal/console" + "github.com/urfave/cli/v2" +) + +type HistoryNew struct { + service MigrationService +} + +func NewHistoryNew( + service MigrationService, +) *HistoryNew { + return &HistoryNew{ + service: service, + } +} + +func (h *HistoryNew) Run(ctx *cli.Context) error { + limit, err := args.ParseStepStringOrDefault(ctx.Args().Get(0), defaultGetHistoryLimit) + if err != nil { + return err + } + + migrations, err := h.service.NewMigrations(ctx.Context, limit) + if err != nil { + return err + } + + migrationsCount := migrations.Len() + if migrationsCount == 0 { + console.SuccessLn(noNewMigrationsFound) + return nil + } + + if limit > 0 { + console.Warnf( + "Showing the last %d %s: \n", + migrationsCount, + console.NumberPlural(migrationsCount, "migration", "migrations"), + ) + } else { + console.Warnf( + "Total %d %s been applied before: \n", + migrationsCount, + console.NumberPlural(migrationsCount, "migration has", "migrations have"), + ) + } + + printMigrations(migrations, true) + return nil +} diff --git a/internal/action/mockaction/Console.go b/internal/action/mockaction/Console.go new file mode 100644 index 0000000..e3a32b0 --- /dev/null +++ b/internal/action/mockaction/Console.go @@ -0,0 +1,592 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockaction + +import mock "github.com/stretchr/testify/mock" + +// Console is an autogenerated mock type for the Console type +type Console struct { + mock.Mock +} + +type Console_Expecter struct { + mock *mock.Mock +} + +func (_m *Console) EXPECT() *Console_Expecter { + return &Console_Expecter{mock: &_m.Mock} +} + +// Confirm provides a mock function with given fields: s +func (_m *Console) Confirm(s string) bool { + ret := _m.Called(s) + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(s) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Console_Confirm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Confirm' +type Console_Confirm_Call struct { + *mock.Call +} + +// Confirm is a helper method to define mock.On call +// - s string +func (_e *Console_Expecter) Confirm(s interface{}) *Console_Confirm_Call { + return &Console_Confirm_Call{Call: _e.mock.On("Confirm", s)} +} + +func (_c *Console_Confirm_Call) Run(run func(s string)) *Console_Confirm_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Confirm_Call) Return(_a0 bool) *Console_Confirm_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Console_Confirm_Call) RunAndReturn(run func(string) bool) *Console_Confirm_Call { + _c.Call.Return(run) + return _c +} + +// Error provides a mock function with given fields: message +func (_m *Console) Error(message string) { + _m.Called(message) +} + +// Console_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' +type Console_Error_Call struct { + *mock.Call +} + +// Error is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Error(message interface{}) *Console_Error_Call { + return &Console_Error_Call{Call: _e.mock.On("Error", message)} +} + +func (_c *Console_Error_Call) Run(run func(message string)) *Console_Error_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Error_Call) Return() *Console_Error_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Error_Call) RunAndReturn(run func(string)) *Console_Error_Call { + _c.Call.Return(run) + return _c +} + +// ErrorLn provides a mock function with given fields: message +func (_m *Console) ErrorLn(message string) { + _m.Called(message) +} + +// Console_ErrorLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ErrorLn' +type Console_ErrorLn_Call struct { + *mock.Call +} + +// ErrorLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) ErrorLn(message interface{}) *Console_ErrorLn_Call { + return &Console_ErrorLn_Call{Call: _e.mock.On("ErrorLn", message)} +} + +func (_c *Console_ErrorLn_Call) Run(run func(message string)) *Console_ErrorLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_ErrorLn_Call) Return() *Console_ErrorLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_ErrorLn_Call) RunAndReturn(run func(string)) *Console_ErrorLn_Call { + _c.Call.Return(run) + return _c +} + +// Errorf provides a mock function with given fields: message, a +func (_m *Console) Errorf(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Errorf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Errorf' +type Console_Errorf_Call struct { + *mock.Call +} + +// Errorf is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Errorf(message interface{}, a ...interface{}) *Console_Errorf_Call { + return &Console_Errorf_Call{Call: _e.mock.On("Errorf", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Errorf_Call) Run(run func(message string, a ...interface{})) *Console_Errorf_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Errorf_Call) Return() *Console_Errorf_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Errorf_Call) RunAndReturn(run func(string, ...interface{})) *Console_Errorf_Call { + _c.Call.Return(run) + return _c +} + +// Fatal provides a mock function with given fields: err +func (_m *Console) Fatal(err error) { + _m.Called(err) +} + +// Console_Fatal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fatal' +type Console_Fatal_Call struct { + *mock.Call +} + +// Fatal is a helper method to define mock.On call +// - err error +func (_e *Console_Expecter) Fatal(err interface{}) *Console_Fatal_Call { + return &Console_Fatal_Call{Call: _e.mock.On("Fatal", err)} +} + +func (_c *Console_Fatal_Call) Run(run func(err error)) *Console_Fatal_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(error)) + }) + return _c +} + +func (_c *Console_Fatal_Call) Return() *Console_Fatal_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Fatal_Call) RunAndReturn(run func(error)) *Console_Fatal_Call { + _c.Call.Return(run) + return _c +} + +// Info provides a mock function with given fields: message +func (_m *Console) Info(message string) { + _m.Called(message) +} + +// Console_Info_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Info' +type Console_Info_Call struct { + *mock.Call +} + +// Info is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Info(message interface{}) *Console_Info_Call { + return &Console_Info_Call{Call: _e.mock.On("Info", message)} +} + +func (_c *Console_Info_Call) Run(run func(message string)) *Console_Info_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Info_Call) Return() *Console_Info_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Info_Call) RunAndReturn(run func(string)) *Console_Info_Call { + _c.Call.Return(run) + return _c +} + +// InfoLn provides a mock function with given fields: message +func (_m *Console) InfoLn(message string) { + _m.Called(message) +} + +// Console_InfoLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InfoLn' +type Console_InfoLn_Call struct { + *mock.Call +} + +// InfoLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) InfoLn(message interface{}) *Console_InfoLn_Call { + return &Console_InfoLn_Call{Call: _e.mock.On("InfoLn", message)} +} + +func (_c *Console_InfoLn_Call) Run(run func(message string)) *Console_InfoLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_InfoLn_Call) Return() *Console_InfoLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_InfoLn_Call) RunAndReturn(run func(string)) *Console_InfoLn_Call { + _c.Call.Return(run) + return _c +} + +// Infof provides a mock function with given fields: message, a +func (_m *Console) Infof(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Infof_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Infof' +type Console_Infof_Call struct { + *mock.Call +} + +// Infof is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Infof(message interface{}, a ...interface{}) *Console_Infof_Call { + return &Console_Infof_Call{Call: _e.mock.On("Infof", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Infof_Call) Run(run func(message string, a ...interface{})) *Console_Infof_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Infof_Call) Return() *Console_Infof_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Infof_Call) RunAndReturn(run func(string, ...interface{})) *Console_Infof_Call { + _c.Call.Return(run) + return _c +} + +// NumberPlural provides a mock function with given fields: count, one, many +func (_m *Console) NumberPlural(count int, one string, many string) string { + ret := _m.Called(count, one, many) + + var r0 string + if rf, ok := ret.Get(0).(func(int, string, string) string); ok { + r0 = rf(count, one, many) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Console_NumberPlural_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NumberPlural' +type Console_NumberPlural_Call struct { + *mock.Call +} + +// NumberPlural is a helper method to define mock.On call +// - count int +// - one string +// - many string +func (_e *Console_Expecter) NumberPlural(count interface{}, one interface{}, many interface{}) *Console_NumberPlural_Call { + return &Console_NumberPlural_Call{Call: _e.mock.On("NumberPlural", count, one, many)} +} + +func (_c *Console_NumberPlural_Call) Run(run func(count int, one string, many string)) *Console_NumberPlural_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *Console_NumberPlural_Call) Return(_a0 string) *Console_NumberPlural_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Console_NumberPlural_Call) RunAndReturn(run func(int, string, string) string) *Console_NumberPlural_Call { + _c.Call.Return(run) + return _c +} + +// Success provides a mock function with given fields: message +func (_m *Console) Success(message string) { + _m.Called(message) +} + +// Console_Success_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Success' +type Console_Success_Call struct { + *mock.Call +} + +// Success is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Success(message interface{}) *Console_Success_Call { + return &Console_Success_Call{Call: _e.mock.On("Success", message)} +} + +func (_c *Console_Success_Call) Run(run func(message string)) *Console_Success_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Success_Call) Return() *Console_Success_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Success_Call) RunAndReturn(run func(string)) *Console_Success_Call { + _c.Call.Return(run) + return _c +} + +// SuccessLn provides a mock function with given fields: message +func (_m *Console) SuccessLn(message string) { + _m.Called(message) +} + +// Console_SuccessLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuccessLn' +type Console_SuccessLn_Call struct { + *mock.Call +} + +// SuccessLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) SuccessLn(message interface{}) *Console_SuccessLn_Call { + return &Console_SuccessLn_Call{Call: _e.mock.On("SuccessLn", message)} +} + +func (_c *Console_SuccessLn_Call) Run(run func(message string)) *Console_SuccessLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_SuccessLn_Call) Return() *Console_SuccessLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_SuccessLn_Call) RunAndReturn(run func(string)) *Console_SuccessLn_Call { + _c.Call.Return(run) + return _c +} + +// Successf provides a mock function with given fields: message, a +func (_m *Console) Successf(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Successf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Successf' +type Console_Successf_Call struct { + *mock.Call +} + +// Successf is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Successf(message interface{}, a ...interface{}) *Console_Successf_Call { + return &Console_Successf_Call{Call: _e.mock.On("Successf", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Successf_Call) Run(run func(message string, a ...interface{})) *Console_Successf_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Successf_Call) Return() *Console_Successf_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Successf_Call) RunAndReturn(run func(string, ...interface{})) *Console_Successf_Call { + _c.Call.Return(run) + return _c +} + +// Warn provides a mock function with given fields: message +func (_m *Console) Warn(message string) { + _m.Called(message) +} + +// Console_Warn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warn' +type Console_Warn_Call struct { + *mock.Call +} + +// Warn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Warn(message interface{}) *Console_Warn_Call { + return &Console_Warn_Call{Call: _e.mock.On("Warn", message)} +} + +func (_c *Console_Warn_Call) Run(run func(message string)) *Console_Warn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Warn_Call) Return() *Console_Warn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Warn_Call) RunAndReturn(run func(string)) *Console_Warn_Call { + _c.Call.Return(run) + return _c +} + +// WarnLn provides a mock function with given fields: message +func (_m *Console) WarnLn(message string) { + _m.Called(message) +} + +// Console_WarnLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WarnLn' +type Console_WarnLn_Call struct { + *mock.Call +} + +// WarnLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) WarnLn(message interface{}) *Console_WarnLn_Call { + return &Console_WarnLn_Call{Call: _e.mock.On("WarnLn", message)} +} + +func (_c *Console_WarnLn_Call) Run(run func(message string)) *Console_WarnLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_WarnLn_Call) Return() *Console_WarnLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_WarnLn_Call) RunAndReturn(run func(string)) *Console_WarnLn_Call { + _c.Call.Return(run) + return _c +} + +// Warnf provides a mock function with given fields: message, a +func (_m *Console) Warnf(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Warnf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warnf' +type Console_Warnf_Call struct { + *mock.Call +} + +// Warnf is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Warnf(message interface{}, a ...interface{}) *Console_Warnf_Call { + return &Console_Warnf_Call{Call: _e.mock.On("Warnf", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Warnf_Call) Run(run func(message string, a ...interface{})) *Console_Warnf_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Warnf_Call) Return() *Console_Warnf_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Warnf_Call) RunAndReturn(run func(string, ...interface{})) *Console_Warnf_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewConsole interface { + mock.TestingT + Cleanup(func()) +} + +// NewConsole creates a new instance of Console. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConsole(t mockConstructorTestingTNewConsole) *Console { + mock := &Console{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/action/mockaction/File.go b/internal/action/mockaction/File.go new file mode 100644 index 0000000..3cab560 --- /dev/null +++ b/internal/action/mockaction/File.go @@ -0,0 +1,127 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockaction + +import mock "github.com/stretchr/testify/mock" + +// File is an autogenerated mock type for the File type +type File struct { + mock.Mock +} + +type File_Expecter struct { + mock *mock.Mock +} + +func (_m *File) EXPECT() *File_Expecter { + return &File_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: filename +func (_m *File) Create(filename string) error { + ret := _m.Called(filename) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(filename) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// File_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type File_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - filename string +func (_e *File_Expecter) Create(filename interface{}) *File_Create_Call { + return &File_Create_Call{Call: _e.mock.On("Create", filename)} +} + +func (_c *File_Create_Call) Run(run func(filename string)) *File_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *File_Create_Call) Return(_a0 error) *File_Create_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *File_Create_Call) RunAndReturn(run func(string) error) *File_Create_Call { + _c.Call.Return(run) + return _c +} + +// Exists provides a mock function with given fields: path +func (_m *File) Exists(path string) (bool, error) { + ret := _m.Called(path) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { + return rf(path) + } + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(path) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(path) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// File_Exists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exists' +type File_Exists_Call struct { + *mock.Call +} + +// Exists is a helper method to define mock.On call +// - path string +func (_e *File_Expecter) Exists(path interface{}) *File_Exists_Call { + return &File_Exists_Call{Call: _e.mock.On("Exists", path)} +} + +func (_c *File_Exists_Call) Run(run func(path string)) *File_Exists_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *File_Exists_Call) Return(_a0 bool, _a1 error) *File_Exists_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *File_Exists_Call) RunAndReturn(run func(string) (bool, error)) *File_Exists_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewFile interface { + mock.TestingT + Cleanup(func()) +} + +// NewFile creates a new instance of File. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFile(t mockConstructorTestingTNewFile) *File { + mock := &File{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/action/mockaction/FileNameBuilder.go b/internal/action/mockaction/FileNameBuilder.go new file mode 100644 index 0000000..b86a8a7 --- /dev/null +++ b/internal/action/mockaction/FileNameBuilder.go @@ -0,0 +1,139 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockaction + +import mock "github.com/stretchr/testify/mock" + +// FileNameBuilder is an autogenerated mock type for the FileNameBuilder type +type FileNameBuilder struct { + mock.Mock +} + +type FileNameBuilder_Expecter struct { + mock *mock.Mock +} + +func (_m *FileNameBuilder) EXPECT() *FileNameBuilder_Expecter { + return &FileNameBuilder_Expecter{mock: &_m.Mock} +} + +// Down provides a mock function with given fields: version, forceSafely +func (_m *FileNameBuilder) Down(version string, forceSafely bool) (string, bool) { + ret := _m.Called(version, forceSafely) + + var r0 string + var r1 bool + if rf, ok := ret.Get(0).(func(string, bool) (string, bool)); ok { + return rf(version, forceSafely) + } + if rf, ok := ret.Get(0).(func(string, bool) string); ok { + r0 = rf(version, forceSafely) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, bool) bool); ok { + r1 = rf(version, forceSafely) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// FileNameBuilder_Down_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Down' +type FileNameBuilder_Down_Call struct { + *mock.Call +} + +// Down is a helper method to define mock.On call +// - version string +// - forceSafely bool +func (_e *FileNameBuilder_Expecter) Down(version interface{}, forceSafely interface{}) *FileNameBuilder_Down_Call { + return &FileNameBuilder_Down_Call{Call: _e.mock.On("Down", version, forceSafely)} +} + +func (_c *FileNameBuilder_Down_Call) Run(run func(version string, forceSafely bool)) *FileNameBuilder_Down_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *FileNameBuilder_Down_Call) Return(fname string, safely bool) *FileNameBuilder_Down_Call { + _c.Call.Return(fname, safely) + return _c +} + +func (_c *FileNameBuilder_Down_Call) RunAndReturn(run func(string, bool) (string, bool)) *FileNameBuilder_Down_Call { + _c.Call.Return(run) + return _c +} + +// Up provides a mock function with given fields: version, forceSafely +func (_m *FileNameBuilder) Up(version string, forceSafely bool) (string, bool) { + ret := _m.Called(version, forceSafely) + + var r0 string + var r1 bool + if rf, ok := ret.Get(0).(func(string, bool) (string, bool)); ok { + return rf(version, forceSafely) + } + if rf, ok := ret.Get(0).(func(string, bool) string); ok { + r0 = rf(version, forceSafely) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, bool) bool); ok { + r1 = rf(version, forceSafely) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// FileNameBuilder_Up_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Up' +type FileNameBuilder_Up_Call struct { + *mock.Call +} + +// Up is a helper method to define mock.On call +// - version string +// - forceSafely bool +func (_e *FileNameBuilder_Expecter) Up(version interface{}, forceSafely interface{}) *FileNameBuilder_Up_Call { + return &FileNameBuilder_Up_Call{Call: _e.mock.On("Up", version, forceSafely)} +} + +func (_c *FileNameBuilder_Up_Call) Run(run func(version string, forceSafely bool)) *FileNameBuilder_Up_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *FileNameBuilder_Up_Call) Return(fname string, safely bool) *FileNameBuilder_Up_Call { + _c.Call.Return(fname, safely) + return _c +} + +func (_c *FileNameBuilder_Up_Call) RunAndReturn(run func(string, bool) (string, bool)) *FileNameBuilder_Up_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewFileNameBuilder interface { + mock.TestingT + Cleanup(func()) +} + +// NewFileNameBuilder creates a new instance of FileNameBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFileNameBuilder(t mockConstructorTestingTNewFileNameBuilder) *FileNameBuilder { + mock := &FileNameBuilder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/action/mockaction/MigrationService.go b/internal/action/mockaction/MigrationService.go new file mode 100644 index 0000000..e7aa10b --- /dev/null +++ b/internal/action/mockaction/MigrationService.go @@ -0,0 +1,238 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockaction + +import ( + context "context" + + entity "github.com/raoptimus/db-migrator.go/internal/dal/entity" + mock "github.com/stretchr/testify/mock" +) + +// MigrationService is an autogenerated mock type for the MigrationService type +type MigrationService struct { + mock.Mock +} + +type MigrationService_Expecter struct { + mock *mock.Mock +} + +func (_m *MigrationService) EXPECT() *MigrationService_Expecter { + return &MigrationService_Expecter{mock: &_m.Mock} +} + +// ApplyFile provides a mock function with given fields: ctx, _a1, fileName, safely +func (_m *MigrationService) ApplyFile(ctx context.Context, _a1 *entity.Migration, fileName string, safely bool) error { + ret := _m.Called(ctx, _a1, fileName, safely) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *entity.Migration, string, bool) error); ok { + r0 = rf(ctx, _a1, fileName, safely) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MigrationService_ApplyFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyFile' +type MigrationService_ApplyFile_Call struct { + *mock.Call +} + +// ApplyFile is a helper method to define mock.On call +// - ctx context.Context +// - _a1 *entity.Migration +// - fileName string +// - safely bool +func (_e *MigrationService_Expecter) ApplyFile(ctx interface{}, _a1 interface{}, fileName interface{}, safely interface{}) *MigrationService_ApplyFile_Call { + return &MigrationService_ApplyFile_Call{Call: _e.mock.On("ApplyFile", ctx, _a1, fileName, safely)} +} + +func (_c *MigrationService_ApplyFile_Call) Run(run func(ctx context.Context, _a1 *entity.Migration, fileName string, safely bool)) *MigrationService_ApplyFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*entity.Migration), args[2].(string), args[3].(bool)) + }) + return _c +} + +func (_c *MigrationService_ApplyFile_Call) Return(_a0 error) *MigrationService_ApplyFile_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MigrationService_ApplyFile_Call) RunAndReturn(run func(context.Context, *entity.Migration, string, bool) error) *MigrationService_ApplyFile_Call { + _c.Call.Return(run) + return _c +} + +// Migrations provides a mock function with given fields: ctx, limit +func (_m *MigrationService) Migrations(ctx context.Context, limit int) (entity.Migrations, error) { + ret := _m.Called(ctx, limit) + + var r0 entity.Migrations + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (entity.Migrations, error)); ok { + return rf(ctx, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int) entity.Migrations); ok { + r0 = rf(ctx, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(entity.Migrations) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MigrationService_Migrations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrations' +type MigrationService_Migrations_Call struct { + *mock.Call +} + +// Migrations is a helper method to define mock.On call +// - ctx context.Context +// - limit int +func (_e *MigrationService_Expecter) Migrations(ctx interface{}, limit interface{}) *MigrationService_Migrations_Call { + return &MigrationService_Migrations_Call{Call: _e.mock.On("Migrations", ctx, limit)} +} + +func (_c *MigrationService_Migrations_Call) Run(run func(ctx context.Context, limit int)) *MigrationService_Migrations_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int)) + }) + return _c +} + +func (_c *MigrationService_Migrations_Call) Return(_a0 entity.Migrations, _a1 error) *MigrationService_Migrations_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MigrationService_Migrations_Call) RunAndReturn(run func(context.Context, int) (entity.Migrations, error)) *MigrationService_Migrations_Call { + _c.Call.Return(run) + return _c +} + +// NewMigrations provides a mock function with given fields: ctx, limit +func (_m *MigrationService) NewMigrations(ctx context.Context, limit int) (entity.Migrations, error) { + ret := _m.Called(ctx, limit) + + var r0 entity.Migrations + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (entity.Migrations, error)); ok { + return rf(ctx, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int) entity.Migrations); ok { + r0 = rf(ctx, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(entity.Migrations) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MigrationService_NewMigrations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewMigrations' +type MigrationService_NewMigrations_Call struct { + *mock.Call +} + +// NewMigrations is a helper method to define mock.On call +// - ctx context.Context +// - limit int +func (_e *MigrationService_Expecter) NewMigrations(ctx interface{}, limit interface{}) *MigrationService_NewMigrations_Call { + return &MigrationService_NewMigrations_Call{Call: _e.mock.On("NewMigrations", ctx, limit)} +} + +func (_c *MigrationService_NewMigrations_Call) Run(run func(ctx context.Context, limit int)) *MigrationService_NewMigrations_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int)) + }) + return _c +} + +func (_c *MigrationService_NewMigrations_Call) Return(_a0 entity.Migrations, _a1 error) *MigrationService_NewMigrations_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MigrationService_NewMigrations_Call) RunAndReturn(run func(context.Context, int) (entity.Migrations, error)) *MigrationService_NewMigrations_Call { + _c.Call.Return(run) + return _c +} + +// RevertFile provides a mock function with given fields: ctx, _a1, fileName, safely +func (_m *MigrationService) RevertFile(ctx context.Context, _a1 *entity.Migration, fileName string, safely bool) error { + ret := _m.Called(ctx, _a1, fileName, safely) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *entity.Migration, string, bool) error); ok { + r0 = rf(ctx, _a1, fileName, safely) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MigrationService_RevertFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RevertFile' +type MigrationService_RevertFile_Call struct { + *mock.Call +} + +// RevertFile is a helper method to define mock.On call +// - ctx context.Context +// - _a1 *entity.Migration +// - fileName string +// - safely bool +func (_e *MigrationService_Expecter) RevertFile(ctx interface{}, _a1 interface{}, fileName interface{}, safely interface{}) *MigrationService_RevertFile_Call { + return &MigrationService_RevertFile_Call{Call: _e.mock.On("RevertFile", ctx, _a1, fileName, safely)} +} + +func (_c *MigrationService_RevertFile_Call) Run(run func(ctx context.Context, _a1 *entity.Migration, fileName string, safely bool)) *MigrationService_RevertFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*entity.Migration), args[2].(string), args[3].(bool)) + }) + return _c +} + +func (_c *MigrationService_RevertFile_Call) Return(_a0 error) *MigrationService_RevertFile_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MigrationService_RevertFile_Call) RunAndReturn(run func(context.Context, *entity.Migration, string, bool) error) *MigrationService_RevertFile_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewMigrationService interface { + mock.TestingT + Cleanup(func()) +} + +// NewMigrationService creates a new instance of MigrationService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewMigrationService(t mockConstructorTestingTNewMigrationService) *MigrationService { + mock := &MigrationService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/action/redo.go b/internal/action/redo.go new file mode 100644 index 0000000..be58f05 --- /dev/null +++ b/internal/action/redo.go @@ -0,0 +1,100 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "fmt" + + "github.com/raoptimus/db-migrator.go/internal/args" + "github.com/raoptimus/db-migrator.go/internal/console" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" + "github.com/urfave/cli/v2" +) + +type Redo struct { + service MigrationService + fileNameBuilder FileNameBuilder + interactive bool +} + +func NewRedo( + service MigrationService, + fileNameBuilder FileNameBuilder, + interactive bool, +) *Redo { + return &Redo{ + service: service, + fileNameBuilder: fileNameBuilder, + interactive: interactive, + } +} + +func (r *Redo) Run(ctx *cli.Context) error { + limit, err := args.ParseStepStringOrDefault(ctx.Args().Get(0), 1) + if err != nil { + return err + } + + migrations, err := r.service.Migrations(ctx.Context, limit) + if err != nil { + return err + } + + migrationsCount := migrations.Len() + if migrationsCount == 0 { + console.SuccessLn("No migration has been done before.") + return nil + } + + console.Warnf( + "Total %d %s to be redone: \n", + migrationsCount, + console.NumberPlural(migrationsCount, "migration", "migrations"), + ) + + printMigrations(migrations, false) + + question := fmt.Sprintf("Redo the above %d %s?", + migrationsCount, console.NumberPlural(migrationsCount, "migration", "migrations"), + ) + if r.interactive && !console.Confirm(question) { + return nil + } + + reversedMigrations := make(entity.Migrations, 0, len(migrations)) + for i := range migrations { + migration := &migrations[i] + fileName, safely := r.fileNameBuilder.Down(migration.Version, false) + + if err := r.service.RevertFile(ctx.Context, migration, fileName, safely); err != nil { + console.ErrorLn("Migration failed. The rest of the migrations are canceled.") + return err + } + + reversedMigrations = append(reversedMigrations, migrations[i]) + } + + for i := range reversedMigrations { + migration := &reversedMigrations[i] + fileName, safely := r.fileNameBuilder.Up(migration.Version, false) + + if err := r.service.ApplyFile(ctx.Context, migration, fileName, safely); err != nil { + console.ErrorLn("Migration failed. The rest of the migrations are canceled.\n") + return err + } + } + + console.Warnf( + "%d %s redone.", + migrationsCount, + console.NumberPlural(migrationsCount, migrationWas, migrationsWere), + ) + console.SuccessLn("Migration redone successfully.\n") + return nil +} diff --git a/internal/action/to.go b/internal/action/to.go new file mode 100644 index 0000000..0a179c9 --- /dev/null +++ b/internal/action/to.go @@ -0,0 +1,30 @@ +package action + +import ( + "github.com/raoptimus/db-migrator.go/internal/console" + "github.com/urfave/cli/v2" +) + +type To struct { + service MigrationService + fileNameBuilder FileNameBuilder + interactive bool +} + +func NewTo( + service MigrationService, + fileNameBuilder FileNameBuilder, + interactive bool, +) *To { + return &To{ + service: service, + fileNameBuilder: fileNameBuilder, + interactive: interactive, + } +} + +func (t *To) Run(ctx *cli.Context) error { + // version string from args + console.Info("coming soon") + return nil +} diff --git a/internal/action/upgrade.go b/internal/action/upgrade.go new file mode 100644 index 0000000..5d515c8 --- /dev/null +++ b/internal/action/upgrade.go @@ -0,0 +1,111 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package action + +import ( + "fmt" + + "github.com/raoptimus/db-migrator.go/internal/args" + "github.com/urfave/cli/v2" +) + +type Upgrade struct { + console Console + service MigrationService + fileNameBuilder FileNameBuilder + interactive bool +} + +func NewUpgrade( + console Console, + service MigrationService, + fileNameBuilder FileNameBuilder, + interactive bool, +) *Upgrade { + return &Upgrade{ + console: console, + service: service, + fileNameBuilder: fileNameBuilder, + interactive: interactive, + } +} + +func (u *Upgrade) Run(ctx *cli.Context) error { + limit, err := args.ParseStepStringOrDefault(ctx.Args().Get(0), minLimit) + if err != nil { + return err + } + + migrations, err := u.service.NewMigrations(ctx.Context, limit) + if err != nil { + return err + } + + totalNewMigrations := migrations.Len() + if totalNewMigrations == 0 { + u.console.SuccessLn(noNewMigrationsFound) + return nil + } + + if limit > 0 && migrations.Len() > limit { + migrations = migrations[:limit] + } + + if migrations.Len() == totalNewMigrations { + u.console.Warnf( + "Total %d new %s to be applied: \n", + migrations.Len(), + u.console.NumberPlural(migrations.Len(), "migration", "migrations"), + ) + } else { + u.console.Warnf( + "Total %d out of %d new %s to be applied: \n", + migrations.Len(), + totalNewMigrations, + u.console.NumberPlural(totalNewMigrations, "migration", "migrations"), + ) + } + + printMigrations(migrations, false) + + question := fmt.Sprintf("ApplyFile the above %s?", + u.console.NumberPlural(migrations.Len(), "migration", "migrations"), + ) + if u.interactive && !u.console.Confirm(question) { + return nil + } + + var applied int + for i := range migrations { + migration := &migrations[i] + fileName, safely := u.fileNameBuilder.Up(migration.Version, false) + + if err := u.service.ApplyFile(ctx.Context, migration, fileName, safely); err != nil { + u.console.Errorf("%d from %d %s applied.\n", + applied, + migrations.Len(), + u.console.NumberPlural(applied, migrationWas, migrationsWere), + ) + u.console.Error("The rest of the migrations are canceled.\n") + + return err + } + + applied++ + } + + u.console.Successf( + "%d %s applied\n", + migrations.Len(), + u.console.NumberPlural(migrations.Len(), migrationWas, migrationsWere), + ) + u.console.SuccessLn(migratedUpSuccessfully) + + return nil +} diff --git a/internal/action/upgrade_test.go b/internal/action/upgrade_test.go new file mode 100644 index 0000000..c08e1de --- /dev/null +++ b/internal/action/upgrade_test.go @@ -0,0 +1,28 @@ +package action + +import ( + "testing" + + "github.com/raoptimus/db-migrator.go/internal/action/mockaction" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" + "github.com/stretchr/testify/assert" +) + +func TestUpgrade_Run_NoMigrations_NoError(t *testing.T) { + ctx := cliContext(t, "2") + + serv := mockaction.NewMigrationService(t) + serv.EXPECT(). + NewMigrations(ctx.Context, 2). + Return(entity.Migrations{}, nil) + + c := mockaction.NewConsole(t) + c.EXPECT(). + SuccessLn("No new migrations found. Your system is up-to-date.") + + fb := mockaction.NewFileNameBuilder(t) + + upgrade := NewUpgrade(c, serv, fb, true) + err := upgrade.Run(ctx) + assert.NoError(t, err) +} diff --git a/internal/args/limit.go b/internal/args/limit.go new file mode 100644 index 0000000..e07a082 --- /dev/null +++ b/internal/args/limit.go @@ -0,0 +1,35 @@ +package args + +import ( + "fmt" + "strconv" + + "github.com/pkg/errors" +) + +const ( + empty = "" + all = "all" +) + +var ErrArgumentMustBeGreaterThanZero = errors.New("the step argument must be greater than 0") + +func ParseStepStringOrDefault(value string, defaults int) (int, error) { + switch value { + case empty: + return defaults, nil + case all: + return 0, nil + default: + i, err := strconv.Atoi(value) + if err != nil { + return -1, fmt.Errorf("the step argument %s is not valid", value) + } + + if i < 1 { + return -1, ErrArgumentMustBeGreaterThanZero + } + + return i, nil + } +} diff --git a/migrator/migrator_clickhouse_cluster_test.go b/internal/bak/migrator_clickhouse_cluster_integration_test.go.bak similarity index 76% rename from migrator/migrator_clickhouse_cluster_test.go rename to internal/bak/migrator_clickhouse_cluster_integration_test.go.bak index 3282f1a..8253069 100644 --- a/migrator/migrator_clickhouse_cluster_test.go +++ b/internal/bak/migrator_clickhouse_cluster_integration_test.go.bak @@ -1,12 +1,24 @@ -package migrator +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package bak import ( - "github.com/stretchr/testify/assert" "os" "testing" + + "github.com/stretchr/testify/assert" ) func TestMigrateService_ClickHouseCluster_UpDown(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } var m1 *Service var m2 *Service var err error @@ -29,7 +41,7 @@ func TestMigrateService_ClickHouseCluster_UpDown(t *testing.T) { } func createClickhouse1ClusterMigrator() (*Service, error) { - return New(Options{ + return New(&Options{ DSN: os.Getenv("CLICKHOUSE_CLUSTER_DSN1"), Directory: os.Getenv("CLICKHOUSE_CLUSTER_MIGRATIONS_PATH"), TableName: "migration", @@ -40,7 +52,7 @@ func createClickhouse1ClusterMigrator() (*Service, error) { } func createClickhouse2ClusterMigrator() (*Service, error) { - return New(Options{ + return New(&Options{ DSN: os.Getenv("CLICKHOUSE_CLUSTER_DSN2"), Directory: os.Getenv("CLICKHOUSE_CLUSTER_MIGRATIONS_PATH"), TableName: "migration", diff --git a/migrator/migrator_clickhouse_test.go b/internal/bak/migrator_clickhouse_integration_test.go.bak similarity index 67% rename from migrator/migrator_clickhouse_test.go rename to internal/bak/migrator_clickhouse_integration_test.go.bak index 30de128..f5fc8cd 100644 --- a/migrator/migrator_clickhouse_test.go +++ b/internal/bak/migrator_clickhouse_integration_test.go.bak @@ -1,13 +1,26 @@ -package migrator +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package bak import ( "database/sql" - "github.com/stretchr/testify/assert" "os" "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) func TestMigrateService_ClickHouse_UpDown(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } m, err := createClickhouseMigrator() assert.NoError(t, err) @@ -23,13 +36,13 @@ func TestMigrateService_ClickHouse_UpDown(t *testing.T) { } func createClickhouseMigrator() (*Service, error) { - return New(Options{ + return New(&Options{ DSN: os.Getenv("CLICKHOUSE_DSN"), Directory: os.Getenv("CLICKHOUSE_MIGRATIONS_PATH"), TableName: "migration", Compact: false, Interactive: false, - }) + }, logrus.StandardLogger()) } func assertEqualMigrationsCount(t *testing.T, db *sql.DB, expected int) { diff --git a/migrator/migrator_mysql_test.go b/internal/bak/migrator_mysql_integration_test.go.bak similarity index 85% rename from migrator/migrator_mysql_test.go rename to internal/bak/migrator_mysql_integration_test.go.bak index 4d363a7..ba298d6 100644 --- a/migrator/migrator_mysql_test.go +++ b/internal/bak/migrator_mysql_integration_test.go.bak @@ -1,12 +1,17 @@ -package migrator +package bak import ( - "github.com/stretchr/testify/assert" "os" "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" ) func TestMigrateService_Mysql_UpDown_Successfully(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } m, err := createMysqlMigrator("migration") assert.NoError(t, err) @@ -30,6 +35,9 @@ func TestMigrateService_Mysql_UpDown_Successfully(t *testing.T) { } func TestMigrateService_Mysql_Redo_Successfully(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } m, err := createPostgresMigrator("migration") assert.NoError(t, err) @@ -52,11 +60,11 @@ func TestMigrateService_Mysql_Redo_Successfully(t *testing.T) { } func createMysqlMigrator(migrationTableName string) (*Service, error) { - return New(Options{ + return New(&Options{ DSN: os.Getenv("MYSQL_DSN"), Directory: os.Getenv("MYSQL_MIGRATIONS_PATH"), TableName: migrationTableName, Compact: false, Interactive: false, - }) + }, logrus.StandardLogger()) } diff --git a/migrator/migrator_postgres_test.go b/internal/bak/migrator_postgres_integration_test.go.bak similarity index 75% rename from migrator/migrator_postgres_test.go rename to internal/bak/migrator_postgres_integration_test.go.bak index cd7d981..261c65e 100644 --- a/migrator/migrator_postgres_test.go +++ b/internal/bak/migrator_postgres_integration_test.go.bak @@ -1,12 +1,25 @@ -package migrator +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package bak import ( - "github.com/stretchr/testify/assert" "os" "testing" + + "github.com/raoptimus/db-migrator.go/internal/app" + "github.com/stretchr/testify/assert" ) func TestMigrateService_Postgres_UpDown_Successfully(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } m, err := createPostgresMigrator("migration") assert.NoError(t, err) @@ -33,6 +46,9 @@ func TestMigrateService_Postgres_UpDown_Successfully(t *testing.T) { } func TestMigrateService_Postgres_Redo_Successfully(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } m, err := createPostgresMigrator("migration") assert.NoError(t, err) @@ -55,6 +71,9 @@ func TestMigrateService_Postgres_Redo_Successfully(t *testing.T) { } func TestMigrateService_Postgres_UpDown_WithSchema_Successfully(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } m, err := createPostgresMigrator("docker.migration") assert.NoError(t, err) @@ -81,10 +100,13 @@ func TestMigrateService_Postgres_UpDown_WithSchema_Successfully(t *testing.T) { } func TestMigrateService_Postgres_Redo_WithSchema_Successfully(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } m, err := createPostgresMigrator("docker.migration") assert.NoError(t, err) - err = m.Down("all") + err = m.Downgrade("all") assert.NoError(t, err) err = m.Up("2") @@ -102,8 +124,8 @@ func TestMigrateService_Postgres_Redo_WithSchema_Successfully(t *testing.T) { assert.NoError(t, err) } -func createPostgresMigrator(migrationTableName string) (*Service, error) { - return New(Options{ +func createPostgresMigrator(migrationTableName string) (*app.DBService, error) { + return app.New(&app.Options{ DSN: os.Getenv("POSTGRES_DSN"), Directory: os.Getenv("POSTGRES_MIGRATIONS_PATH"), TableName: migrationTableName, diff --git a/internal/builder/dependencies.go b/internal/builder/dependencies.go new file mode 100644 index 0000000..26d0668 --- /dev/null +++ b/internal/builder/dependencies.go @@ -0,0 +1,6 @@ +package builder + +//go:generate mockery --name=File --outpkg=mockbuilder --output=./mockbuilder +type File interface { + Exists(fileName string) (bool, error) +} diff --git a/internal/builder/filename.go b/internal/builder/filename.go new file mode 100644 index 0000000..deadf7a --- /dev/null +++ b/internal/builder/filename.go @@ -0,0 +1,58 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package builder + +import ( + "path/filepath" +) + +const ( + safelyUpSuffix = ".safe.up.sql" + safelyDownSuffix = ".safe.down.sql" + unsafelyUpSuffix = ".up.sql" + unsafelyDownSuffix = ".down.sql" +) + +type FileName struct { + file File + migrationsDirectory string +} + +func NewFileName(file File, migrationsDirectory string) *FileName { + return &FileName{ + file: file, + migrationsDirectory: migrationsDirectory, + } +} + +// Up builds a file name for migration update. +func (s *FileName) Up(version string, forceSafely bool) (fname string, safely bool) { + return s.build(version, safelyUpSuffix, unsafelyUpSuffix, forceSafely) +} + +// Down builds a file name for migration downgrade. +func (s *FileName) Down(version string, forceSafely bool) (fname string, safely bool) { + return s.build(version, safelyDownSuffix, unsafelyDownSuffix, forceSafely) +} + +func (s *FileName) build( + version, + safelySuffix, + unsafelySuffix string, + forceSafely bool, +) (fname string, safely bool) { + safelyFile := filepath.Join(s.migrationsDirectory, version+safelySuffix) + unsafelyFile := filepath.Join(s.migrationsDirectory, version+unsafelySuffix) + + if exists, _ := s.file.Exists(unsafelyFile); exists && !forceSafely { + return unsafelyFile, false + } + + return safelyFile, true +} diff --git a/internal/builder/filename_test.go b/internal/builder/filename_test.go new file mode 100644 index 0000000..6679b73 --- /dev/null +++ b/internal/builder/filename_test.go @@ -0,0 +1,142 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package builder + +import ( + "testing" + + "github.com/raoptimus/db-migrator.go/internal/builder/mockbuilder" + "github.com/stretchr/testify/assert" +) + +func TestFileNameBuilder_BuildUpFileName(t *testing.T) { + tests := []struct { + name string + forceSafely bool + version string + expectedSafely bool + existsFileName string + expectedFileName string + fileExists bool + }{ + { + name: "unsafely file exists expects unsafely filename", + forceSafely: false, + version: "210328_221600_test", + expectedSafely: false, + existsFileName: "/someDir/210328_221600_test.up.sql", + expectedFileName: "/someDir/210328_221600_test.up.sql", + fileExists: true, + }, + { + name: "unsafely file not exists expects safely filename", + forceSafely: false, + version: "210328_221600_test", + expectedSafely: true, + existsFileName: "/someDir/210328_221600_test.up.sql", + expectedFileName: "/someDir/210328_221600_test.safe.up.sql", + fileExists: false, + }, + { + name: "unsafely file exists force safely expects safely filename", + forceSafely: true, + version: "210328_221600_test", + expectedSafely: true, + existsFileName: "/someDir/210328_221600_test.up.sql", + expectedFileName: "/someDir/210328_221600_test.safe.up.sql", + fileExists: true, + }, + { + name: "unsafely file not exists force safely expects safely filename", + forceSafely: true, + version: "210328_221600_test", + expectedSafely: true, + existsFileName: "/someDir/210328_221600_test.up.sql", + expectedFileName: "/someDir/210328_221600_test.safe.up.sql", + fileExists: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + file := mockbuilder.NewFile(t) + file.EXPECT(). + Exists(test.existsFileName). + Return(test.fileExists, nil) + + fb := NewFileName(file, "/someDir") + fileName, safely := fb.Up(test.version, test.forceSafely) + + assert.Equal(t, test.expectedSafely, safely) + assert.Equal(t, test.expectedFileName, fileName) + }) + } +} + +func TestFileNameBuilder_BuildDownFileName(t *testing.T) { + tests := []struct { + name string + forceSafely bool + version string + expectedSafely bool + existsFileName string + expectedFileName string + fileExists bool + }{ + { + name: "unsafely file exists expects unsafely filename", + forceSafely: false, + version: "210328_221600_test", + expectedSafely: false, + existsFileName: "/someDir/210328_221600_test.down.sql", + expectedFileName: "/someDir/210328_221600_test.down.sql", + fileExists: true, + }, + { + name: "unsafely file not exists expects safely filename", + forceSafely: false, + version: "210328_221600_test", + expectedSafely: true, + existsFileName: "/someDir/210328_221600_test.down.sql", + expectedFileName: "/someDir/210328_221600_test.safe.down.sql", + fileExists: false, + }, + { + name: "unsafely file exists force safely expects safely filename", + forceSafely: true, + version: "210328_221600_test", + expectedSafely: true, + existsFileName: "/someDir/210328_221600_test.down.sql", + expectedFileName: "/someDir/210328_221600_test.safe.down.sql", + fileExists: true, + }, + { + name: "unsafely file not exists force safely expects safely filename", + forceSafely: true, + version: "210328_221600_test", + expectedSafely: true, + existsFileName: "/someDir/210328_221600_test.down.sql", + expectedFileName: "/someDir/210328_221600_test.safe.down.sql", + fileExists: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + file := mockbuilder.NewFile(t) + file.EXPECT(). + Exists(test.existsFileName). + Return(test.fileExists, nil) + + fb := NewFileName(file, "/someDir") + fileName, safely := fb.Down(test.version, test.forceSafely) + + assert.Equal(t, test.expectedSafely, safely) + assert.Equal(t, test.expectedFileName, fileName) + }) + } +} diff --git a/internal/builder/mockbuilder/File.go b/internal/builder/mockbuilder/File.go new file mode 100644 index 0000000..2b66702 --- /dev/null +++ b/internal/builder/mockbuilder/File.go @@ -0,0 +1,85 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockbuilder + +import mock "github.com/stretchr/testify/mock" + +// File is an autogenerated mock type for the File type +type File struct { + mock.Mock +} + +type File_Expecter struct { + mock *mock.Mock +} + +func (_m *File) EXPECT() *File_Expecter { + return &File_Expecter{mock: &_m.Mock} +} + +// Exists provides a mock function with given fields: fileName +func (_m *File) Exists(fileName string) (bool, error) { + ret := _m.Called(fileName) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { + return rf(fileName) + } + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(fileName) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(fileName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// File_Exists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exists' +type File_Exists_Call struct { + *mock.Call +} + +// Exists is a helper method to define mock.On call +// - fileName string +func (_e *File_Expecter) Exists(fileName interface{}) *File_Exists_Call { + return &File_Exists_Call{Call: _e.mock.On("Exists", fileName)} +} + +func (_c *File_Exists_Call) Run(run func(fileName string)) *File_Exists_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *File_Exists_Call) Return(_a0 bool, _a1 error) *File_Exists_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *File_Exists_Call) RunAndReturn(run func(string) (bool, error)) *File_Exists_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewFile interface { + mock.TestingT + Cleanup(func()) +} + +// NewFile creates a new instance of File. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFile(t mockConstructorTestingTNewFile) *File { + mock := &File{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/console/confirm.go b/internal/console/confirm.go similarity index 82% rename from console/confirm.go rename to internal/console/confirm.go index 8502f13..e2ecb52 100644 --- a/console/confirm.go +++ b/internal/console/confirm.go @@ -1,16 +1,15 @@ /** * This file is part of the raoptimus/db-migrator.go library * - * @copyright Copyright (c) Evgeniy Urvantsev + * @copyright Copyright (c) Evgeniy Urvantsev * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md * @link https://github.com/raoptimus/db-migrator.go */ + package console import ( "bufio" - "fmt" - "log" "os" "strings" ) @@ -19,11 +18,11 @@ func Confirm(s string) bool { reader := bufio.NewReader(os.Stdin) for { - fmt.Printf("%s [y/n]: ", s) + Infof("%s [y/n]: ", s) response, err := reader.ReadString('\n') if err != nil { - log.Fatal(err) + Fatal(err) } response = strings.ToLower(strings.TrimSpace(response)) diff --git a/internal/console/output.go b/internal/console/output.go new file mode 100644 index 0000000..d9e96d4 --- /dev/null +++ b/internal/console/output.go @@ -0,0 +1,86 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package console + +import ( + "fmt" + "os" +) + +var ( + Black = Color("\033[1;30m%s\033[0m") + Red = Color("\033[1;31m%s\033[0m") + Green = Color("\033[1;32m%s\033[0m") + Yellow = Color("\033[1;33m%s\033[0m") + Purple = Color("\033[1;34m%s\033[0m") + Magenta = Color("\033[1;35m%s\033[0m") + Teal = Color("\033[1;36m%s\033[0m") + White = Color("\033[1;37m%s\033[0m") +) + +func Info(message string) { + fmt.Print(Black(message)) +} +func InfoLn(message string) { + fmt.Println(Black(message)) +} +func Infof(message string, a ...any) { + fmt.Printf(Black(message), a...) +} + +func Success(message string) { + fmt.Print(Green(message)) +} +func SuccessLn(message string) { + fmt.Println(Green(message)) +} +func Successf(message string, a ...any) { + fmt.Printf(Green(message), a...) +} + +func Warn(message string) { + fmt.Print(Yellow(message)) +} +func WarnLn(message string) { + fmt.Println(Yellow(message)) +} +func Warnf(message string, a ...any) { + fmt.Printf(Yellow(message), a...) +} + +func Error(message string) { + fmt.Print(Red(message)) +} +func ErrorLn(message string) { + fmt.Println(Red(message)) +} +func Errorf(message string, a ...any) { + fmt.Printf(Red(message), a...) +} + +func Fatal(err error) { + Errorf("Exception: %v", err) + os.Exit(1) +} + +func Color(colorString string) func(...interface{}) string { + sprint := func(args ...interface{}) string { + return fmt.Sprintf(colorString, fmt.Sprint(args...)) + } + + return sprint +} + +func NumberPlural(c int, one, many string) string { + if c > 1 { + return many + } + + return one +} diff --git a/internal/dal/connection/connection.go b/internal/dal/connection/connection.go new file mode 100644 index 0000000..a23c0ab --- /dev/null +++ b/internal/dal/connection/connection.go @@ -0,0 +1,156 @@ +package connection + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/pkg/errors" +) + +type ContextKey string + +const contextKeyTX ContextKey = "tx" + +type Connection struct { + driver Driver + dsn string + db *sql.DB + ping bool +} + +func New(dsn string) (*Connection, error) { + switch { + case strings.HasPrefix(dsn, "clickhouse://"): + return clickhouse(dsn) + case strings.HasPrefix(dsn, "postgres://"): + return postgres(dsn) + case strings.HasPrefix(dsn, "mysql://"): + return mysql(dsn) + default: + return nil, fmt.Errorf("driver \"%s\" doesn't support", dsn) + } +} + +// DSN returns the connection string. +func (c *Connection) DSN() string { + return c.dsn +} + +// Driver returns the driver name used to connect to the database. +func (c *Connection) Driver() Driver { + return c.driver +} + +// Ping checks connection +func (c *Connection) Ping() error { + if c.ping { + return nil + } + if err := c.db.Ping(); err != nil { + return errors.Wrapf(err, "ping %v connection: %v", c.Driver(), c.dsn) + } + c.ping = true + return nil +} + +// QueryContext executes a query that returns rows, typically a SELECT. +// The args are for any placeholder parameters in the query. +func (c *Connection) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) { + if err := c.Ping(); err != nil { + return nil, err + } + return c.db.QueryContext(ctx, query, args...) +} + +// ExecContext executes a query without returning any rows. +// The args are for any placeholder parameters in the query. +func (c *Connection) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) { + if err := c.Ping(); err != nil { + return nil, err + } + v := ctx.Value(contextKeyTX) + if v != nil { + if tx, ok := v.(*sql.Tx); ok { + stmt, err := tx.PrepareContext(ctx, query) + if err != nil { + return nil, err + } + return stmt.ExecContext(ctx, args...) + } + } + return c.db.ExecContext(ctx, query, args...) +} + +// Transaction executes body in func txFn into transaction. +func (c *Connection) Transaction(ctx context.Context, txFn func(ctx context.Context) error) error { + if err := c.Ping(); err != nil { + return err + } + if v := ctx.Value(contextKeyTX); v != nil { + return errors.New("active transaction does not close") + } + + tx, err := c.db.BeginTx(ctx, nil) + if err != nil { + return err + } + + ctxWithTX := context.WithValue(ctx, contextKeyTX, tx) + + if err := txFn(ctxWithTX); err != nil { + if err2 := tx.Rollback(); err2 != nil { + return errors.Wrap(err, err2.Error()) + } + return err + } + + return tx.Commit() +} + +// clickhouse returns repository with clickhouse configuration. +func clickhouse(dsn string) (*Connection, error) { + dsn, err := NormalizeClickhouseDSN(dsn) + if err != nil { + return nil, err + } + db, err := sql.Open("clickhouse", dsn) + if err != nil { + return nil, err + } + + return &Connection{ + driver: DriverClickhouse, + dsn: dsn, + db: db, + }, nil +} + +// postgres returns repository with postgres configuration. +func postgres(dsn string) (*Connection, error) { + db, err := sql.Open(DriverPostgres.String(), dsn) + if err != nil { + return nil, errors.Wrap(err, "open postgres connection") + } + + return &Connection{ + driver: DriverPostgres, + dsn: dsn, + db: db, + }, nil +} + +// mysql returns repository with mysql configuration. +func mysql(dsn string) (*Connection, error) { + db, err := sql.Open(DriverMySQL.String(), dsn[8:]) + if err != nil { + return nil, errors.Wrap(err, "open mysql connection") + } + + return &Connection{ + driver: DriverMySQL, + dsn: dsn, + db: db, + }, nil +} diff --git a/internal/dal/connection/driver.go b/internal/dal/connection/driver.go new file mode 100644 index 0000000..059499d --- /dev/null +++ b/internal/dal/connection/driver.go @@ -0,0 +1,13 @@ +package connection + +type Driver string + +const ( + DriverClickhouse Driver = "clickhouse" + DriverMySQL Driver = "mysql" + DriverPostgres Driver = "postgres" +) + +func (d Driver) String() string { + return string(d) +} diff --git a/internal/dal/connection/dsn.go b/internal/dal/connection/dsn.go new file mode 100644 index 0000000..4c0a641 --- /dev/null +++ b/internal/dal/connection/dsn.go @@ -0,0 +1,28 @@ +package connection + +import ( + "fmt" + "net/url" + "path" +) + +func NormalizeClickhouseDSN(dsn string) (string, error) { + dsnURL, err := url.Parse(dsn) + if err != nil { + return dsn, err + } + + password, _ := dsnURL.User.Password() + hostWithPort := dsnURL.Host + if dsnURL.Port() == "" { + hostWithPort += ":9000" + } + + return fmt.Sprintf("tcp://%s?username=%s&password=%s&database=%s&%s", + hostWithPort, + dsnURL.User.Username(), + password, + path.Base(dsnURL.Path), + dsnURL.RawQuery, + ), nil +} diff --git a/migrator/db/clickhouseMigration/helpers_test.go b/internal/dal/connection/dsn_test.go similarity index 87% rename from migrator/db/clickhouseMigration/helpers_test.go rename to internal/dal/connection/dsn_test.go index e9cf36e..cb2131f 100644 --- a/migrator/db/clickhouseMigration/helpers_test.go +++ b/internal/dal/connection/dsn_test.go @@ -1,17 +1,18 @@ -package clickhouseMigration +package connection import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) -func TestNormalizeDSNSuccessfully(t *testing.T) { +func TestNormalizeClickhouseDSN(t *testing.T) { var err error var actualDSN, originDSN, expectedDSN string for _, data := range getDataProvider() { originDSN, expectedDSN = data[0], data[1] - actualDSN, err = NormalizeDSN(originDSN) + actualDSN, err = NormalizeClickhouseDSN(originDSN) assert.NoError(t, err) assert.Equal(t, expectedDSN, actualDSN) } diff --git a/internal/dal/entity/migration.go b/internal/dal/entity/migration.go new file mode 100644 index 0000000..1ebafd0 --- /dev/null +++ b/internal/dal/entity/migration.go @@ -0,0 +1,35 @@ +package entity + +import ( + "sort" + "time" +) + +type Migration struct { + Version string `db:"version"` + ApplyTime int `db:"apply_time"` + // BodySQL string `db:"body_sql"` + // ExecutedSQL string `db:"executed_sql"` + // Release string `db:"release"` +} +type Migrations []Migration + +func (s Migrations) Len() int { + return len(s) +} + +func (s Migrations) Less(i, j int) bool { + return s[i].Version < s[j].Version +} + +func (s Migrations) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s Migrations) SortByVersion() { + sort.Sort(s) +} + +func (s Migration) ApplyTimeFormat() string { + return time.Unix(int64(s.ApplyTime), 0).Format("2006-01-02 15:04:05") +} diff --git a/internal/dal/repository/adapter.go b/internal/dal/repository/adapter.go new file mode 100644 index 0000000..01f1637 --- /dev/null +++ b/internal/dal/repository/adapter.go @@ -0,0 +1,117 @@ +package repository + +import ( + "context" + "fmt" + "strings" + + "github.com/go-sql-driver/mysql" + "github.com/raoptimus/db-migrator.go/internal/dal/connection" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" +) + +type adapter interface { + Migrations(ctx context.Context, limit int) (entity.Migrations, error) + HasMigrationHistoryTable(ctx context.Context) (exists bool, err error) + InsertMigration(ctx context.Context, version string) error + RemoveMigration(ctx context.Context, version string) error + ExecQuery(ctx context.Context, query string, args ...any) error + ExecQueryTransaction(ctx context.Context, fnTx func(ctx context.Context) error) error + DropMigrationHistoryTable(ctx context.Context) error + CreateMigrationHistoryTable(ctx context.Context) error + MigrationsCount(ctx context.Context) (int, error) + TableNameWithSchema() string + ForceSafely() bool +} + +type Repository struct { + adapter +} + +func (r *Repository) Migrations(ctx context.Context, limit int) (entity.Migrations, error) { + return r.adapter.Migrations(ctx, limit) +} + +func (r *Repository) HasMigrationHistoryTable(ctx context.Context) (exists bool, err error) { + return r.adapter.HasMigrationHistoryTable(ctx) +} + +func (r *Repository) InsertMigration(ctx context.Context, version string) error { + return r.adapter.InsertMigration(ctx, version) +} + +func (r *Repository) RemoveMigration(ctx context.Context, version string) error { + return r.adapter.RemoveMigration(ctx, version) +} + +func (r *Repository) ExecQuery(ctx context.Context, query string, args ...any) error { + return r.adapter.ExecQuery(ctx, query, args...) +} + +func (r *Repository) ExecQueryTransaction(ctx context.Context, fnTx func(ctx context.Context) error) error { + return r.adapter.ExecQueryTransaction(ctx, fnTx) +} + +func (r *Repository) DropMigrationHistoryTable(ctx context.Context) error { + return r.adapter.DropMigrationHistoryTable(ctx) +} + +func (r *Repository) CreateMigrationHistoryTable(ctx context.Context) error { + return r.adapter.CreateMigrationHistoryTable(ctx) +} + +func (r *Repository) MigrationsCount(ctx context.Context) (int, error) { + return r.adapter.MigrationsCount(ctx) +} + +func (r *Repository) TableNameWithSchema() string { + return r.adapter.TableNameWithSchema() +} + +func (r *Repository) ForceSafely() bool { + return r.adapter.ForceSafely() +} + +// create creates repository adapter +// +//nolint:ireturn,nolintlint // its ok +func create(conn Connection, options *Options) (adapter, error) { + switch conn.Driver() { + case connection.DriverMySQL: + cfg, err := mysql.ParseDSN(conn.DSN()) + if err != nil { + return nil, err + } + repo := NewMySQL(conn, &Options{ + TableName: options.TableName, + SchemaName: cfg.DBName, + }) + return repo, err + case connection.DriverPostgres: + var tableName, schemaName string + if strings.Contains(options.TableName, ".") { + parts := strings.Split(options.TableName, ".") + schemaName = parts[0] + tableName = parts[1] + } else { + schemaName = postgresDefaultSchema + tableName = options.TableName + } + + repo := NewPostgres(conn, &Options{ + TableName: tableName, + SchemaName: schemaName, + }) + return repo, nil + case connection.DriverClickhouse: + repo := NewClickhouse(conn, &Options{ + TableName: options.TableName, + SchemaName: options.SchemaName, + ClusterName: options.ClusterName, + ShardName: options.ShardName, + }) + return repo, nil + default: + return nil, fmt.Errorf("driver \"%s\" doesn't support", conn.Driver()) + } +} diff --git a/internal/dal/repository/clickhouse.go b/internal/dal/repository/clickhouse.go new file mode 100644 index 0000000..08de39c --- /dev/null +++ b/internal/dal/repository/clickhouse.go @@ -0,0 +1,271 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package repository + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/ClickHouse/clickhouse-go" + "github.com/pkg/errors" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" +) + +type Clickhouse struct { + conn Connection + options *Options +} + +func NewClickhouse(conn Connection, options *Options) *Clickhouse { + return &Clickhouse{ + conn: conn, + options: options, + } +} + +// Migrations returns applied migrations history. +func (c *Clickhouse) Migrations(ctx context.Context, limit int) (entity.Migrations, error) { + var ( + q = fmt.Sprintf( + ` + SELECT version, apply_time + FROM %s + WHERE is_deleted = 0 + ORDER BY apply_time DESC, version DESC + LIMIT ?`, + c.options.TableName, + ) + migrations entity.Migrations + ) + + rows, err := c.conn.QueryContext(ctx, q, limit) + if err != nil { + return nil, errors.Wrap(c.dbError(err, q), "get migrations") + } + for rows.Next() { + var ( + version string + applyTime int + ) + + if err := rows.Scan(&version, &applyTime); err != nil { + return nil, errors.Wrap(c.dbError(err, q), "get migrations") + } + + migrations = append(migrations, + entity.Migration{ + Version: version, + ApplyTime: applyTime, + }, + ) + } + + if err := rows.Err(); err != nil { + return nil, errors.Wrap(c.dbError(err, q), "get migrations") + } + + return migrations, nil +} + +// HasMigrationHistoryTable returns true if migration history table exists. +func (c *Clickhouse) HasMigrationHistoryTable(ctx context.Context) (exists bool, err error) { + var ( + q = ` + SELECT database, table + FROM system.columns + WHERE table = ? AND database = currentDatabase() + ` + rows *sql.Rows + ) + + rows, err = c.conn.QueryContext(ctx, q, c.options.TableName) + if err != nil { + return false, errors.Wrap(c.dbError(err, q), "get table schema") + } + + for rows.Next() { + var ( + database string + table string + ) + if err := rows.Scan(&database, &table); err != nil { + return false, errors.Wrap(c.dbError(err, q), "get table schema") + } + + //todo: scan columns to tableScheme + if table == c.options.TableName { + return true, nil + } + } + + if err := rows.Err(); err != nil { + return false, errors.Wrap(c.dbError(err, q), "get table schema") + } + + return false, nil +} + +// InsertMigration inserts the new migration record. +func (c *Clickhouse) InsertMigration(ctx context.Context, version string) error { + return c.insertMigration(ctx, version, false) +} + +// RemoveMigration removes the migration record. +func (c *Clickhouse) RemoveMigration(ctx context.Context, version string) error { + return c.insertMigration(ctx, version, true) +} + +// ExecQuery executes a query without returning any rows. +// The args are for any placeholder parameters in the query. +func (c *Clickhouse) ExecQuery(ctx context.Context, query string, args ...any) error { + _, err := c.conn.ExecContext(ctx, query, args...) + return err +} + +// ExecQueryTransaction executes txFn in transaction. +// todo: называется ExecQuery но query не принимает. подумать +func (c *Clickhouse) ExecQueryTransaction(ctx context.Context, txFn func(ctx context.Context) error) error { + return c.conn.Transaction(ctx, txFn) +} + +// CreateMigrationHistoryTable creates a new migration history table. +func (c *Clickhouse) CreateMigrationHistoryTable(ctx context.Context) error { + var q string + if c.options.ClusterName == "" { + q = fmt.Sprintf( + ` + CREATE TABLE %s ( + version String, + date Date DEFAULT toDate(apply_time), + apply_time UInt32, + is_deleted UInt8 + ) ENGINE = ReplacingMergeTree(apply_time) + PRIMARY KEY (version) + PARTITION BY (toYYYYMM(date)) + ORDER BY (version) + SETTINGS index_granularity=8192 + `, + c.options.TableName, + ) + } else { + q = fmt.Sprintf( + ` + CREATE TABLE %[1]s ON CLUSTER %[2]s ( + version String, + date Date DEFAULT toDate(apply_time), + apply_time UInt32, + is_deleted UInt8 + ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/%[2]s_%[1]s', '{replica}', apply_time) + PRIMARY KEY (version) + PARTITION BY (toYYYYMM(date)) + ORDER BY (version) + SETTINGS index_granularity=8192 + `, + c.options.TableName, + c.options.ClusterName, + ) + } + + if _, err := c.conn.ExecContext(ctx, q); err != nil { + return errors.Wrap(c.dbError(err, q), "create migration history table") + } + + return nil +} + +// DropMigrationHistoryTable drops the migration history table. +func (c *Clickhouse) DropMigrationHistoryTable(ctx context.Context) error { + q := fmt.Sprintf(`DROP TABLE %s`, c.options.TableName) + if _, err := c.conn.ExecContext(ctx, q); err != nil { + return errors.Wrap(c.dbError(err, q), "drop migration history table") + } + return nil +} + +// MigrationsCount returns the number of migrations +func (c *Clickhouse) MigrationsCount(ctx context.Context) (int, error) { + q := fmt.Sprintf(`SELECT count(*) FROM %s WHERE is_deleted = 0`, c.options.TableName) + rows, err := c.conn.QueryContext(ctx, q) + if err != nil { + return 0, err + } + var count int + if rows.Next() { + if err := rows.Scan(&count); err != nil { + return 0, c.dbError(err, q) + } + } + if err := rows.Err(); err != nil { + return 0, c.dbError(err, q) + } + return count, nil +} + +func (c *Clickhouse) TableNameWithSchema() string { + return c.options.SchemaName + "." + c.options.TableName +} + +func (c *Clickhouse) ForceSafely() bool { + return true +} + +// insertMigration inserts migration record. +func (c *Clickhouse) insertMigration(ctx context.Context, version string, isDeleted bool) error { + q := fmt.Sprintf(` + INSERT INTO %s (version, apply_time, is_deleted) + VALUES(?, ?, ?)`, + c.options.TableName, + ) + + now := uint32(time.Now().Unix()) + var isDeletedInt int + if isDeleted { + isDeletedInt = 1 + } + + if err := c.ExecQueryTransaction(ctx, func(ctx context.Context) error { + return c.ExecQuery(ctx, q, version, now, isDeletedInt) + }); err != nil { + return errors.Wrap(c.dbError(err, q), "insert migration") + } + + return c.optimizeTable(ctx) +} + +// optimizeTable optimizes tables. +func (c *Clickhouse) optimizeTable(ctx context.Context) error { + var q string + if c.options.ClusterName == "" { + q = fmt.Sprintf("OPTIMIZE TABLE %s FINAL", c.options.TableName) + } else { + q = fmt.Sprintf("OPTIMIZE TABLE %s ON CLUSTER %s FINAL", c.options.TableName, c.options.ClusterName) + } + if _, err := c.conn.ExecContext(ctx, q); err != nil { + return errors.Wrap(c.dbError(err, q), "optimize table") + } + + return nil +} + +// dbError returns DBError is err is db error else returns got error. +func (c *Clickhouse) dbError(err error, q string) error { + var clickEx *clickhouse.Exception + if !errors.As(err, &clickEx) { + return err + } + + return &DBError{ + Code: string(clickEx.Code), + Message: clickEx.Message, + Details: clickEx.StackTrace, + InternalQuery: q, + } +} diff --git a/internal/dal/repository/dependencies.go b/internal/dal/repository/dependencies.go new file mode 100644 index 0000000..985dc97 --- /dev/null +++ b/internal/dal/repository/dependencies.go @@ -0,0 +1,18 @@ +package repository + +import ( + "context" + "database/sql" + + "github.com/raoptimus/db-migrator.go/internal/dal/connection" +) + +//go:generate mockery --name=Connection --outpkg=mockrepository --output=./mockrepository +type Connection interface { + DSN() string + Driver() connection.Driver + Ping() error + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + Transaction(ctx context.Context, txFn func(ctx context.Context) error) error +} diff --git a/internal/dal/repository/errors.go b/internal/dal/repository/errors.go new file mode 100644 index 0000000..2f69a54 --- /dev/null +++ b/internal/dal/repository/errors.go @@ -0,0 +1,42 @@ +package repository + +import ( + "strings" +) + +type DBError struct { + Code string + Severity string + Message string + Details string + InternalQuery string +} + +func (d *DBError) Error() string { + var sb strings.Builder + sb.WriteString("SQLSTATE[") + sb.WriteString(d.Code) + sb.WriteString("]: ") + + if d.Severity != "" { + sb.WriteString(d.Severity) + sb.WriteString(": ") + } + + sb.WriteString(d.Message) + sb.WriteString("\n") + + if d.InternalQuery != "" { + sb.WriteString("The SQL being executed was: ") + sb.WriteString(d.InternalQuery) + sb.WriteString("\n") + } + + if d.Details != "" { + sb.WriteString("Details: ") + sb.WriteString(d.Details) + sb.WriteString("\n") + } + + return strings.TrimRight(sb.String(), "\n") +} diff --git a/internal/dal/repository/mockrepository/Connection.go b/internal/dal/repository/mockrepository/Connection.go new file mode 100644 index 0000000..f69721c --- /dev/null +++ b/internal/dal/repository/mockrepository/Connection.go @@ -0,0 +1,339 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockrepository + +import ( + context "context" + + connection "github.com/raoptimus/db-migrator.go/internal/dal/connection" + + mock "github.com/stretchr/testify/mock" + + sql "database/sql" +) + +// Connection is an autogenerated mock type for the Connection type +type Connection struct { + mock.Mock +} + +type Connection_Expecter struct { + mock *mock.Mock +} + +func (_m *Connection) EXPECT() *Connection_Expecter { + return &Connection_Expecter{mock: &_m.Mock} +} + +// DSN provides a mock function with given fields: +func (_m *Connection) DSN() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Connection_DSN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DSN' +type Connection_DSN_Call struct { + *mock.Call +} + +// DSN is a helper method to define mock.On call +func (_e *Connection_Expecter) DSN() *Connection_DSN_Call { + return &Connection_DSN_Call{Call: _e.mock.On("DSN")} +} + +func (_c *Connection_DSN_Call) Run(run func()) *Connection_DSN_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Connection_DSN_Call) Return(_a0 string) *Connection_DSN_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Connection_DSN_Call) RunAndReturn(run func() string) *Connection_DSN_Call { + _c.Call.Return(run) + return _c +} + +// Driver provides a mock function with given fields: +func (_m *Connection) Driver() connection.Driver { + ret := _m.Called() + + var r0 connection.Driver + if rf, ok := ret.Get(0).(func() connection.Driver); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(connection.Driver) + } + + return r0 +} + +// Connection_Driver_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Driver' +type Connection_Driver_Call struct { + *mock.Call +} + +// Driver is a helper method to define mock.On call +func (_e *Connection_Expecter) Driver() *Connection_Driver_Call { + return &Connection_Driver_Call{Call: _e.mock.On("Driver")} +} + +func (_c *Connection_Driver_Call) Run(run func()) *Connection_Driver_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Connection_Driver_Call) Return(_a0 connection.Driver) *Connection_Driver_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Connection_Driver_Call) RunAndReturn(run func() connection.Driver) *Connection_Driver_Call { + _c.Call.Return(run) + return _c +} + +// ExecContext provides a mock function with given fields: ctx, query, args +func (_m *Connection) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 sql.Result + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (sql.Result, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) sql.Result); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Connection_ExecContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecContext' +type Connection_ExecContext_Call struct { + *mock.Call +} + +// ExecContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Connection_Expecter) ExecContext(ctx interface{}, query interface{}, args ...interface{}) *Connection_ExecContext_Call { + return &Connection_ExecContext_Call{Call: _e.mock.On("ExecContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Connection_ExecContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Connection_ExecContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Connection_ExecContext_Call) Return(_a0 sql.Result, _a1 error) *Connection_ExecContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Connection_ExecContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (sql.Result, error)) *Connection_ExecContext_Call { + _c.Call.Return(run) + return _c +} + +// Ping provides a mock function with given fields: +func (_m *Connection) Ping() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Connection_Ping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ping' +type Connection_Ping_Call struct { + *mock.Call +} + +// Ping is a helper method to define mock.On call +func (_e *Connection_Expecter) Ping() *Connection_Ping_Call { + return &Connection_Ping_Call{Call: _e.mock.On("Ping")} +} + +func (_c *Connection_Ping_Call) Run(run func()) *Connection_Ping_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Connection_Ping_Call) Return(_a0 error) *Connection_Ping_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Connection_Ping_Call) RunAndReturn(run func() error) *Connection_Ping_Call { + _c.Call.Return(run) + return _c +} + +// QueryContext provides a mock function with given fields: ctx, query, args +func (_m *Connection) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 *sql.Rows + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (*sql.Rows, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Rows); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Rows) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Connection_QueryContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryContext' +type Connection_QueryContext_Call struct { + *mock.Call +} + +// QueryContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Connection_Expecter) QueryContext(ctx interface{}, query interface{}, args ...interface{}) *Connection_QueryContext_Call { + return &Connection_QueryContext_Call{Call: _e.mock.On("QueryContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Connection_QueryContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Connection_QueryContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Connection_QueryContext_Call) Return(_a0 *sql.Rows, _a1 error) *Connection_QueryContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Connection_QueryContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (*sql.Rows, error)) *Connection_QueryContext_Call { + _c.Call.Return(run) + return _c +} + +// Transaction provides a mock function with given fields: ctx, txFn +func (_m *Connection) Transaction(ctx context.Context, txFn func(context.Context) error) error { + ret := _m.Called(ctx, txFn) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, func(context.Context) error) error); ok { + r0 = rf(ctx, txFn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Connection_Transaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Transaction' +type Connection_Transaction_Call struct { + *mock.Call +} + +// Transaction is a helper method to define mock.On call +// - ctx context.Context +// - txFn func(context.Context) error +func (_e *Connection_Expecter) Transaction(ctx interface{}, txFn interface{}) *Connection_Transaction_Call { + return &Connection_Transaction_Call{Call: _e.mock.On("Transaction", ctx, txFn)} +} + +func (_c *Connection_Transaction_Call) Run(run func(ctx context.Context, txFn func(context.Context) error)) *Connection_Transaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(func(context.Context) error)) + }) + return _c +} + +func (_c *Connection_Transaction_Call) Return(_a0 error) *Connection_Transaction_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Connection_Transaction_Call) RunAndReturn(run func(context.Context, func(context.Context) error) error) *Connection_Transaction_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewConnection interface { + mock.TestingT + Cleanup(func()) +} + +// NewConnection creates a new instance of Connection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConnection(t mockConstructorTestingTNewConnection) *Connection { + mock := &Connection{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/dal/repository/mysql.go b/internal/dal/repository/mysql.go new file mode 100644 index 0000000..8f9384a --- /dev/null +++ b/internal/dal/repository/mysql.go @@ -0,0 +1,204 @@ +package repository + +import ( + "context" + "database/sql" + "fmt" + "strconv" + "time" + + "github.com/go-sql-driver/mysql" + "github.com/pkg/errors" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" +) + +type MySQL struct { + conn Connection + options *Options +} + +func NewMySQL(conn Connection, options *Options) *MySQL { + return &MySQL{ + conn: conn, + options: options, + } +} + +// Migrations returns applied migrations history. +func (m *MySQL) Migrations(ctx context.Context, limit int) (entity.Migrations, error) { + var ( + q = fmt.Sprintf( + ` + SELECT version, apply_time + FROM %s + ORDER BY apply_time DESC, version DESC + LIMIT ?`, + m.options.TableName, + ) + migrations entity.Migrations + ) + + rows, err := m.conn.QueryContext(ctx, q, limit) + if err != nil { + return nil, errors.Wrap(m.dbError(err, q), "get migrations") + } + for rows.Next() { + var ( + version string + applyTime int + ) + + if err := rows.Scan(&version, &applyTime); err != nil { + return nil, errors.Wrap(m.dbError(err, q), "get migrations") + } + + migrations = append(migrations, + entity.Migration{ + Version: version, + ApplyTime: applyTime, + }, + ) + } + + if err := rows.Err(); err != nil { + return nil, errors.Wrap(m.dbError(err, q), "get migrations") + } + + return migrations, nil +} + +// HasMigrationHistoryTable returns true if migration history table exists. +func (m *MySQL) HasMigrationHistoryTable(ctx context.Context) (exists bool, err error) { + var ( + q = ` + SELECT EXISTS( + SELECT * + FROM information_schema.tables + WHERE table_name = ? AND table_schema = ? + ) + ` + rows *sql.Rows + ) + + rows, err = m.conn.QueryContext(ctx, q, m.options.TableName, m.options.SchemaName) + if err != nil { + return false, errors.Wrap(m.dbError(err, q), "get schema table") + } + + for rows.Next() { + if err := rows.Scan(&exists); err != nil { + return false, errors.Wrap(m.dbError(err, q), "get schema table") + } + } + + if err = rows.Err(); err != nil { + return false, errors.Wrap(m.dbError(err, q), "get schema table") + } + + return exists, nil +} + +// InsertMigration inserts the new migration record. +func (m *MySQL) InsertMigration(ctx context.Context, version string) error { + q := fmt.Sprintf(` + INSERT INTO %s (version, apply_time) + VALUES (?, ?)`, + m.options.TableName, + ) + now := uint32(time.Now().Unix()) + if _, err := m.conn.ExecContext(ctx, q, version, now); err != nil { + return errors.Wrap(m.dbError(err, q), "insert migration") + } + return nil +} + +// RemoveMigration removes the migration record. +func (m *MySQL) RemoveMigration(ctx context.Context, version string) error { + q := fmt.Sprintf(`DELETE FROM %s WHERE version = ?`, m.options.TableName) + if _, err := m.conn.ExecContext(ctx, q, version); err != nil { + return errors.Wrap(m.dbError(err, q), "remove migration") + } + return nil +} + +// ExecQuery executes a query without returning any rows. +// The args are for any placeholder parameters in the query. +func (m *MySQL) ExecQuery(ctx context.Context, query string, args ...any) error { + _, err := m.conn.ExecContext(ctx, query, args...) + return err +} + +// ExecQueryTransaction executes a query in transaction without returning any rows. +// The args are for any placeholder parameters in the query. +func (m *MySQL) ExecQueryTransaction(ctx context.Context, txFn func(ctx context.Context) error) error { + return m.conn.Transaction(ctx, txFn) +} + +// CreateMigrationHistoryTable creates a new migration history table. +func (m *MySQL) CreateMigrationHistoryTable(ctx context.Context) error { + q := fmt.Sprintf( + ` + CREATE TABLE %s ( + version VARCHAR(180) PRIMARY KEY, + apply_time INT + ) + ENGINE=InnoDB + `, + m.options.TableName, + ) + + if _, err := m.conn.ExecContext(ctx, q); err != nil { + return errors.Wrap(m.dbError(err, q), "create migration history table") + } + return nil +} + +// DropMigrationHistoryTable drops the migration history table. +func (m *MySQL) DropMigrationHistoryTable(ctx context.Context) error { + q := fmt.Sprintf(`DROP TABLE %s`, m.options.TableName) + if _, err := m.conn.ExecContext(ctx, q); err != nil { + return errors.Wrap(m.dbError(err, q), "drop migration history table") + } + return nil +} + +// MigrationsCount returns the number of migrations +func (m *MySQL) MigrationsCount(ctx context.Context) (int, error) { + q := fmt.Sprintf(`SELECT count(*) FROM %s`, m.options.TableName) + rows, err := m.conn.QueryContext(ctx, q) + if err != nil { + return 0, err + } + var count int + if rows.Next() { + if err := rows.Scan(&count); err != nil { + return 0, m.dbError(err, q) + } + } + if err := rows.Err(); err != nil { + return 0, m.dbError(err, q) + } + return count, nil +} + +func (m *MySQL) TableNameWithSchema() string { + return m.options.SchemaName + "." + m.options.TableName +} + +func (m *MySQL) ForceSafely() bool { + return false +} + +// dbError returns DBError is err is db error else returns got error. +func (m *MySQL) dbError(err error, q string) error { + var mysqlErr *mysql.MySQLError + if !errors.As(err, &mysqlErr) { + return err + } + + return &DBError{ + Code: strconv.Itoa(int(mysqlErr.Number)), + Message: mysqlErr.Message, + InternalQuery: q, + } +} diff --git a/internal/dal/repository/options.go b/internal/dal/repository/options.go new file mode 100644 index 0000000..d5f6122 --- /dev/null +++ b/internal/dal/repository/options.go @@ -0,0 +1,8 @@ +package repository + +type Options struct { + TableName string + SchemaName string + ClusterName string + ShardName string +} diff --git a/internal/dal/repository/postgres.go b/internal/dal/repository/postgres.go new file mode 100644 index 0000000..23d1cb7 --- /dev/null +++ b/internal/dal/repository/postgres.go @@ -0,0 +1,223 @@ +package repository + +import ( + "context" + "database/sql" + "fmt" + "time" + + "github.com/lib/pq" + "github.com/pkg/errors" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" +) + +const postgresDefaultSchema = "public" + +type Postgres struct { + conn Connection + options *Options +} + +func NewPostgres(conn Connection, options *Options) *Postgres { + return &Postgres{ + conn: conn, + options: options, + } +} + +// Migrations returns applied migrations history. +func (p *Postgres) Migrations(ctx context.Context, limit int) (entity.Migrations, error) { + var ( + q = fmt.Sprintf( + ` + SELECT version, apply_time + FROM %s + ORDER BY apply_time DESC, version DESC + LIMIT $1`, + p.TableNameWithSchema(), + ) + migrations entity.Migrations + ) + + rows, err := p.conn.QueryContext(ctx, q, limit) + if err != nil { + return nil, errors.Wrap(p.dbError(err, q), "get migrations") + } + for rows.Next() { + var ( + version string + applyTime int + ) + + if err := rows.Scan(&version, &applyTime); err != nil { + return nil, errors.Wrap(p.dbError(err, q), "get migrations") + } + + migrations = append(migrations, + entity.Migration{ + Version: version, + ApplyTime: applyTime, + }, + ) + } + + if err := rows.Err(); err != nil { + return nil, errors.Wrap(p.dbError(err, q), "get migrations") + } + + return migrations, nil +} + +// HasMigrationHistoryTable returns true if migration history table exists. +func (p *Postgres) HasMigrationHistoryTable(ctx context.Context) (exists bool, err error) { + var ( + q = ` + SELECT + d.nspname AS table_schema, + c.relname AS table_name + FROM pg_class c + LEFT JOIN pg_namespace d ON d.oid = c.relnamespace + WHERE (c.relname, d.nspname) = ($1, $2) + ` + rows *sql.Rows + ) + + rows, err = p.conn.QueryContext(ctx, q, p.options.TableName, p.options.SchemaName) + if err != nil { + return false, errors.Wrap(p.dbError(err, q), "get schema table") + } + + for rows.Next() { + var ( + tableName string + schema string + ) + if err := rows.Scan(&schema, &tableName); err != nil { + return false, errors.Wrap(p.dbError(err, q), "get schema table") + } + + //todo: scan columns to tableScheme + if tableName == p.options.TableName { + return true, nil + } + } + + if err := rows.Err(); err != nil { + return false, errors.Wrap(p.dbError(err, q), "get schema table") + } + + return false, nil +} + +// InsertMigration inserts the new migration record. +func (p *Postgres) InsertMigration(ctx context.Context, version string) error { + q := fmt.Sprintf(` + INSERT INTO %s (version, apply_time) + VALUES ($1, $2)`, + p.TableNameWithSchema(), + ) + now := uint32(time.Now().Unix()) + if _, err := p.conn.ExecContext(ctx, q, version, now); err != nil { + return errors.Wrap(p.dbError(err, q), "insert migration") + } + return nil +} + +// RemoveMigration removes the migration record. +func (p *Postgres) RemoveMigration(ctx context.Context, version string) error { + q := fmt.Sprintf(`DELETE FROM %s WHERE (version) = ($1)`, p.TableNameWithSchema()) + if _, err := p.conn.ExecContext(ctx, q, version); err != nil { + return errors.Wrap(p.dbError(err, q), "remove migration") + } + return nil +} + +// ExecQuery executes a query without returning any rows. +// The args are for any placeholder parameters in the query. +func (p *Postgres) ExecQuery(ctx context.Context, query string, args ...any) error { + fmt.Println(query) + if _, err := p.conn.ExecContext(ctx, query, args...); err != nil { + return p.dbError(err, query) + } + return nil +} + +// ExecQueryTransaction executes a query in transaction without returning any rows. +// The args are for any placeholder parameters in the query. +func (p *Postgres) ExecQueryTransaction(ctx context.Context, txFn func(ctx context.Context) error) error { + return p.conn.Transaction(ctx, txFn) +} + +// CreateMigrationHistoryTable creates a new migration history table. +func (p *Postgres) CreateMigrationHistoryTable(ctx context.Context) error { + q := fmt.Sprintf( + ` + CREATE TABLE %s ( + version varchar(180) PRIMARY KEY, + apply_time integer + ) + `, + p.TableNameWithSchema(), + ) + + if _, err := p.conn.ExecContext(ctx, q); err != nil { + return errors.Wrap(p.dbError(err, q), "create migration history table") + } + return nil +} + +// DropMigrationHistoryTable drops the migration history table. +func (p *Postgres) DropMigrationHistoryTable(ctx context.Context) error { + q := fmt.Sprintf(`DROP TABLE %s`, p.TableNameWithSchema()) + if _, err := p.conn.ExecContext(ctx, q); err != nil { + return errors.Wrap(p.dbError(err, q), "drop migration history table") + } + return nil +} + +// MigrationsCount returns the number of migrations +func (p *Postgres) MigrationsCount(ctx context.Context) (int, error) { + q := fmt.Sprintf(`SELECT count(*) FROM %s`, p.TableNameWithSchema()) + rows, err := p.conn.QueryContext(ctx, q) + if err != nil { + return 0, err + } + var count int + if rows.Next() { + if err := rows.Scan(&count); err != nil { + return 0, p.dbError(err, q) + } + } + if err := rows.Err(); err != nil { + return 0, p.dbError(err, q) + } + return count, nil +} + +func (p *Postgres) TableNameWithSchema() string { + return p.options.SchemaName + "." + p.options.TableName +} + +func (p *Postgres) ForceSafely() bool { + return false +} + +// dbError returns DBError is err is db error else returns got error. +func (p *Postgres) dbError(err error, q string) error { + var pgErr *pq.Error + if !errors.As(err, &pgErr) { + return err + } + + if q == "" { + q = pgErr.InternalQuery + } + + return &DBError{ + Code: pgErr.SQLState(), + Severity: pgErr.Severity, + Message: pgErr.Message, + Details: pgErr.Detail, + InternalQuery: q, + } +} diff --git a/internal/dal/repository/postgres_test.go b/internal/dal/repository/postgres_test.go new file mode 100644 index 0000000..7656536 --- /dev/null +++ b/internal/dal/repository/postgres_test.go @@ -0,0 +1,44 @@ +package repository + +import ( + "context" + "testing" + + "github.com/lib/pq" + "github.com/raoptimus/db-migrator.go/internal/dal/connection" + "github.com/raoptimus/db-migrator.go/internal/dal/repository/mockrepository" + "github.com/stretchr/testify/assert" +) + +func TestPostgres_ExecQuery_Successfully(t *testing.T) { + ctx := context.Background() + conn := mockrepository.NewConnection(t) + conn.EXPECT(). + Driver(). + Return(connection.DriverPostgres) + conn.EXPECT(). + ExecContext(ctx, "SELECT 1"). + Return(nil, nil) + + repo, err := New(conn, &Options{}) + assert.NoError(t, err) + err = repo.ExecQuery(ctx, "SELECT 1") + assert.NoError(t, err) +} + +func TestPostgres_ExecQuery_Failure(t *testing.T) { + ctx := context.Background() + conn := mockrepository.NewConnection(t) + conn.EXPECT(). + Driver(). + Return(connection.DriverPostgres) + conn.EXPECT(). + ExecContext(ctx, "SELECT 1"). + Return(nil, &pq.Error{Severity: pq.Efatal}) + + repo, err := New(conn, &Options{}) + assert.NoError(t, err) + err = repo.ExecQuery(ctx, "SELECT 1") + assert.Error(t, err) + assert.Equal(t, err, &DBError{Severity: pq.Efatal, InternalQuery: "SELECT 1"}) +} diff --git a/internal/dal/repository/repository.go b/internal/dal/repository/repository.go new file mode 100644 index 0000000..fcf6786 --- /dev/null +++ b/internal/dal/repository/repository.go @@ -0,0 +1,12 @@ +package repository + +// New creates repository by connection +func New(conn Connection, options *Options) (*Repository, error) { + r, err := create(conn, options) + if err != nil { + return nil, err + } + return &Repository{ + adapter: r, + }, nil +} diff --git a/internal/domain/model/migration.go b/internal/domain/model/migration.go new file mode 100644 index 0000000..06bbdd5 --- /dev/null +++ b/internal/domain/model/migration.go @@ -0,0 +1,10 @@ +package model + +type Migration struct { + Version string + ApplyTime int + BodySQL string + ExecutedSQL string + Release string +} +type Migrations []Migration diff --git a/iofile/file.go b/internal/iofile/file.go similarity index 58% rename from iofile/file.go rename to internal/iofile/file.go index 0cddf7a..fb5c1f9 100644 --- a/iofile/file.go +++ b/internal/iofile/file.go @@ -1,13 +1,20 @@ /** * This file is part of the raoptimus/db-migrator.go library * - * @copyright Copyright (c) Evgeniy Urvantsev + * @copyright Copyright (c) Evgeniy Urvantsev * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md * @link https://github.com/raoptimus/db-migrator.go */ + package iofile -import "os" +import ( + "os" + + "github.com/pkg/errors" +) + +const fileModeExecutable = 0o755 func Exists(path string) bool { if _, err := os.Stat(path); os.IsNotExist(err) { @@ -21,13 +28,22 @@ func CreateDirectory(path string) error { return nil } - return os.Mkdir(path, 0755) + if err := os.Mkdir(path, fileModeExecutable); err != nil { + return errors.Wrapf(err, "creating directory %s", path) + } + + return nil } func CreateFile(filename string) error { f, err := os.Create(filename) + if err == nil { + err = f.Close() + } + if err != nil { - return err + return errors.Wrapf(err, "creating file %s", filename) } - return f.Close() + + return nil } diff --git a/internal/migrator/db_service.go b/internal/migrator/db_service.go new file mode 100644 index 0000000..349bae1 --- /dev/null +++ b/internal/migrator/db_service.go @@ -0,0 +1,149 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package migrator + +import ( + "github.com/raoptimus/db-migrator.go/internal/action" + "github.com/raoptimus/db-migrator.go/internal/builder" + "github.com/raoptimus/db-migrator.go/internal/dal/connection" + "github.com/raoptimus/db-migrator.go/internal/dal/repository" + "github.com/raoptimus/db-migrator.go/internal/service" + "github.com/raoptimus/db-migrator.go/pkg/console" + "github.com/raoptimus/db-migrator.go/pkg/iohelp" + "github.com/raoptimus/db-migrator.go/pkg/timex" +) + +type ( + DBService struct { + options *Options + fileNameBuilder FileNameBuilder + migrationServiceFunc func() (*service.Migration, error) + + conn *connection.Connection + repo *repository.Repository + } + Options struct { + DSN string + Directory string + TableName string + ClusterName string + Compact bool + Interactive bool + MaxSQLOutputLength int + } +) + +func New(options *Options) *DBService { + fb := builder.NewFileName(iohelp.StdFile, options.Directory) + dbs := &DBService{ + options: options, + fileNameBuilder: fb, + } + dbs.migrationServiceFunc = dbs.migrationService + return dbs +} + +func (s *DBService) Create() *action.Create { + return action.NewCreate( + timex.StdTime, + iohelp.StdFile, + console.Std, + s.fileNameBuilder, + s.options.Directory, + ) +} + +func (s *DBService) Upgrade() (*action.Upgrade, error) { + serv, err := s.migrationServiceFunc() + if err != nil { + return nil, err + } + + return action.NewUpgrade( + console.Std, + serv, + s.fileNameBuilder, + s.options.Interactive, + ), nil +} + +func (s *DBService) Downgrade() (*action.Downgrade, error) { + serv, err := s.migrationServiceFunc() + if err != nil { + return nil, err + } + return action.NewDowngrade(serv, s.fileNameBuilder, s.options.Interactive), nil +} + +func (s *DBService) To() (*action.To, error) { + serv, err := s.migrationServiceFunc() + if err != nil { + return nil, err + } + return action.NewTo(serv, s.fileNameBuilder, s.options.Interactive), nil +} + +func (s *DBService) History() (*action.History, error) { + serv, err := s.migrationServiceFunc() + if err != nil { + return nil, err + } + return action.NewHistory(serv), nil +} + +func (s *DBService) HistoryNew() (*action.HistoryNew, error) { + serv, err := s.migrationServiceFunc() + if err != nil { + return nil, err + } + return action.NewHistoryNew(serv), nil +} + +func (s *DBService) Redo() (*action.Redo, error) { + serv, err := s.migrationServiceFunc() + if err != nil { + return nil, err + } + return action.NewRedo(serv, s.fileNameBuilder, s.options.Interactive), nil +} + +func (s *DBService) migrationService() (*service.Migration, error) { + if s.conn == nil { + conn, err := connection.New(s.options.DSN) + if err != nil { + return nil, err + } + s.conn = conn + } + + if s.repo == nil { + repo, err := repository.New( + s.conn, + &repository.Options{ + TableName: s.options.TableName, + ClusterName: s.options.ClusterName, + }, + ) + if err != nil { + return nil, err + } + s.repo = repo + } + + return service.NewMigration( + &service.Options{ + MaxSQLOutputLength: s.options.MaxSQLOutputLength, + Directory: s.options.Directory, + Compact: s.options.Compact, + }, + console.Std, + iohelp.StdFile, + s.repo, + ), nil +} diff --git a/internal/migrator/db_service_integration_test.go b/internal/migrator/db_service_integration_test.go new file mode 100644 index 0000000..49aba58 --- /dev/null +++ b/internal/migrator/db_service_integration_test.go @@ -0,0 +1,113 @@ +package migrator + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/raoptimus/db-migrator.go/internal/dal/repository" + "github.com/stretchr/testify/assert" +) + +func TestIntegrationDBService_UpDown_Successfully(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + // region data provider + tests := []struct { + name string + countMigrationsSQL string + options *Options + }{ + { + name: "postgres", + options: &Options{ + DSN: os.Getenv("POSTGRES_DSN"), + Directory: migrationsPathAbs(os.Getenv("POSTGRES_MIGRATIONS_PATH")), + TableName: "migration", + Compact: true, + Interactive: false, + }, + }, + { + name: "mysql", + options: &Options{ + DSN: os.Getenv("MYSQL_DSN"), + Directory: migrationsPathAbs(os.Getenv("MYSQL_MIGRATIONS_PATH")), + TableName: "migration", + Compact: true, + Interactive: false, + }, + }, + { + name: "clickhouse", + options: &Options{ + DSN: os.Getenv("CLICKHOUSE_DSN"), + Directory: migrationsPathAbs(os.Getenv("CLICKHOUSE_MIGRATIONS_PATH")), + TableName: "migration", + Compact: true, + Interactive: false, + }, + }, + { + name: "clickhouse_cluster", + options: &Options{ + DSN: os.Getenv("CLICKHOUSE_CLUSTER_DSN1"), + Directory: migrationsPathAbs(os.Getenv("CLICKHOUSE_CLUSTER_MIGRATIONS_PATH")), + TableName: "migration", + ClusterName: os.Getenv("CLICKHOUSE_CLUSTER_NAME"), + Compact: true, + Interactive: false, + }, + }, + } + // endregion + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + dbServ := New(tt.options) + down, err := dbServ.Downgrade() + assert.NoError(t, err) + up, err := dbServ.Upgrade() + assert.NoError(t, err) + + defer func() { + _ = down.Run(cliContext(t, "all")) + }() + + err = up.Run(cliContext(t, "2")) + assert.NoError(t, err) + assertEqualRowsCount(t, ctx, dbServ.repo, 3) + + err = up.Run(cliContext(t, "1")) // migration with error + assert.Error(t, err) + assertEqualRowsCount(t, ctx, dbServ.repo, 3) + err = dbServ.repo.ExecQuery(ctx, "select * from test") // checks table exists + assert.NoError(t, err) + + err = down.Run(cliContext(t, "all")) + assert.NoError(t, err) + assertEqualRowsCount(t, ctx, dbServ.repo, 1) + }) + } +} + +func assertEqualRowsCount( + t *testing.T, + ctx context.Context, + repo *repository.Repository, + expected int, +) { + count, err := repo.MigrationsCount(ctx) + assert.NoError(t, err) + assert.Equal(t, expected, count) +} + +func migrationsPathAbs(basePath string) string { + path, _ := filepath.Abs("../../" + basePath) + return path +} diff --git a/internal/migrator/db_service_test.go b/internal/migrator/db_service_test.go new file mode 100644 index 0000000..137f3bb --- /dev/null +++ b/internal/migrator/db_service_test.go @@ -0,0 +1,83 @@ +package migrator + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" +) + +func TestDBService_Create_ReturnsAction(t *testing.T) { + dbServ := New(&Options{}) + action := dbServ.Create() + assert.NotNil(t, action) +} + +func TestDBService_Upgrade_ReturnsAction(t *testing.T) { + dbServ := New(&Options{ + DSN: "postgres://docker:docker@postgres:5432/docker?sslmode=disable", + }) + action, err := dbServ.Upgrade() + assert.NoError(t, err) + assert.NotNil(t, action) +} + +func TestDBService_Downgrade_ReturnsAction(t *testing.T) { + dbServ := New(&Options{ + DSN: "postgres://docker:docker@postgres:5432/docker?sslmode=disable", + }) + action, err := dbServ.Downgrade() + assert.NoError(t, err) + assert.NotNil(t, action) +} + +func TestDBService_To_ReturnsAction(t *testing.T) { + dbServ := New(&Options{ + DSN: "postgres://docker:docker@postgres:5432/docker?sslmode=disable", + }) + action, err := dbServ.To() + assert.NoError(t, err) + assert.NotNil(t, action) +} + +func TestDBService_History_ReturnsAction(t *testing.T) { + dbServ := New(&Options{ + DSN: "postgres://docker:docker@postgres:5432/docker?sslmode=disable", + }) + action, err := dbServ.History() + assert.NoError(t, err) + assert.NotNil(t, action) +} + +func TestDBService_HistoryNew_ReturnsAction(t *testing.T) { + dbServ := New(&Options{ + DSN: "postgres://docker:docker@postgres:5432/docker?sslmode=disable", + }) + action, err := dbServ.HistoryNew() + assert.NoError(t, err) + assert.NotNil(t, action) +} + +func TestDBService_Redo_ReturnsAction(t *testing.T) { + dbServ := New(&Options{ + DSN: "postgres://docker:docker@postgres:5432/docker?sslmode=disable", + }) + action, err := dbServ.Redo() + assert.NoError(t, err) + assert.NotNil(t, action) +} + +func flagSet(t *testing.T, argument string) *flag.FlagSet { + flagSet := flag.NewFlagSet("test", 0) + err := flagSet.Parse([]string{argument}) + assert.NoError(t, err) + + return flagSet +} + +func cliContext(t *testing.T, argument string) *cli.Context { + flagSet := flagSet(t, argument) + + return cli.NewContext(nil, flagSet, nil) +} diff --git a/internal/migrator/dependencies.go b/internal/migrator/dependencies.go new file mode 100644 index 0000000..b917429 --- /dev/null +++ b/internal/migrator/dependencies.go @@ -0,0 +1,26 @@ +package migrator + +import ( + "context" + "database/sql" + + "github.com/raoptimus/db-migrator.go/internal/dal/connection" +) + +//go:generate mockery --name=Connection --outpkg=mockmigrator --output=./mockmigrator +type Connection interface { + DSN() string + Driver() connection.Driver + Ping() error + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + Transaction(ctx context.Context, txFn func(ctx context.Context) error) error +} + +//go:generate mockery --name=FileNameBuilder --outpkg=mockmigrator --output=./mockmigrator +type FileNameBuilder interface { + // Up builds a file name for migration update + Up(version string, forceSafely bool) (fname string, safely bool) + // Down builds a file name for migration downgrade + Down(version string, forceSafely bool) (fname string, safely bool) +} diff --git a/internal/migrator/mockmigrator/Connection.go b/internal/migrator/mockmigrator/Connection.go new file mode 100644 index 0000000..0b255df --- /dev/null +++ b/internal/migrator/mockmigrator/Connection.go @@ -0,0 +1,298 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockmigrator + +import ( + context "context" + + connection "github.com/raoptimus/db-migrator.go/internal/dal/connection" + + mock "github.com/stretchr/testify/mock" + + sql "database/sql" +) + +// Connection is an autogenerated mock type for the Connection type +type Connection struct { + mock.Mock +} + +type Connection_Expecter struct { + mock *mock.Mock +} + +func (_m *Connection) EXPECT() *Connection_Expecter { + return &Connection_Expecter{mock: &_m.Mock} +} + +// DSN provides a mock function with given fields: +func (_m *Connection) DSN() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Connection_DSN_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DSN' +type Connection_DSN_Call struct { + *mock.Call +} + +// DSN is a helper method to define mock.On call +func (_e *Connection_Expecter) DSN() *Connection_DSN_Call { + return &Connection_DSN_Call{Call: _e.mock.On("DSN")} +} + +func (_c *Connection_DSN_Call) Run(run func()) *Connection_DSN_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Connection_DSN_Call) Return(_a0 string) *Connection_DSN_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Connection_DSN_Call) RunAndReturn(run func() string) *Connection_DSN_Call { + _c.Call.Return(run) + return _c +} + +// Driver provides a mock function with given fields: +func (_m *Connection) Driver() connection.Driver { + ret := _m.Called() + + var r0 connection.Driver + if rf, ok := ret.Get(0).(func() connection.Driver); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(connection.Driver) + } + + return r0 +} + +// Connection_Driver_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Driver' +type Connection_Driver_Call struct { + *mock.Call +} + +// Driver is a helper method to define mock.On call +func (_e *Connection_Expecter) Driver() *Connection_Driver_Call { + return &Connection_Driver_Call{Call: _e.mock.On("Driver")} +} + +func (_c *Connection_Driver_Call) Run(run func()) *Connection_Driver_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Connection_Driver_Call) Return(_a0 connection.Driver) *Connection_Driver_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Connection_Driver_Call) RunAndReturn(run func() connection.Driver) *Connection_Driver_Call { + _c.Call.Return(run) + return _c +} + +// ExecContext provides a mock function with given fields: ctx, query, args +func (_m *Connection) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 sql.Result + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (sql.Result, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) sql.Result); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(sql.Result) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Connection_ExecContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecContext' +type Connection_ExecContext_Call struct { + *mock.Call +} + +// ExecContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Connection_Expecter) ExecContext(ctx interface{}, query interface{}, args ...interface{}) *Connection_ExecContext_Call { + return &Connection_ExecContext_Call{Call: _e.mock.On("ExecContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Connection_ExecContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Connection_ExecContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Connection_ExecContext_Call) Return(_a0 sql.Result, _a1 error) *Connection_ExecContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Connection_ExecContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (sql.Result, error)) *Connection_ExecContext_Call { + _c.Call.Return(run) + return _c +} + +// QueryContext provides a mock function with given fields: ctx, query, args +func (_m *Connection) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 *sql.Rows + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) (*sql.Rows, error)); ok { + return rf(ctx, query, args...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) *sql.Rows); ok { + r0 = rf(ctx, query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Rows) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...interface{}) error); ok { + r1 = rf(ctx, query, args...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Connection_QueryContext_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueryContext' +type Connection_QueryContext_Call struct { + *mock.Call +} + +// QueryContext is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Connection_Expecter) QueryContext(ctx interface{}, query interface{}, args ...interface{}) *Connection_QueryContext_Call { + return &Connection_QueryContext_Call{Call: _e.mock.On("QueryContext", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Connection_QueryContext_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Connection_QueryContext_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Connection_QueryContext_Call) Return(_a0 *sql.Rows, _a1 error) *Connection_QueryContext_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Connection_QueryContext_Call) RunAndReturn(run func(context.Context, string, ...interface{}) (*sql.Rows, error)) *Connection_QueryContext_Call { + _c.Call.Return(run) + return _c +} + +// Transaction provides a mock function with given fields: ctx, txFn +func (_m *Connection) Transaction(ctx context.Context, txFn func(context.Context) error) error { + ret := _m.Called(ctx, txFn) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, func(context.Context) error) error); ok { + r0 = rf(ctx, txFn) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Connection_Transaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Transaction' +type Connection_Transaction_Call struct { + *mock.Call +} + +// Transaction is a helper method to define mock.On call +// - ctx context.Context +// - txFn func(context.Context) error +func (_e *Connection_Expecter) Transaction(ctx interface{}, txFn interface{}) *Connection_Transaction_Call { + return &Connection_Transaction_Call{Call: _e.mock.On("Transaction", ctx, txFn)} +} + +func (_c *Connection_Transaction_Call) Run(run func(ctx context.Context, txFn func(context.Context) error)) *Connection_Transaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(func(context.Context) error)) + }) + return _c +} + +func (_c *Connection_Transaction_Call) Return(_a0 error) *Connection_Transaction_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Connection_Transaction_Call) RunAndReturn(run func(context.Context, func(context.Context) error) error) *Connection_Transaction_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewConnection interface { + mock.TestingT + Cleanup(func()) +} + +// NewConnection creates a new instance of Connection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConnection(t mockConstructorTestingTNewConnection) *Connection { + mock := &Connection{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/migrator/mockmigrator/FileNameBuilder.go b/internal/migrator/mockmigrator/FileNameBuilder.go new file mode 100644 index 0000000..2be4aac --- /dev/null +++ b/internal/migrator/mockmigrator/FileNameBuilder.go @@ -0,0 +1,139 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockmigrator + +import mock "github.com/stretchr/testify/mock" + +// FileNameBuilder is an autogenerated mock type for the FileNameBuilder type +type FileNameBuilder struct { + mock.Mock +} + +type FileNameBuilder_Expecter struct { + mock *mock.Mock +} + +func (_m *FileNameBuilder) EXPECT() *FileNameBuilder_Expecter { + return &FileNameBuilder_Expecter{mock: &_m.Mock} +} + +// Down provides a mock function with given fields: version, forceSafely +func (_m *FileNameBuilder) Down(version string, forceSafely bool) (string, bool) { + ret := _m.Called(version, forceSafely) + + var r0 string + var r1 bool + if rf, ok := ret.Get(0).(func(string, bool) (string, bool)); ok { + return rf(version, forceSafely) + } + if rf, ok := ret.Get(0).(func(string, bool) string); ok { + r0 = rf(version, forceSafely) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, bool) bool); ok { + r1 = rf(version, forceSafely) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// FileNameBuilder_Down_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Down' +type FileNameBuilder_Down_Call struct { + *mock.Call +} + +// Down is a helper method to define mock.On call +// - version string +// - forceSafely bool +func (_e *FileNameBuilder_Expecter) Down(version interface{}, forceSafely interface{}) *FileNameBuilder_Down_Call { + return &FileNameBuilder_Down_Call{Call: _e.mock.On("Down", version, forceSafely)} +} + +func (_c *FileNameBuilder_Down_Call) Run(run func(version string, forceSafely bool)) *FileNameBuilder_Down_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *FileNameBuilder_Down_Call) Return(fname string, safely bool) *FileNameBuilder_Down_Call { + _c.Call.Return(fname, safely) + return _c +} + +func (_c *FileNameBuilder_Down_Call) RunAndReturn(run func(string, bool) (string, bool)) *FileNameBuilder_Down_Call { + _c.Call.Return(run) + return _c +} + +// Up provides a mock function with given fields: version, forceSafely +func (_m *FileNameBuilder) Up(version string, forceSafely bool) (string, bool) { + ret := _m.Called(version, forceSafely) + + var r0 string + var r1 bool + if rf, ok := ret.Get(0).(func(string, bool) (string, bool)); ok { + return rf(version, forceSafely) + } + if rf, ok := ret.Get(0).(func(string, bool) string); ok { + r0 = rf(version, forceSafely) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, bool) bool); ok { + r1 = rf(version, forceSafely) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// FileNameBuilder_Up_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Up' +type FileNameBuilder_Up_Call struct { + *mock.Call +} + +// Up is a helper method to define mock.On call +// - version string +// - forceSafely bool +func (_e *FileNameBuilder_Expecter) Up(version interface{}, forceSafely interface{}) *FileNameBuilder_Up_Call { + return &FileNameBuilder_Up_Call{Call: _e.mock.On("Up", version, forceSafely)} +} + +func (_c *FileNameBuilder_Up_Call) Run(run func(version string, forceSafely bool)) *FileNameBuilder_Up_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(bool)) + }) + return _c +} + +func (_c *FileNameBuilder_Up_Call) Return(fname string, safely bool) *FileNameBuilder_Up_Call { + _c.Call.Return(fname, safely) + return _c +} + +func (_c *FileNameBuilder_Up_Call) RunAndReturn(run func(string, bool) (string, bool)) *FileNameBuilder_Up_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewFileNameBuilder interface { + mock.TestingT + Cleanup(func()) +} + +// NewFileNameBuilder creates a new instance of FileNameBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFileNameBuilder(t mockConstructorTestingTNewFileNameBuilder) *FileNameBuilder { + mock := &FileNameBuilder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/service/dependencies.go b/internal/service/dependencies.go new file mode 100644 index 0000000..54b1008 --- /dev/null +++ b/internal/service/dependencies.go @@ -0,0 +1,63 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package service + +import ( + "context" + "io" + + "github.com/raoptimus/db-migrator.go/internal/dal/entity" +) + +//go:generate mockery --name=Console --outpkg=mockaction --output=./mockaction +type Console interface { + Confirm(s string) bool + Info(message string) + InfoLn(message string) + Infof(message string, a ...any) + Success(message string) + SuccessLn(message string) + Successf(message string, a ...any) + Warn(message string) + WarnLn(message string) + Warnf(message string, a ...any) + Error(message string) + ErrorLn(message string) + Errorf(message string, a ...any) + Fatal(err error) + NumberPlural(count int, one, many string) string +} + +//go:generate mockery --name=File --outpkg=mockservice --output=./mockservice +type File interface { + Exists(fileName string) (bool, error) + Open(filename string) (io.ReadCloser, error) + ReadAll(filename string) ([]byte, error) +} + +//go:generate mockery --name=Repository --outpkg=mockservice --output=./mockservice +type Repository interface { + // Migrations returns applied migrations history. + Migrations(ctx context.Context, limit int) (entity.Migrations, error) + // HasMigrationHistoryTable returns true if migration history table exists. + HasMigrationHistoryTable(ctx context.Context) (exists bool, err error) + // InsertMigration inserts the new migration record. + InsertMigration(ctx context.Context, version string) error + // RemoveMigration removes the migration record. + RemoveMigration(ctx context.Context, version string) error + // ExecQuery executes a query without returning any rows. + // The args are for any placeholder parameters in the query. + ExecQuery(ctx context.Context, query string, args ...any) error + ExecQueryTransaction(ctx context.Context, fnTx func(ctx context.Context) error) error + DropMigrationHistoryTable(ctx context.Context) error + CreateMigrationHistoryTable(ctx context.Context) error + MigrationsCount(ctx context.Context) (int, error) + TableNameWithSchema() string + ForceSafely() bool +} diff --git a/internal/service/errors.go b/internal/service/errors.go new file mode 100644 index 0000000..6d2a245 --- /dev/null +++ b/internal/service/errors.go @@ -0,0 +1,7 @@ +package service + +import ( + "github.com/pkg/errors" +) + +var ErrMigrationVersionReserved = errors.New("migration version reserved") diff --git a/internal/service/migration.go b/internal/service/migration.go new file mode 100644 index 0000000..8b951c4 --- /dev/null +++ b/internal/service/migration.go @@ -0,0 +1,327 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package service + +import ( + "context" + "fmt" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/pkg/errors" + _ "github.com/raoptimus/db-migrator.go/internal/console" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" + "github.com/raoptimus/db-migrator.go/pkg/sqlio" +) + +const ( + baseMigration = "000000_000000_base" + baseMigrationsCount = 1 + defaultLimit = 10000 + regexpFileNameGroupCount = 5 +) + +var ( + regexpFileName = regexp.MustCompile(`^(\d{6}_?\d{6}[A-Za-z0-9_]+)\.((safe)\.)?(up|down)\.sql$`) +) + +type Migration struct { + options *Options + console Console + file File + repo Repository +} + +func NewMigration( + options *Options, + console Console, + file File, + repo Repository, +) *Migration { + return &Migration{ + options: options, + console: console, + file: file, + repo: repo, + } +} + +func (m *Migration) InitializeTableHistory(ctx context.Context) error { + exists, err := m.repo.HasMigrationHistoryTable(ctx) + if err != nil { + return err + } + + if exists { + return nil + } + + m.console.Warnf("Creating migration history table %s...", m.repo.TableNameWithSchema()) + + if err := m.repo.CreateMigrationHistoryTable(ctx); err != nil { + return err + } + + if err := m.repo.InsertMigration(ctx, baseMigration); err != nil { + if err2 := m.repo.DropMigrationHistoryTable(ctx); err2 != nil { + return errors.Wrap(err, err2.Error()) + } + return err + } + + m.console.SuccessLn("Done") + return nil +} + +func (m *Migration) Migrations(ctx context.Context, limit int) (entity.Migrations, error) { + if err := m.InitializeTableHistory(ctx); err != nil { + return nil, err + } + if limit < 1 { + limit = defaultLimit + } + migrations, err := m.repo.Migrations(ctx, limit+baseMigrationsCount) + if err != nil { + return nil, err + } + + for i := range migrations { + if migrations[i].Version == baseMigration { + return append(migrations[:i], migrations[i+1:]...), nil + } + } + + return migrations, nil +} + +func (m *Migration) NewMigrations(ctx context.Context, limit int) (entity.Migrations, error) { + if err := m.InitializeTableHistory(ctx); err != nil { + return nil, err + } + if limit < 1 { + limit = defaultLimit + } + migrations, err := m.repo.Migrations(ctx, limit+baseMigrationsCount) + if err != nil { + return nil, err + } + + var files []string + files, err = filepath.Glob(filepath.Join(m.options.Directory, "*.up.sql")) + if err != nil { + return nil, err + } + + newMigrations := make(entity.Migrations, 0) + var baseFilename string + + for _, file := range files { + baseFilename = filepath.Base(file) + groups := regexpFileName.FindStringSubmatch(baseFilename) + if len(groups) != regexpFileNameGroupCount { + return nil, fmt.Errorf("file name %s is invalid", baseFilename) + } + found := false + for _, migration := range migrations { + if migration.Version == baseMigration { + continue + } + if migration.Version == groups[1] { + found = true + break + } + } + if !found { + newMigrations = append( + newMigrations, + entity.Migration{ + Version: groups[1], + }, + ) + } + } + + newMigrations.SortByVersion() + return newMigrations, err +} + +func (m *Migration) ApplySQL( + ctx context.Context, + safely bool, + version, + upSQL, + downSQL string, +) error { + if version == baseMigration { + return ErrMigrationVersionReserved + } + m.console.Warnf("*** applying %s\n", version) + scanner := sqlio.NewScanner(strings.NewReader(upSQL)) + elapsedTime, err := m.apply(ctx, scanner, safely) + if err != nil { + m.console.Errorf("*** failed to apply %s (time: %.3fs)\n", version, elapsedTime.Seconds()) + return err + } + if err := m.repo.InsertMigration(ctx, version); err != nil { + return err + } + // todo: save downSQL + m.console.Successf("*** applied %s (time: %.3fs)\n", version, elapsedTime.Seconds()) + return nil +} + +func (m *Migration) RevertSQL( + ctx context.Context, + safely bool, + version, + upSQL, + downSQL string, +) error { + if version == baseMigration { + return ErrMigrationVersionReserved + } + m.console.Warnf("*** reverting %s\n", version) + scanner := sqlio.NewScanner(strings.NewReader(downSQL)) + elapsedTime, err := m.apply(ctx, scanner, safely) + if err != nil { + m.console.Errorf("*** failed to reverted %s (time: %.3fs)\n", version, elapsedTime.Seconds()) + return err + } + if err := m.repo.RemoveMigration(ctx, version); err != nil { + return err + } + m.console.Warnf("*** reverted %s (time: %.3fs)\n", version, elapsedTime.Seconds()) + return nil +} + +func (m *Migration) ApplyFile(ctx context.Context, entity *entity.Migration, fileName string, safely bool) error { + if entity.Version == baseMigration { + return ErrMigrationVersionReserved + } + m.console.Warnf("*** applying %s\n", entity.Version) + scanner, err := m.scannerByFile(fileName) + if err != nil { + return err + } + elapsedTime, err := m.apply(ctx, scanner, safely) + if err != nil { + m.console.Errorf("*** failed to apply %s (time: %.3fs)\n", entity.Version, elapsedTime.Seconds()) + return err + } + if err := m.repo.InsertMigration(ctx, entity.Version); err != nil { + return err + } + m.console.Successf("*** applied %s (time: %.3fs)\n", entity.Version, elapsedTime.Seconds()) + return nil +} + +func (m *Migration) RevertFile(ctx context.Context, entity *entity.Migration, fileName string, safely bool) error { + if entity.Version == baseMigration { + return ErrMigrationVersionReserved + } + m.console.Warnf("*** reverting %s\n", entity.Version) + scanner, err := m.scannerByFile(fileName) + if err != nil { + return err + } + + elapsedTime, err := m.apply(ctx, scanner, safely) + if err != nil { + m.console.Errorf("*** failed to reverted %s (time: %.3fs)\n", + entity.Version, elapsedTime.Seconds()) + } + if err := m.repo.RemoveMigration(ctx, entity.Version); err != nil { + return err + } + m.console.Warnf("*** reverted %s (time: %.3fs)\n", + entity.Version, elapsedTime.Seconds()) + return nil +} + +func (m *Migration) BeginCommand(sqlQuery string) time.Time { + sqlQueryOutput := m.SQLQueryOutput(sqlQuery) + if !m.options.Compact { + m.console.Infof(" > execute SQL: %s ...\n", sqlQueryOutput) + } + + return time.Now() +} + +func (m *Migration) ExecQuery(ctx context.Context, sqlQuery string) error { + start := m.BeginCommand(sqlQuery) + if err := m.repo.ExecQuery(ctx, sqlQuery); err != nil { + return err + } + m.EndCommand(start) + + return nil +} + +func (m *Migration) SQLQueryOutput(sqlQuery string) string { + sqlQueryOutput := sqlQuery + if m.options.MaxSQLOutputLength > 0 && m.options.MaxSQLOutputLength < len(sqlQuery) { + sqlQueryOutput = sqlQuery[:m.options.MaxSQLOutputLength] + } + + return sqlQueryOutput +} + +func (m *Migration) EndCommand(start time.Time) { + if m.options.Compact { + m.console.Infof(" done (time: '%.3fs)\n", time.Since(start).Seconds()) + } +} + +func (m *Migration) apply( + ctx context.Context, + scanner *sqlio.Scanner, + safely bool, +) (time.Duration, error) { + start := time.Now() + processScanFunc := func(ctx context.Context) error { + var q string + for scanner.Scan() { + q = scanner.SQL() + if q == "" { + continue + } + if err := m.ExecQuery(ctx, q); err != nil { + return err + } + } + return scanner.Err() + } + + var err error + if m.repo.ForceSafely() || safely { + err = m.repo.ExecQueryTransaction(ctx, processScanFunc) + } else { + err = processScanFunc(ctx) + } + elapsedTime := time.Since(start) + return elapsedTime, err +} + +func (m *Migration) scannerByFile(fileName string) (*sqlio.Scanner, error) { + exists, err := m.file.Exists(fileName) + if err != nil { + return nil, errors.Wrapf(err, "migration file %s does not exists", fileName) + } + if !exists { + return nil, fmt.Errorf("migration file %s does not exists", fileName) + } + + f, err := m.file.Open(fileName) + if err != nil { + return nil, errors.Wrapf(err, "migration file %s does not read", fileName) + } + return sqlio.NewScanner(f), nil +} diff --git a/internal/service/migration_test.go b/internal/service/migration_test.go new file mode 100644 index 0000000..74850a2 --- /dev/null +++ b/internal/service/migration_test.go @@ -0,0 +1,98 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package service + +import ( + "context" + "io" + "strings" + "testing" + + "github.com/raoptimus/db-migrator.go/internal/action/mockaction" + "github.com/raoptimus/db-migrator.go/internal/dal/entity" + "github.com/raoptimus/db-migrator.go/internal/service/mockservice" + "github.com/raoptimus/db-migrator.go/pkg/console" + "github.com/stretchr/testify/assert" +) + +func TestMigration_BeginCommand(t *testing.T) { + repo := mockservice.NewRepository(t) + file := mockservice.NewFile(t) + c := mockaction.NewConsole(t) + c.EXPECT(). + Infof(" > execute SQL: %s ...\n", "select 1"). + Once() + serv := NewMigration(&Options{}, c, file, repo) + serv.BeginCommand("select 1") +} + +func TestMigration_ApplyFile_MultiSTMT_Successfully(t *testing.T) { + t.Skip("Skip") + ctx := context.Background() + fileName := "000000_000000_test.up.sql" + + sqlReader := strings.NewReader("select 1; select 2;") + sqlReaderCloser := io.NopCloser(sqlReader) + + file := mockservice.NewFile(t) + file.EXPECT().Exists(fileName).Return(true, nil) + file.EXPECT().Open(fileName).Return(sqlReaderCloser, nil) + + repo := mockservice.NewRepository(t) + repo.EXPECT(). + ExecQuery(ctx, "select 1"). + RunAndReturn(func(ctx context.Context, s string, i ...interface{}) error { + return nil + }). + Once() + repo.EXPECT(). + ExecQuery(ctx, "select 2"). + RunAndReturn(func(ctx context.Context, s string, i ...interface{}) error { + return nil + }). + Once() + repo.EXPECT(). + InsertMigration(ctx, "000000_000000_test"). + Return(nil) + + serv := NewMigration(&Options{}, console.NewDummy(true), file, repo) + err := serv.ApplyFile(ctx, + &entity.Migration{Version: "000000_000000_test"}, + fileName, + false, + ) + assert.NoError(t, err) +} + +func TestMigration_ApplyFile_SimpleSTMT_Successfully(t *testing.T) { + t.Skip("Skip") + ctx := context.Background() + fileName := "000000_000000_test.up.sql" + + file := mockservice.NewFile(t) + file.EXPECT().Exists(fileName).Return(true, nil) + file.EXPECT().ReadAll(fileName).Return([]byte("select 1;"), nil) + + repo := mockservice.NewRepository(t) + repo.EXPECT(). + ExecQuery(ctx, "select 1;"). + Return(nil) + repo.EXPECT(). + InsertMigration(ctx, "000000_000000_test"). + Return(nil) + + serv := NewMigration(&Options{}, console.NewDummy(true), file, repo) + + err := serv.ApplyFile(ctx, + &entity.Migration{Version: "000000_000000_test"}, + fileName, + false, + ) + assert.NoError(t, err) +} diff --git a/internal/service/mockaction/Console.go b/internal/service/mockaction/Console.go new file mode 100644 index 0000000..e3a32b0 --- /dev/null +++ b/internal/service/mockaction/Console.go @@ -0,0 +1,592 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockaction + +import mock "github.com/stretchr/testify/mock" + +// Console is an autogenerated mock type for the Console type +type Console struct { + mock.Mock +} + +type Console_Expecter struct { + mock *mock.Mock +} + +func (_m *Console) EXPECT() *Console_Expecter { + return &Console_Expecter{mock: &_m.Mock} +} + +// Confirm provides a mock function with given fields: s +func (_m *Console) Confirm(s string) bool { + ret := _m.Called(s) + + var r0 bool + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(s) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Console_Confirm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Confirm' +type Console_Confirm_Call struct { + *mock.Call +} + +// Confirm is a helper method to define mock.On call +// - s string +func (_e *Console_Expecter) Confirm(s interface{}) *Console_Confirm_Call { + return &Console_Confirm_Call{Call: _e.mock.On("Confirm", s)} +} + +func (_c *Console_Confirm_Call) Run(run func(s string)) *Console_Confirm_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Confirm_Call) Return(_a0 bool) *Console_Confirm_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Console_Confirm_Call) RunAndReturn(run func(string) bool) *Console_Confirm_Call { + _c.Call.Return(run) + return _c +} + +// Error provides a mock function with given fields: message +func (_m *Console) Error(message string) { + _m.Called(message) +} + +// Console_Error_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Error' +type Console_Error_Call struct { + *mock.Call +} + +// Error is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Error(message interface{}) *Console_Error_Call { + return &Console_Error_Call{Call: _e.mock.On("Error", message)} +} + +func (_c *Console_Error_Call) Run(run func(message string)) *Console_Error_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Error_Call) Return() *Console_Error_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Error_Call) RunAndReturn(run func(string)) *Console_Error_Call { + _c.Call.Return(run) + return _c +} + +// ErrorLn provides a mock function with given fields: message +func (_m *Console) ErrorLn(message string) { + _m.Called(message) +} + +// Console_ErrorLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ErrorLn' +type Console_ErrorLn_Call struct { + *mock.Call +} + +// ErrorLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) ErrorLn(message interface{}) *Console_ErrorLn_Call { + return &Console_ErrorLn_Call{Call: _e.mock.On("ErrorLn", message)} +} + +func (_c *Console_ErrorLn_Call) Run(run func(message string)) *Console_ErrorLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_ErrorLn_Call) Return() *Console_ErrorLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_ErrorLn_Call) RunAndReturn(run func(string)) *Console_ErrorLn_Call { + _c.Call.Return(run) + return _c +} + +// Errorf provides a mock function with given fields: message, a +func (_m *Console) Errorf(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Errorf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Errorf' +type Console_Errorf_Call struct { + *mock.Call +} + +// Errorf is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Errorf(message interface{}, a ...interface{}) *Console_Errorf_Call { + return &Console_Errorf_Call{Call: _e.mock.On("Errorf", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Errorf_Call) Run(run func(message string, a ...interface{})) *Console_Errorf_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Errorf_Call) Return() *Console_Errorf_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Errorf_Call) RunAndReturn(run func(string, ...interface{})) *Console_Errorf_Call { + _c.Call.Return(run) + return _c +} + +// Fatal provides a mock function with given fields: err +func (_m *Console) Fatal(err error) { + _m.Called(err) +} + +// Console_Fatal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fatal' +type Console_Fatal_Call struct { + *mock.Call +} + +// Fatal is a helper method to define mock.On call +// - err error +func (_e *Console_Expecter) Fatal(err interface{}) *Console_Fatal_Call { + return &Console_Fatal_Call{Call: _e.mock.On("Fatal", err)} +} + +func (_c *Console_Fatal_Call) Run(run func(err error)) *Console_Fatal_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(error)) + }) + return _c +} + +func (_c *Console_Fatal_Call) Return() *Console_Fatal_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Fatal_Call) RunAndReturn(run func(error)) *Console_Fatal_Call { + _c.Call.Return(run) + return _c +} + +// Info provides a mock function with given fields: message +func (_m *Console) Info(message string) { + _m.Called(message) +} + +// Console_Info_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Info' +type Console_Info_Call struct { + *mock.Call +} + +// Info is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Info(message interface{}) *Console_Info_Call { + return &Console_Info_Call{Call: _e.mock.On("Info", message)} +} + +func (_c *Console_Info_Call) Run(run func(message string)) *Console_Info_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Info_Call) Return() *Console_Info_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Info_Call) RunAndReturn(run func(string)) *Console_Info_Call { + _c.Call.Return(run) + return _c +} + +// InfoLn provides a mock function with given fields: message +func (_m *Console) InfoLn(message string) { + _m.Called(message) +} + +// Console_InfoLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InfoLn' +type Console_InfoLn_Call struct { + *mock.Call +} + +// InfoLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) InfoLn(message interface{}) *Console_InfoLn_Call { + return &Console_InfoLn_Call{Call: _e.mock.On("InfoLn", message)} +} + +func (_c *Console_InfoLn_Call) Run(run func(message string)) *Console_InfoLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_InfoLn_Call) Return() *Console_InfoLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_InfoLn_Call) RunAndReturn(run func(string)) *Console_InfoLn_Call { + _c.Call.Return(run) + return _c +} + +// Infof provides a mock function with given fields: message, a +func (_m *Console) Infof(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Infof_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Infof' +type Console_Infof_Call struct { + *mock.Call +} + +// Infof is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Infof(message interface{}, a ...interface{}) *Console_Infof_Call { + return &Console_Infof_Call{Call: _e.mock.On("Infof", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Infof_Call) Run(run func(message string, a ...interface{})) *Console_Infof_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Infof_Call) Return() *Console_Infof_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Infof_Call) RunAndReturn(run func(string, ...interface{})) *Console_Infof_Call { + _c.Call.Return(run) + return _c +} + +// NumberPlural provides a mock function with given fields: count, one, many +func (_m *Console) NumberPlural(count int, one string, many string) string { + ret := _m.Called(count, one, many) + + var r0 string + if rf, ok := ret.Get(0).(func(int, string, string) string); ok { + r0 = rf(count, one, many) + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Console_NumberPlural_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NumberPlural' +type Console_NumberPlural_Call struct { + *mock.Call +} + +// NumberPlural is a helper method to define mock.On call +// - count int +// - one string +// - many string +func (_e *Console_Expecter) NumberPlural(count interface{}, one interface{}, many interface{}) *Console_NumberPlural_Call { + return &Console_NumberPlural_Call{Call: _e.mock.On("NumberPlural", count, one, many)} +} + +func (_c *Console_NumberPlural_Call) Run(run func(count int, one string, many string)) *Console_NumberPlural_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(int), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *Console_NumberPlural_Call) Return(_a0 string) *Console_NumberPlural_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Console_NumberPlural_Call) RunAndReturn(run func(int, string, string) string) *Console_NumberPlural_Call { + _c.Call.Return(run) + return _c +} + +// Success provides a mock function with given fields: message +func (_m *Console) Success(message string) { + _m.Called(message) +} + +// Console_Success_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Success' +type Console_Success_Call struct { + *mock.Call +} + +// Success is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Success(message interface{}) *Console_Success_Call { + return &Console_Success_Call{Call: _e.mock.On("Success", message)} +} + +func (_c *Console_Success_Call) Run(run func(message string)) *Console_Success_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Success_Call) Return() *Console_Success_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Success_Call) RunAndReturn(run func(string)) *Console_Success_Call { + _c.Call.Return(run) + return _c +} + +// SuccessLn provides a mock function with given fields: message +func (_m *Console) SuccessLn(message string) { + _m.Called(message) +} + +// Console_SuccessLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SuccessLn' +type Console_SuccessLn_Call struct { + *mock.Call +} + +// SuccessLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) SuccessLn(message interface{}) *Console_SuccessLn_Call { + return &Console_SuccessLn_Call{Call: _e.mock.On("SuccessLn", message)} +} + +func (_c *Console_SuccessLn_Call) Run(run func(message string)) *Console_SuccessLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_SuccessLn_Call) Return() *Console_SuccessLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_SuccessLn_Call) RunAndReturn(run func(string)) *Console_SuccessLn_Call { + _c.Call.Return(run) + return _c +} + +// Successf provides a mock function with given fields: message, a +func (_m *Console) Successf(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Successf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Successf' +type Console_Successf_Call struct { + *mock.Call +} + +// Successf is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Successf(message interface{}, a ...interface{}) *Console_Successf_Call { + return &Console_Successf_Call{Call: _e.mock.On("Successf", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Successf_Call) Run(run func(message string, a ...interface{})) *Console_Successf_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Successf_Call) Return() *Console_Successf_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Successf_Call) RunAndReturn(run func(string, ...interface{})) *Console_Successf_Call { + _c.Call.Return(run) + return _c +} + +// Warn provides a mock function with given fields: message +func (_m *Console) Warn(message string) { + _m.Called(message) +} + +// Console_Warn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warn' +type Console_Warn_Call struct { + *mock.Call +} + +// Warn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) Warn(message interface{}) *Console_Warn_Call { + return &Console_Warn_Call{Call: _e.mock.On("Warn", message)} +} + +func (_c *Console_Warn_Call) Run(run func(message string)) *Console_Warn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_Warn_Call) Return() *Console_Warn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Warn_Call) RunAndReturn(run func(string)) *Console_Warn_Call { + _c.Call.Return(run) + return _c +} + +// WarnLn provides a mock function with given fields: message +func (_m *Console) WarnLn(message string) { + _m.Called(message) +} + +// Console_WarnLn_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WarnLn' +type Console_WarnLn_Call struct { + *mock.Call +} + +// WarnLn is a helper method to define mock.On call +// - message string +func (_e *Console_Expecter) WarnLn(message interface{}) *Console_WarnLn_Call { + return &Console_WarnLn_Call{Call: _e.mock.On("WarnLn", message)} +} + +func (_c *Console_WarnLn_Call) Run(run func(message string)) *Console_WarnLn_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *Console_WarnLn_Call) Return() *Console_WarnLn_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_WarnLn_Call) RunAndReturn(run func(string)) *Console_WarnLn_Call { + _c.Call.Return(run) + return _c +} + +// Warnf provides a mock function with given fields: message, a +func (_m *Console) Warnf(message string, a ...interface{}) { + var _ca []interface{} + _ca = append(_ca, message) + _ca = append(_ca, a...) + _m.Called(_ca...) +} + +// Console_Warnf_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Warnf' +type Console_Warnf_Call struct { + *mock.Call +} + +// Warnf is a helper method to define mock.On call +// - message string +// - a ...interface{} +func (_e *Console_Expecter) Warnf(message interface{}, a ...interface{}) *Console_Warnf_Call { + return &Console_Warnf_Call{Call: _e.mock.On("Warnf", + append([]interface{}{message}, a...)...)} +} + +func (_c *Console_Warnf_Call) Run(run func(message string, a ...interface{})) *Console_Warnf_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(string), variadicArgs...) + }) + return _c +} + +func (_c *Console_Warnf_Call) Return() *Console_Warnf_Call { + _c.Call.Return() + return _c +} + +func (_c *Console_Warnf_Call) RunAndReturn(run func(string, ...interface{})) *Console_Warnf_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewConsole interface { + mock.TestingT + Cleanup(func()) +} + +// NewConsole creates a new instance of Console. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewConsole(t mockConstructorTestingTNewConsole) *Console { + mock := &Console{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/service/mockservice/File.go b/internal/service/mockservice/File.go new file mode 100644 index 0000000..7db92ac --- /dev/null +++ b/internal/service/mockservice/File.go @@ -0,0 +1,197 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockservice + +import ( + io "io" + + mock "github.com/stretchr/testify/mock" +) + +// File is an autogenerated mock type for the File type +type File struct { + mock.Mock +} + +type File_Expecter struct { + mock *mock.Mock +} + +func (_m *File) EXPECT() *File_Expecter { + return &File_Expecter{mock: &_m.Mock} +} + +// Exists provides a mock function with given fields: fileName +func (_m *File) Exists(fileName string) (bool, error) { + ret := _m.Called(fileName) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(string) (bool, error)); ok { + return rf(fileName) + } + if rf, ok := ret.Get(0).(func(string) bool); ok { + r0 = rf(fileName) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(fileName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// File_Exists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exists' +type File_Exists_Call struct { + *mock.Call +} + +// Exists is a helper method to define mock.On call +// - fileName string +func (_e *File_Expecter) Exists(fileName interface{}) *File_Exists_Call { + return &File_Exists_Call{Call: _e.mock.On("Exists", fileName)} +} + +func (_c *File_Exists_Call) Run(run func(fileName string)) *File_Exists_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *File_Exists_Call) Return(_a0 bool, _a1 error) *File_Exists_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *File_Exists_Call) RunAndReturn(run func(string) (bool, error)) *File_Exists_Call { + _c.Call.Return(run) + return _c +} + +// Open provides a mock function with given fields: filename +func (_m *File) Open(filename string) (io.ReadCloser, error) { + ret := _m.Called(filename) + + var r0 io.ReadCloser + var r1 error + if rf, ok := ret.Get(0).(func(string) (io.ReadCloser, error)); ok { + return rf(filename) + } + if rf, ok := ret.Get(0).(func(string) io.ReadCloser); ok { + r0 = rf(filename) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.ReadCloser) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(filename) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// File_Open_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Open' +type File_Open_Call struct { + *mock.Call +} + +// Open is a helper method to define mock.On call +// - filename string +func (_e *File_Expecter) Open(filename interface{}) *File_Open_Call { + return &File_Open_Call{Call: _e.mock.On("Open", filename)} +} + +func (_c *File_Open_Call) Run(run func(filename string)) *File_Open_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *File_Open_Call) Return(_a0 io.ReadCloser, _a1 error) *File_Open_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *File_Open_Call) RunAndReturn(run func(string) (io.ReadCloser, error)) *File_Open_Call { + _c.Call.Return(run) + return _c +} + +// ReadAll provides a mock function with given fields: filename +func (_m *File) ReadAll(filename string) ([]byte, error) { + ret := _m.Called(filename) + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(string) ([]byte, error)); ok { + return rf(filename) + } + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(filename) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(filename) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// File_ReadAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadAll' +type File_ReadAll_Call struct { + *mock.Call +} + +// ReadAll is a helper method to define mock.On call +// - filename string +func (_e *File_Expecter) ReadAll(filename interface{}) *File_ReadAll_Call { + return &File_ReadAll_Call{Call: _e.mock.On("ReadAll", filename)} +} + +func (_c *File_ReadAll_Call) Run(run func(filename string)) *File_ReadAll_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string)) + }) + return _c +} + +func (_c *File_ReadAll_Call) Return(_a0 []byte, _a1 error) *File_ReadAll_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *File_ReadAll_Call) RunAndReturn(run func(string) ([]byte, error)) *File_ReadAll_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewFile interface { + mock.TestingT + Cleanup(func()) +} + +// NewFile creates a new instance of File. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFile(t mockConstructorTestingTNewFile) *File { + mock := &File{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/service/mockservice/Repository.go b/internal/service/mockservice/Repository.go new file mode 100644 index 0000000..dcf3178 --- /dev/null +++ b/internal/service/mockservice/Repository.go @@ -0,0 +1,546 @@ +// Code generated by mockery. DO NOT EDIT. + +package mockservice + +import ( + context "context" + + entity "github.com/raoptimus/db-migrator.go/internal/dal/entity" + mock "github.com/stretchr/testify/mock" +) + +// Repository is an autogenerated mock type for the Repository type +type Repository struct { + mock.Mock +} + +type Repository_Expecter struct { + mock *mock.Mock +} + +func (_m *Repository) EXPECT() *Repository_Expecter { + return &Repository_Expecter{mock: &_m.Mock} +} + +// CreateMigrationHistoryTable provides a mock function with given fields: ctx +func (_m *Repository) CreateMigrationHistoryTable(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Repository_CreateMigrationHistoryTable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateMigrationHistoryTable' +type Repository_CreateMigrationHistoryTable_Call struct { + *mock.Call +} + +// CreateMigrationHistoryTable is a helper method to define mock.On call +// - ctx context.Context +func (_e *Repository_Expecter) CreateMigrationHistoryTable(ctx interface{}) *Repository_CreateMigrationHistoryTable_Call { + return &Repository_CreateMigrationHistoryTable_Call{Call: _e.mock.On("CreateMigrationHistoryTable", ctx)} +} + +func (_c *Repository_CreateMigrationHistoryTable_Call) Run(run func(ctx context.Context)) *Repository_CreateMigrationHistoryTable_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Repository_CreateMigrationHistoryTable_Call) Return(_a0 error) *Repository_CreateMigrationHistoryTable_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_CreateMigrationHistoryTable_Call) RunAndReturn(run func(context.Context) error) *Repository_CreateMigrationHistoryTable_Call { + _c.Call.Return(run) + return _c +} + +// DropMigrationHistoryTable provides a mock function with given fields: ctx +func (_m *Repository) DropMigrationHistoryTable(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Repository_DropMigrationHistoryTable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DropMigrationHistoryTable' +type Repository_DropMigrationHistoryTable_Call struct { + *mock.Call +} + +// DropMigrationHistoryTable is a helper method to define mock.On call +// - ctx context.Context +func (_e *Repository_Expecter) DropMigrationHistoryTable(ctx interface{}) *Repository_DropMigrationHistoryTable_Call { + return &Repository_DropMigrationHistoryTable_Call{Call: _e.mock.On("DropMigrationHistoryTable", ctx)} +} + +func (_c *Repository_DropMigrationHistoryTable_Call) Run(run func(ctx context.Context)) *Repository_DropMigrationHistoryTable_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Repository_DropMigrationHistoryTable_Call) Return(_a0 error) *Repository_DropMigrationHistoryTable_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_DropMigrationHistoryTable_Call) RunAndReturn(run func(context.Context) error) *Repository_DropMigrationHistoryTable_Call { + _c.Call.Return(run) + return _c +} + +// ExecQuery provides a mock function with given fields: ctx, query, args +func (_m *Repository) ExecQuery(ctx context.Context, query string, args ...interface{}) error { + var _ca []interface{} + _ca = append(_ca, ctx, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...interface{}) error); ok { + r0 = rf(ctx, query, args...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Repository_ExecQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecQuery' +type Repository_ExecQuery_Call struct { + *mock.Call +} + +// ExecQuery is a helper method to define mock.On call +// - ctx context.Context +// - query string +// - args ...interface{} +func (_e *Repository_Expecter) ExecQuery(ctx interface{}, query interface{}, args ...interface{}) *Repository_ExecQuery_Call { + return &Repository_ExecQuery_Call{Call: _e.mock.On("ExecQuery", + append([]interface{}{ctx, query}, args...)...)} +} + +func (_c *Repository_ExecQuery_Call) Run(run func(ctx context.Context, query string, args ...interface{})) *Repository_ExecQuery_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]interface{}, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(interface{}) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *Repository_ExecQuery_Call) Return(_a0 error) *Repository_ExecQuery_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_ExecQuery_Call) RunAndReturn(run func(context.Context, string, ...interface{}) error) *Repository_ExecQuery_Call { + _c.Call.Return(run) + return _c +} + +// ExecQueryTransaction provides a mock function with given fields: ctx, fnTx +func (_m *Repository) ExecQueryTransaction(ctx context.Context, fnTx func(context.Context) error) error { + ret := _m.Called(ctx, fnTx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, func(context.Context) error) error); ok { + r0 = rf(ctx, fnTx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Repository_ExecQueryTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ExecQueryTransaction' +type Repository_ExecQueryTransaction_Call struct { + *mock.Call +} + +// ExecQueryTransaction is a helper method to define mock.On call +// - ctx context.Context +// - fnTx func(context.Context) error +func (_e *Repository_Expecter) ExecQueryTransaction(ctx interface{}, fnTx interface{}) *Repository_ExecQueryTransaction_Call { + return &Repository_ExecQueryTransaction_Call{Call: _e.mock.On("ExecQueryTransaction", ctx, fnTx)} +} + +func (_c *Repository_ExecQueryTransaction_Call) Run(run func(ctx context.Context, fnTx func(context.Context) error)) *Repository_ExecQueryTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(func(context.Context) error)) + }) + return _c +} + +func (_c *Repository_ExecQueryTransaction_Call) Return(_a0 error) *Repository_ExecQueryTransaction_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_ExecQueryTransaction_Call) RunAndReturn(run func(context.Context, func(context.Context) error) error) *Repository_ExecQueryTransaction_Call { + _c.Call.Return(run) + return _c +} + +// ForceSafely provides a mock function with given fields: +func (_m *Repository) ForceSafely() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Repository_ForceSafely_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ForceSafely' +type Repository_ForceSafely_Call struct { + *mock.Call +} + +// ForceSafely is a helper method to define mock.On call +func (_e *Repository_Expecter) ForceSafely() *Repository_ForceSafely_Call { + return &Repository_ForceSafely_Call{Call: _e.mock.On("ForceSafely")} +} + +func (_c *Repository_ForceSafely_Call) Run(run func()) *Repository_ForceSafely_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Repository_ForceSafely_Call) Return(_a0 bool) *Repository_ForceSafely_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_ForceSafely_Call) RunAndReturn(run func() bool) *Repository_ForceSafely_Call { + _c.Call.Return(run) + return _c +} + +// HasMigrationHistoryTable provides a mock function with given fields: ctx +func (_m *Repository) HasMigrationHistoryTable(ctx context.Context) (bool, error) { + ret := _m.Called(ctx) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (bool, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) bool); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repository_HasMigrationHistoryTable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HasMigrationHistoryTable' +type Repository_HasMigrationHistoryTable_Call struct { + *mock.Call +} + +// HasMigrationHistoryTable is a helper method to define mock.On call +// - ctx context.Context +func (_e *Repository_Expecter) HasMigrationHistoryTable(ctx interface{}) *Repository_HasMigrationHistoryTable_Call { + return &Repository_HasMigrationHistoryTable_Call{Call: _e.mock.On("HasMigrationHistoryTable", ctx)} +} + +func (_c *Repository_HasMigrationHistoryTable_Call) Run(run func(ctx context.Context)) *Repository_HasMigrationHistoryTable_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Repository_HasMigrationHistoryTable_Call) Return(exists bool, err error) *Repository_HasMigrationHistoryTable_Call { + _c.Call.Return(exists, err) + return _c +} + +func (_c *Repository_HasMigrationHistoryTable_Call) RunAndReturn(run func(context.Context) (bool, error)) *Repository_HasMigrationHistoryTable_Call { + _c.Call.Return(run) + return _c +} + +// InsertMigration provides a mock function with given fields: ctx, version +func (_m *Repository) InsertMigration(ctx context.Context, version string) error { + ret := _m.Called(ctx, version) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, version) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Repository_InsertMigration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertMigration' +type Repository_InsertMigration_Call struct { + *mock.Call +} + +// InsertMigration is a helper method to define mock.On call +// - ctx context.Context +// - version string +func (_e *Repository_Expecter) InsertMigration(ctx interface{}, version interface{}) *Repository_InsertMigration_Call { + return &Repository_InsertMigration_Call{Call: _e.mock.On("InsertMigration", ctx, version)} +} + +func (_c *Repository_InsertMigration_Call) Run(run func(ctx context.Context, version string)) *Repository_InsertMigration_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Repository_InsertMigration_Call) Return(_a0 error) *Repository_InsertMigration_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_InsertMigration_Call) RunAndReturn(run func(context.Context, string) error) *Repository_InsertMigration_Call { + _c.Call.Return(run) + return _c +} + +// Migrations provides a mock function with given fields: ctx, limit +func (_m *Repository) Migrations(ctx context.Context, limit int) (entity.Migrations, error) { + ret := _m.Called(ctx, limit) + + var r0 entity.Migrations + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (entity.Migrations, error)); ok { + return rf(ctx, limit) + } + if rf, ok := ret.Get(0).(func(context.Context, int) entity.Migrations); ok { + r0 = rf(ctx, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(entity.Migrations) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, limit) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repository_Migrations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Migrations' +type Repository_Migrations_Call struct { + *mock.Call +} + +// Migrations is a helper method to define mock.On call +// - ctx context.Context +// - limit int +func (_e *Repository_Expecter) Migrations(ctx interface{}, limit interface{}) *Repository_Migrations_Call { + return &Repository_Migrations_Call{Call: _e.mock.On("Migrations", ctx, limit)} +} + +func (_c *Repository_Migrations_Call) Run(run func(ctx context.Context, limit int)) *Repository_Migrations_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int)) + }) + return _c +} + +func (_c *Repository_Migrations_Call) Return(_a0 entity.Migrations, _a1 error) *Repository_Migrations_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Repository_Migrations_Call) RunAndReturn(run func(context.Context, int) (entity.Migrations, error)) *Repository_Migrations_Call { + _c.Call.Return(run) + return _c +} + +// MigrationsCount provides a mock function with given fields: ctx +func (_m *Repository) MigrationsCount(ctx context.Context) (int, error) { + ret := _m.Called(ctx) + + var r0 int + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (int, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) int); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repository_MigrationsCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MigrationsCount' +type Repository_MigrationsCount_Call struct { + *mock.Call +} + +// MigrationsCount is a helper method to define mock.On call +// - ctx context.Context +func (_e *Repository_Expecter) MigrationsCount(ctx interface{}) *Repository_MigrationsCount_Call { + return &Repository_MigrationsCount_Call{Call: _e.mock.On("MigrationsCount", ctx)} +} + +func (_c *Repository_MigrationsCount_Call) Run(run func(ctx context.Context)) *Repository_MigrationsCount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *Repository_MigrationsCount_Call) Return(_a0 int, _a1 error) *Repository_MigrationsCount_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Repository_MigrationsCount_Call) RunAndReturn(run func(context.Context) (int, error)) *Repository_MigrationsCount_Call { + _c.Call.Return(run) + return _c +} + +// RemoveMigration provides a mock function with given fields: ctx, version +func (_m *Repository) RemoveMigration(ctx context.Context, version string) error { + ret := _m.Called(ctx, version) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, version) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Repository_RemoveMigration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveMigration' +type Repository_RemoveMigration_Call struct { + *mock.Call +} + +// RemoveMigration is a helper method to define mock.On call +// - ctx context.Context +// - version string +func (_e *Repository_Expecter) RemoveMigration(ctx interface{}, version interface{}) *Repository_RemoveMigration_Call { + return &Repository_RemoveMigration_Call{Call: _e.mock.On("RemoveMigration", ctx, version)} +} + +func (_c *Repository_RemoveMigration_Call) Run(run func(ctx context.Context, version string)) *Repository_RemoveMigration_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Repository_RemoveMigration_Call) Return(_a0 error) *Repository_RemoveMigration_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_RemoveMigration_Call) RunAndReturn(run func(context.Context, string) error) *Repository_RemoveMigration_Call { + _c.Call.Return(run) + return _c +} + +// TableNameWithSchema provides a mock function with given fields: +func (_m *Repository) TableNameWithSchema() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Repository_TableNameWithSchema_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TableNameWithSchema' +type Repository_TableNameWithSchema_Call struct { + *mock.Call +} + +// TableNameWithSchema is a helper method to define mock.On call +func (_e *Repository_Expecter) TableNameWithSchema() *Repository_TableNameWithSchema_Call { + return &Repository_TableNameWithSchema_Call{Call: _e.mock.On("TableNameWithSchema")} +} + +func (_c *Repository_TableNameWithSchema_Call) Run(run func()) *Repository_TableNameWithSchema_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Repository_TableNameWithSchema_Call) Return(_a0 string) *Repository_TableNameWithSchema_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Repository_TableNameWithSchema_Call) RunAndReturn(run func() string) *Repository_TableNameWithSchema_Call { + _c.Call.Return(run) + return _c +} + +type mockConstructorTestingTNewRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewRepository(t mockConstructorTestingTNewRepository) *Repository { + mock := &Repository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/migrator/migrator_to.go b/internal/service/options.go similarity index 53% rename from migrator/migrator_to.go rename to internal/service/options.go index 89a2e5d..d0c2e23 100644 --- a/migrator/migrator_to.go +++ b/internal/service/options.go @@ -1,15 +1,15 @@ /** * This file is part of the raoptimus/db-migrator.go library * - * @copyright Copyright (c) Evgeniy Urvantsev + * @copyright Copyright (c) Evgeniy Urvantsev * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md * @link https://github.com/raoptimus/db-migrator.go */ -package migrator -import "fmt" +package service -func (s *Service) To(version string) error { - fmt.Println("coming soon") - return nil +type Options struct { + MaxSQLOutputLength int + Directory string + Compact bool } diff --git a/migrator/db/clickhouseMigration/helpers.go b/migrator/db/clickhouseMigration/helpers.go deleted file mode 100644 index 207808c..0000000 --- a/migrator/db/clickhouseMigration/helpers.go +++ /dev/null @@ -1,28 +0,0 @@ -package clickhouseMigration - -import ( - "fmt" - "net/url" - "path" -) - -func NormalizeDSN(dsn string) (string, error) { - dsnUrl, err := url.Parse(dsn) - if err != nil { - return dsn, err - } - - password, _ := dsnUrl.User.Password() - hostWithPort := dsnUrl.Host - if dsnUrl.Port() == "" { - hostWithPort += ":9000" - } - - return fmt.Sprintf("tcp://%s?username=%s&password=%s&database=%s&%s", - hostWithPort, - dsnUrl.User.Username(), - password, - path.Base(dsnUrl.Path), - dsnUrl.RawQuery, - ), nil -} diff --git a/migrator/db/clickhouseMigration/migration.go b/migrator/db/clickhouseMigration/migration.go deleted file mode 100644 index 54a9ec3..0000000 --- a/migrator/db/clickhouseMigration/migration.go +++ /dev/null @@ -1,275 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package clickhouseMigration - -import ( - "database/sql" - "fmt" - "github.com/ClickHouse/clickhouse-go" - "github.com/raoptimus/db-migrator.go/console" - "github.com/raoptimus/db-migrator.go/migrator/db" - "log" - "time" -) - -type ( - Migration struct { - connection *sql.DB - tableName string - directory string - clusterName string - } -) - -func New(connection *sql.DB, tableName, clusterName, directory string) *Migration { - return &Migration{ - connection: connection, - tableName: tableName, - clusterName: clusterName, - directory: directory, - } -} - -func (s *Migration) ConvertError(err error, query string) error { - if exception, ok := err.(*clickhouse.Exception); ok { - return fmt.Errorf("exception: [%d] %s \n%s\n", exception.Code, exception.Message, exception.StackTrace) - } - - return fmt.Errorf("exception: %v", err) -} - -func (s *Migration) InitializeTableHistory() error { - exists, err := s.getTableScheme() - if err != nil { - return err - } - - if !exists { - if err := s.createMigrationHistoryTable(); err != nil { - return err - } - } - - return nil -} - -func (s *Migration) GetMigrationHistory(limit int) (db.MigrationEntityList, error) { - if err := s.InitializeTableHistory(); err != nil { - return nil, err - } - - var ( - q = fmt.Sprintf( - ` - SELECT version, apply_time - FROM %s - WHERE is_deleted = 0 - ORDER BY apply_time DESC, version DESC - LIMIT ?`, - s.tableName, - ) - result db.MigrationEntityList - ) - if limit < 1 { - limit = db.DefaultLimit - } - - rows, err := s.connection.Query(q, limit) - if err != nil { - return nil, err - } - for rows.Next() { - var ( - version string - applyTime int - ) - - if err := rows.Scan(&version, &applyTime); err != nil { - return nil, err - } - - if version == db.BaseMigration { - continue - } - result = append(result, - db.MigrationEntity{ - Version: version, - ApplyTime: applyTime, - }, - ) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return result, nil -} - -func (s *Migration) AddMigrationHistory(version string) error { - now := uint32(time.Now().Unix()) - q := fmt.Sprintf(` - INSERT INTO %s (version, apply_time, is_deleted) - VALUES(?, ?, ?)`, - s.tableName, - ) - tx, err := s.connection.Begin() - if err != nil { - return err - } - - stmt, err := tx.Prepare(q) - if err != nil { - return err - } - - if _, err := stmt.Exec(version, now, 0); err != nil { - tx.Rollback() - - return err - } - if err := tx.Commit(); err != nil { - return err - } - - return s.optimizeTable() -} - -func (s *Migration) RemoveMigrationHistory(version string) error { - now := uint32(time.Now().Unix()) - q := fmt.Sprintf(` - INSERT INTO %s (version, apply_time, is_deleted) - VALUES(?, ?, ?)`, - s.tableName, - ) - tx, err := s.connection.Begin() - if err != nil { - return err - } - - stmt, err := tx.Prepare(q) - if err != nil { - return err - } - - if _, err := stmt.Exec(version, now, 1); err != nil { - tx.Rollback() - - return err - } - if err := tx.Commit(); err != nil { - return err - } - - return s.optimizeTable() -} - -func (s *Migration) optimizeTable() error { - var sqlQuery string - if s.clusterName == "" { - sqlQuery = fmt.Sprintf("OPTIMIZE TABLE %s FINAL", s.tableName) - } else { - sqlQuery = fmt.Sprintf("OPTIMIZE TABLE %s ON CLUSTER %s FINAL", s.tableName, s.clusterName) - } - _, err := s.connection.Exec(sqlQuery) - - return err -} - -func (s *Migration) createMigrationHistoryTable() error { - log.Printf(console.Yellow("Creating migration history table %s..."), s.tableName) - var sqlQuery string - if s.clusterName == "" { - sqlQuery = fmt.Sprintf( - ` - CREATE TABLE %s ( - version String, - date Date DEFAULT toDate(apply_time), - apply_time UInt32, - is_deleted UInt8 - ) ENGINE = ReplacingMergeTree(apply_time) - PRIMARY KEY (version) - PARTITION BY (toYYYYMM(date)) - ORDER BY (version) - SETTINGS index_granularity=8192 - `, - s.tableName, - ) - } else { - sqlQuery = fmt.Sprintf( - ` - CREATE TABLE %s ON CLUSTER %s ( - version String, - date Date DEFAULT toDate(apply_time), - apply_time UInt32, - is_deleted UInt8 - ) ENGINE = ReplicatedReplacingMergeTree('/clickhouse/tables/{shard}/%s_%s', '{replica}', apply_time) - PRIMARY KEY (version) - PARTITION BY (toYYYYMM(date)) - ORDER BY (version) - SETTINGS index_granularity=8192 - `, - s.tableName, - s.clusterName, - s.clusterName, - s.tableName, - ) - } - - fmt.Println(sqlQuery) - if _, err := s.connection.Exec(sqlQuery); err != nil { - return err - } - if err := s.AddMigrationHistory(db.BaseMigration); err != nil { - q2 := fmt.Sprintf(`DROP TABLE %s`, s.tableName) - _, _ = s.connection.Exec(q2) - - return err - } - - log.Println(console.Green("Done")) - - return nil -} - -func (s *Migration) getTableScheme() (exists bool, err error) { - var ( - q = ` - SELECT database, table - FROM system.columns - WHERE table = ? AND database = currentDatabase() - ` - rows *sql.Rows - ) - - rows, err = s.connection.Query(q, s.tableName) - if err != nil { - return false, err - } - - for rows.Next() { - var ( - database string - table string - ) - if err := rows.Scan(&database, &table); err != nil { - return false, err - } - - //todo scan columns to tableScheme - if table == s.tableName { - return true, nil - } - } - - if err = rows.Err(); err != nil { - return false, err - } - - return false, nil -} diff --git a/migrator/db/entity.go b/migrator/db/entity.go deleted file mode 100644 index 9e8cafb..0000000 --- a/migrator/db/entity.go +++ /dev/null @@ -1,41 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package db - -import ( - "sort" - "time" -) - -type ( - MigrationEntity struct { - Version string - ApplyTime int - } - MigrationEntityList []MigrationEntity -) - -func (s MigrationEntityList) Len() int { - return len(s) -} - -func (s MigrationEntityList) Less(i, j int) bool { - return s[i].Version < s[j].Version -} - -func (s MigrationEntityList) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s MigrationEntityList) SortByVersion() { - sort.Sort(s) -} - -func (s MigrationEntity) ApplyTimeFormat() string { - return time.Unix(int64(s.ApplyTime), 0).Format("2006-01-02 15:04:05") -} diff --git a/migrator/db/migration.go b/migrator/db/migration.go deleted file mode 100644 index a9f68d9..0000000 --- a/migrator/db/migration.go +++ /dev/null @@ -1,223 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package db - -import ( - "database/sql" - "fmt" - "github.com/raoptimus/db-migrator.go/console" - "github.com/raoptimus/db-migrator.go/iofile" - "github.com/raoptimus/db-migrator.go/migrator/multistmt" - "log" - "path/filepath" - "regexp" - "time" -) - -const BaseMigration = "000000_000000_base" -const DefaultLimit = 10000 - -var Regex = regexp.MustCompile(`^(\d{6}_?\d{6}[A-Za-z0-9\_]+)\.((safe)\.)?(up|down)\.sql$`) - -type ( - MigrationInterface interface { - InitializeTableHistory() error - AddMigrationHistory(version string) error - RemoveMigrationHistory(version string) error - GetMigrationHistory(limit int) (MigrationEntityList, error) - ConvertError(err error, query string) error - } - MigrationOptions struct { - MaxSqlOutputLength int - Directory string - Compact bool - MultiSTMT bool - ForceSafely bool - } - Migration struct { - MigrationInterface - connection *sql.DB - transaction *sql.Tx - options MigrationOptions - } -) - -func NewMigration(m MigrationInterface, conn *sql.DB, options MigrationOptions) *Migration { - return &Migration{ - MigrationInterface: m, - connection: conn, - options: options, - } -} - -func (s *Migration) GetNewMigrations(limit int) (MigrationEntityList, error) { - entityList, err := s.GetMigrationHistory(limit) - if err != nil { - return nil, err - } - var files []string - files, err = filepath.Glob(filepath.Join(s.options.Directory, "*.up.sql")) - if err != nil { - return nil, err - } - - result := MigrationEntityList{} - var baseFilename string - for _, file := range files { - baseFilename = filepath.Base(file) - groups := Regex.FindStringSubmatch(baseFilename) - if len(groups) != 5 { - return nil, fmt.Errorf("file name %s is invalid", baseFilename) - } - found := false - for _, entity := range entityList { - if entity.Version == groups[1] { - found = true - break - } - } - if !found { - result = append( - result, - MigrationEntity{ - Version: groups[1], - }, - ) - } - } - - result.SortByVersion() - - return result, err -} - -func (s *Migration) MigrateUp(entity MigrationEntity, fileName string, safely bool) error { - if entity.Version == BaseMigration { - return nil - } - log.Printf(console.Yellow("*** applying %s"), entity.Version) - - elapsedTime, err := s.executeFile(fileName, s.options.ForceSafely || safely) - if err == nil { - err = s.AddMigrationHistory(entity.Version) - } - if err == nil { - log.Printf(console.Green("*** applied %s (time: %.3fs)"), entity.Version, elapsedTime) - return nil - } - - return fmt.Errorf("*** failed to apply %s (time: %.3fs)\nException: %v", - entity.Version, elapsedTime, err) -} - -func (s *Migration) MigrateDown(entity MigrationEntity, fileName string, safely bool) error { - if entity.Version == BaseMigration { - return nil - } - log.Printf(console.Yellow("*** reverting %s"), entity.Version) - - elapsedTime, err := s.executeFile(fileName, s.options.ForceSafely || safely) - if err == nil { - err = s.RemoveMigrationHistory(entity.Version) - } - if err == nil { - log.Printf(console.Green("*** reverted %s (time: %.3fs)"), entity.Version, elapsedTime) - return nil - } - - return fmt.Errorf("*** failed to reverted %s (time: %.3fs)\nException: %v", - entity.Version, elapsedTime, err) -} - -func (s *Migration) BeginCommand(sqlQuery string) time.Time { - sqlQueryOutput := s.GetSQLQueryOutput(sqlQuery) - if !s.options.Compact { - log.Printf(" > execute SQL: %s ...", sqlQueryOutput) - } - - return time.Now() -} - -func (s *Migration) ExecuteSafely(tx *sql.Tx, sqlQuery string, args ...interface{}) error { - start := s.BeginCommand(sqlQuery) - stmt, err := tx.Prepare(sqlQuery) - if err != nil { - return s.ConvertError(err, sqlQuery) - } - if _, err := stmt.Exec(args...); err != nil { - return s.ConvertError(err, sqlQuery) - } - s.EndCommand(start) - - return nil -} - -func (s *Migration) Execute(sqlQuery string, args ...interface{}) error { - start := s.BeginCommand(sqlQuery) - if _, err := s.connection.Exec(sqlQuery, args...); err != nil { - return s.ConvertError(err, sqlQuery) - } - s.EndCommand(start) - - return nil -} - -func (s *Migration) GetSQLQueryOutput(sqlQuery string) string { - sqlQueryOutput := sqlQuery - if s.options.MaxSqlOutputLength > 0 && s.options.MaxSqlOutputLength < len(sqlQuery) { - sqlQueryOutput = sqlQuery[:s.options.MaxSqlOutputLength] - } - - return sqlQueryOutput -} - -func (s *Migration) EndCommand(start time.Time) { - if s.options.Compact { - log.Printf(" done (time: '%.3fs)", time.Now().Sub(start).Seconds()) - } -} - -func (s *Migration) executeFile(fileName string, safely bool) (elapsedSeconds float64, err error) { - start := time.Now() - if !iofile.Exists(fileName) { - return 0, fmt.Errorf("migration file %s does not exists", fileName) - } - - if safely { - var tx *sql.Tx - tx, err = s.connection.Begin() - if err != nil { - return 0, err - } - - err = multistmt.ReadOrParseSQLFile(fileName, s.options.MultiSTMT, func(sqlQuery string) error { - return s.ExecuteSafely(tx, sqlQuery) - }) - - if err != nil { - _ = tx.Rollback() - return 0, err - } - - if err = tx.Commit(); err != nil { - return 0, err - } - - return time.Now().Sub(start).Seconds(), nil - } - - err = multistmt.ReadOrParseSQLFile(fileName, s.options.MultiSTMT, func(sqlQuery string) error { - return s.Execute(sqlQuery) - }) - - if err != nil { - return 0, err - } - - return time.Now().Sub(start).Seconds(), nil -} diff --git a/migrator/db/mysqlMigration/migration.go b/migrator/db/mysqlMigration/migration.go deleted file mode 100644 index eee0ad3..0000000 --- a/migrator/db/mysqlMigration/migration.go +++ /dev/null @@ -1,197 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package mysqlMigration - -import ( - "database/sql" - "fmt" - "github.com/go-sql-driver/mysql" - "github.com/raoptimus/db-migrator.go/console" - "github.com/raoptimus/db-migrator.go/migrator/db" - "log" - "time" -) - -type ( - Migration struct { - connection *sql.DB - tableSchema string - tableName string - directory string - } -) - -func New(connection *sql.DB, tableName, tableSchema, directory string) *Migration { - return &Migration{ - connection: connection, - tableSchema: tableSchema, - tableName: tableName, - directory: directory, - } -} - -func (s *Migration) internalConvertError(err error, query string) error { - if ex, ok := err.(*mysql.MySQLError); ok { - return fmt.Errorf("SQLSTATE[%d]: %s\nThe SQL being executed was: %s\n", - ex.Number, - ex.Message, - query, - ) - } - - return err -} - -func (s *Migration) ConvertError(err error, query string) error { - return fmt.Errorf("exception: %v", s.internalConvertError(err, query)) -} - -func (s *Migration) InitializeTableHistory() error { - exists, err := s.getTableScheme() - if err != nil { - return err - } - - if !exists { - if err := s.createMigrationHistoryTable(); err != nil { - return err - } - } - - return nil -} - -func (s *Migration) GetMigrationHistory(limit int) (db.MigrationEntityList, error) { - if err := s.InitializeTableHistory(); err != nil { - return nil, err - } - - var ( - q = fmt.Sprintf( - ` - SELECT version, apply_time - FROM %s - ORDER BY apply_time DESC, version DESC - LIMIT ?`, - s.tableName, - ) - result db.MigrationEntityList - ) - if limit < 1 { - limit = db.DefaultLimit - } - - rows, err := s.connection.Query(q, limit) - if err != nil { - return nil, s.internalConvertError(err, q) - } - for rows.Next() { - var ( - version string - applyTime int - ) - - if err := rows.Scan(&version, &applyTime); err != nil { - return nil, err - } - - if version == db.BaseMigration { - continue - } - result = append(result, - db.MigrationEntity{ - Version: version, - ApplyTime: applyTime, - }, - ) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return result, nil -} - -func (s *Migration) AddMigrationHistory(version string) error { - now := uint32(time.Now().Unix()) - q := fmt.Sprintf(` - INSERT INTO %s (version, apply_time) - VALUES (?, ?)`, - s.tableName, - ) - _, err := s.connection.Exec(q, version, now) - - return s.internalConvertError(err, q) -} - -func (s *Migration) RemoveMigrationHistory(version string) error { - q := fmt.Sprintf(`DELETE FROM %s WHERE version = ?`, s.tableName) - _, err := s.connection.Exec(q, version) - - return err -} - -func (s *Migration) createMigrationHistoryTable() error { - log.Printf(console.Yellow("Creating migration history table %s..."), s.tableName) - - q := fmt.Sprintf( - ` - CREATE TABLE %s ( - version VARCHAR(180) PRIMARY KEY, - apply_time INT - ) - ENGINE=InnoDB - `, - s.tableName, - ) - - if _, err := s.connection.Exec(q); err != nil { - return s.internalConvertError(err, q) - } - if err := s.AddMigrationHistory(db.BaseMigration); err != nil { - q2 := fmt.Sprintf(`DROP TABLE %s`, s.tableName) - _, _ = s.connection.Exec(q2) - - return err - } - - log.Println(console.Green("Done")) - - return nil -} - -func (s *Migration) getTableScheme() (exists bool, err error) { - var ( - q = ` - SELECT EXISTS( - SELECT * - FROM information_schema.tables - WHERE table_schema = ? AND table_name = ? - ) - ` - rows *sql.Rows - ) - - rows, err = s.connection.Query(q, s.tableSchema, s.tableName) - if err != nil { - return false, s.internalConvertError(err, q) - } - - for rows.Next() { - if err := rows.Scan(&exists); err != nil { - return false, s.internalConvertError(err, q) - } - } - - if err = rows.Err(); err != nil { - return false, err - } - - return exists, nil -} diff --git a/migrator/db/postgresMigration/migration.go b/migrator/db/postgresMigration/migration.go deleted file mode 100644 index 258b74f..0000000 --- a/migrator/db/postgresMigration/migration.go +++ /dev/null @@ -1,219 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package postgresMigration - -import ( - "database/sql" - "fmt" - "github.com/lib/pq" - _ "github.com/lib/pq" - "github.com/raoptimus/db-migrator.go/console" - "github.com/raoptimus/db-migrator.go/migrator/db" - "log" - "time" -) - -const DefaultSchema = "public" - -type ( - Migration struct { - connection *sql.DB - tableName string - tableSchema string - directory string - } -) - -func New(connection *sql.DB, tableName, tableSchema, directory string) *Migration { - return &Migration{ - connection: connection, - tableName: tableName, - tableSchema: tableSchema, - directory: directory, - } -} - -func (s *Migration) internalConvertError(err error, query string) error { - if ex, ok := err.(*pq.Error); ok { - q := ex.InternalQuery - if q == "" { - q = query - } - return fmt.Errorf("SQLSTATE[%s]: %s: %s\nDETAILS:%s\nThe SQL being executed was: %s\n", - ex.Code, - ex.Severity, - ex.Message, - ex.Detail, - q, - ) - } - - return err -} - -func (s *Migration) ConvertError(err error, query string) error { - return fmt.Errorf("exception: %v", s.internalConvertError(err, query)) -} - -func (s *Migration) InitializeTableHistory() error { - exists, err := s.getTableScheme() - if err != nil { - return err - } - - if !exists { - if err := s.createMigrationHistoryTable(); err != nil { - return err - } - } - - return nil -} - -func (s *Migration) GetMigrationHistory(limit int) (db.MigrationEntityList, error) { - if err := s.InitializeTableHistory(); err != nil { - return nil, err - } - - var ( - q = fmt.Sprintf( - ` - SELECT version, apply_time - FROM %s - ORDER BY apply_time DESC, version DESC - LIMIT $1`, - s.getTableNameWithSchema(), - ) - result db.MigrationEntityList - ) - if limit < 1 { - limit = db.DefaultLimit - } - - rows, err := s.connection.Query(q, limit) - if err != nil { - return nil, s.internalConvertError(err, q) - } - for rows.Next() { - var ( - version string - applyTime int - ) - - if err := rows.Scan(&version, &applyTime); err != nil { - return nil, err - } - - if version == db.BaseMigration { - continue - } - result = append(result, - db.MigrationEntity{ - Version: version, - ApplyTime: applyTime, - }, - ) - } - - if err = rows.Err(); err != nil { - return nil, err - } - - return result, nil -} - -func (s *Migration) AddMigrationHistory(version string) error { - now := uint32(time.Now().Unix()) - q := fmt.Sprintf(` - INSERT INTO %s (version, apply_time) - VALUES ($1, $2)`, - s.getTableNameWithSchema(), - ) - _, err := s.connection.Exec(q, version, now) - - return s.internalConvertError(err, q) -} - -func (s *Migration) RemoveMigrationHistory(version string) error { - q := fmt.Sprintf(`DELETE FROM %s WHERE (version) = ($1)`, s.getTableNameWithSchema()) - _, err := s.connection.Exec(q, version) - - return err -} - -func (s *Migration) createMigrationHistoryTable() error { - log.Printf(console.Yellow("Creating migration history table %s..."), s.getTableNameWithSchema()) - - q := fmt.Sprintf( - ` - CREATE TABLE %s ( - version varchar(180) PRIMARY KEY, - apply_time integer - ) - `, - s.getTableNameWithSchema(), - ) - - if _, err := s.connection.Exec(q); err != nil { - return s.internalConvertError(err, q) - } - if err := s.AddMigrationHistory(db.BaseMigration); err != nil { - q2 := fmt.Sprintf(`DROP TABLE %s`, s.getTableNameWithSchema()) - _, _ = s.connection.Exec(q2) - - return err - } - - log.Println(console.Green("Done")) - - return nil -} - -func (s *Migration) getTableScheme() (exists bool, err error) { - var ( - q = ` - SELECT - d.nspname AS table_schema, - c.relname AS table_name - FROM pg_class c - LEFT JOIN pg_namespace d ON d.oid = c.relnamespace - WHERE (c.relname, d.nspname) = ($1, $2) - ` - rows *sql.Rows - ) - - rows, err = s.connection.Query(q, s.tableName, s.tableSchema) - if err != nil { - return false, s.internalConvertError(err, q) - } - - for rows.Next() { - var ( - tableName string - schema string - ) - if err := rows.Scan(&schema, &tableName); err != nil { - return false, s.internalConvertError(err, q) - } - - //todo scan columns to tableScheme - if tableName == s.tableName { - return true, nil - } - } - - if err = rows.Err(); err != nil { - return false, err - } - - return false, nil -} - -func (s *Migration) getTableNameWithSchema() string { - return s.tableSchema + "." + s.tableName -} diff --git a/migrator/filename_builder.go b/migrator/filename_builder.go deleted file mode 100644 index 166f390..0000000 --- a/migrator/filename_builder.go +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "github.com/raoptimus/db-migrator.go/iofile" - "path/filepath" -) - -const ( - safelyUpSuffix = ".safe.up.sql" - safelyDownSuffix = ".safe.down.sql" - unsafelyUpSuffix = ".up.sql" - unsafelyDownSuffix = ".down.sql" -) - -type FileNameBuilder struct { - migrationsDirectory string -} - -func NewFileNameBuilder(migrationsDirectory string) FileNameBuilder { - return FileNameBuilder{migrationsDirectory: migrationsDirectory} -} - -func (s FileNameBuilder) BuildUpFileName(version string, forceSafely bool) (fname string, safely bool) { - return s.buildFileName(version, safelyUpSuffix, unsafelyUpSuffix, forceSafely) -} - -func (s FileNameBuilder) BuildDownFileName(version string, forceSafely bool) (fname string, safely bool) { - return s.buildFileName(version, safelyDownSuffix, unsafelyDownSuffix, forceSafely) -} - -func (s FileNameBuilder) buildFileName(version string, safelySuffix, unsafelySuffix string, forceSafely bool) (fname string, safely bool) { - safelyFile := filepath.Join(s.migrationsDirectory, version+safelySuffix) - unsafelyFile := filepath.Join(s.migrationsDirectory, version+unsafelySuffix) - - switch { - case iofile.Exists(safelyFile): - return safelyFile, true - case iofile.Exists(unsafelyFile): - return unsafelyFile, false - case forceSafely: - return safelyFile, true - default: - return unsafelyFile, false - } -} diff --git a/migrator/filename_builder_test.go b/migrator/filename_builder_test.go deleted file mode 100644 index 7fdfe0a..0000000 --- a/migrator/filename_builder_test.go +++ /dev/null @@ -1,37 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestFileNameBuilder_BuildUpFileName(t *testing.T) { - fb := NewFileNameBuilder("/someDir") - - fileName, safely := fb.BuildUpFileName("210328_221600_test", true) - assert.True(t, safely) - assert.Equal(t, "/someDir/210328_221600_test.safe.up.sql", fileName) - - fileName, safely = fb.BuildUpFileName("210328_221600_test", false) - assert.False(t, safely) - assert.Equal(t, "/someDir/210328_221600_test.up.sql", fileName) -} - -func TestFileNameBuilder_BuildDownFileName(t *testing.T) { - fb := NewFileNameBuilder("/someDir") - - fileName, safely := fb.BuildDownFileName("210328_221600_test", true) - assert.True(t, safely) - assert.Equal(t, "/someDir/210328_221600_test.safe.down.sql", fileName) - - fileName, safely = fb.BuildDownFileName("210328_221600_test", false) - assert.False(t, safely) - assert.Equal(t, "/someDir/210328_221600_test.down.sql", fileName) -} diff --git a/migrator/helpers.go b/migrator/helpers.go deleted file mode 100644 index 36bfde6..0000000 --- a/migrator/helpers.go +++ /dev/null @@ -1,48 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "errors" - "fmt" - "github.com/raoptimus/db-migrator.go/migrator/db" - "strconv" -) - -func parseLimit(limit string, defaults int) (int, error) { - switch limit { - case "": - return defaults, nil - case "all": - return 0, nil - default: - i, err := strconv.Atoi(limit) - if err != nil { - return -1, fmt.Errorf("The step argument %v is not valid", limit) - } - - if i < 1 { - return -1, errors.New("The step argument must be greater than 0.") - } - - return i, nil - } -} - -func printAllMigrations(hist db.MigrationEntityList, withTime bool) { - for _, item := range hist { - //todo check len of version name - if withTime { - fmt.Printf("\t(%s) %s\n", item.ApplyTimeFormat(), item.Version) - } else { - fmt.Printf("\t%s\n", item.Version) - } - - } - fmt.Println("") -} diff --git a/migrator/migrator.go b/migrator/migrator.go deleted file mode 100644 index bc88643..0000000 --- a/migrator/migrator.go +++ /dev/null @@ -1,165 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "database/sql" - "fmt" - "github.com/ClickHouse/clickhouse-go" - "github.com/go-sql-driver/mysql" - "github.com/raoptimus/db-migrator.go/migrator/db" - "github.com/raoptimus/db-migrator.go/migrator/db/clickhouseMigration" - "github.com/raoptimus/db-migrator.go/migrator/db/mysqlMigration" - "github.com/raoptimus/db-migrator.go/migrator/db/postgresMigration" - "strings" -) - -type ( - Service struct { - options Options - db *sql.DB - migration *db.Migration - fileBuilder FileNameBuilder - } - Options struct { - DSN string - Directory string - TableName string - ClusterName string - Compact bool - Interactive bool - MaxSqlOutputLength int - } -) - -func New(options Options) (*Service, error) { - serv := Service{ - options: options, - fileBuilder: NewFileNameBuilder(options.Directory), - } - if err := serv.init(); err != nil { - return nil, err - } - return &serv, nil -} - -func (s *Service) init() error { - switch { - case strings.HasPrefix(s.options.DSN, "clickhouse://"): - return s.initClickHouse() - case strings.HasPrefix(s.options.DSN, "postgres://"): - return s.initPostgres() - case strings.HasPrefix(s.options.DSN, "mysql://"): - return s.initMysql() - default: - return fmt.Errorf("Driver %s doesn't support", s.options.DSN) - } -} - -func (s *Service) initMysql() error { - connection, err := sql.Open("mysql", s.options.DSN[8:]) - if err != nil { - return err - } - if err := connection.Ping(); err != nil { - return err - } - - var cfg *mysql.Config - cfg, err = mysql.ParseDSN(s.options.DSN) - if err != nil { - return err - } - - s.db = connection - s.migration = db.NewMigration( - mysqlMigration.New(connection, s.options.TableName, cfg.DBName, s.options.Directory), - connection, - db.MigrationOptions{ - MaxSqlOutputLength: s.options.MaxSqlOutputLength, - Directory: s.options.Directory, - Compact: s.options.Compact, - MultiSTMT: false, - ForceSafely: false, - }, - ) - - return nil -} - -func (s *Service) initPostgres() error { - connection, err := sql.Open("postgres", s.options.DSN) - if err != nil { - return err - } - if err := connection.Ping(); err != nil { - return err - } - - var tableName, tableSchema string - if strings.Contains(s.options.TableName, ".") { - parts := strings.Split(s.options.TableName, ".") - tableSchema = parts[0] - tableName = parts[1] - } else { - tableSchema = postgresMigration.DefaultSchema - tableName = s.options.TableName - } - - s.db = connection - s.migration = db.NewMigration( - postgresMigration.New(connection, tableName, tableSchema, s.options.Directory), - connection, - db.MigrationOptions{ - MaxSqlOutputLength: s.options.MaxSqlOutputLength, - Directory: s.options.Directory, - Compact: s.options.Compact, - MultiSTMT: false, - ForceSafely: false, - }, - ) - - return nil -} - -func (s *Service) initClickHouse() error { - dsn, err := clickhouseMigration.NormalizeDSN(s.options.DSN) - if err != nil { - return err - } - connection, err := sql.Open("clickhouse", dsn) - if err != nil { - return err - } - - if err := connection.Ping(); err != nil { - if exception, ok := err.(*clickhouse.Exception); ok { - return fmt.Errorf("[%d] %s \n%s\n", exception.Code, exception.Message, exception.StackTrace) - } - return err - } - s.db = connection - s.migration = db.NewMigration( - clickhouseMigration.New( - connection, - s.options.TableName, - s.options.ClusterName, - s.options.Directory, - ), - connection, - db.MigrationOptions{ - MaxSqlOutputLength: s.options.MaxSqlOutputLength, - Directory: s.options.Directory, - Compact: s.options.Compact, - MultiSTMT: false, - ForceSafely: true, - }, - ) - - return nil -} diff --git a/migrator/migrator_create.go b/migrator/migrator_create.go deleted file mode 100644 index 666bfb5..0000000 --- a/migrator/migrator_create.go +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "errors" - "fmt" - "github.com/raoptimus/db-migrator.go/console" - "github.com/raoptimus/db-migrator.go/iofile" - "log" - "regexp" - "time" -) - -var RegexName = regexp.MustCompile(`^[\w\\\\]+$`) - -func (s *Service) CreateMigration(name string) error { - if !RegexName.MatchString(name) { - return errors.New("The migration name should contain letters, digits, underscore and/or backslash characters only.") - } - - prefix := time.Now().Format("060102_150405") - version := prefix + "_" + name - fileNameUp, _ := s.fileBuilder.BuildUpFileName(version, true) - fileNameDown, _ := s.fileBuilder.BuildDownFileName(version, true) - question := fmt.Sprintf("Create new migration files: \n'%s' and \n'%s'?\n", fileNameUp, fileNameDown) - - if !console.Confirm(question) { - return nil - } - - if err := iofile.CreateDirectory(s.options.Directory); err != nil { - return err - } - - if err := iofile.CreateFile(fileNameUp); err != nil { - return err - } - - if err := iofile.CreateFile(fileNameDown); err != nil { - return err - } - - log.Println(console.Green("New migration created successfully.")) - - return nil -} diff --git a/migrator/migrator_down.go b/migrator/migrator_down.go deleted file mode 100644 index db4f1d0..0000000 --- a/migrator/migrator_down.go +++ /dev/null @@ -1,62 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "fmt" - "github.com/raoptimus/db-migrator.go/console" - "log" -) - -func (s *Service) Down(limit string) error { - limitInt, err := parseLimit(limit, 1) - if err != nil { - return err - } - entityList, err := s.migration.GetMigrationHistory(limitInt) - if err != nil { - return err - } - n := entityList.Len() - if n == 0 { - log.Println(console.Green("No migration has been done before.")) - return nil - } - - fmt.Printf(console.Yellow("Total %d %s to be reverted: \n"), - n, console.NumberPlural(n, "migration", "migrations")) - - printAllMigrations(entityList, false) - - reverted := 0 - question := fmt.Sprintf("Revert the above %d %s?", - n, console.NumberPlural(n, "migration", "migrations"), - ) - if s.options.Interactive && !console.Confirm(question) { - return nil - } - - for _, entity := range entityList { - fileName, safely := s.fileBuilder.BuildDownFileName(entity.Version, true) - if err := s.migration.MigrateDown(entity, fileName, safely); err != nil { - return fmt.Errorf( - "%v\n%d from %d %s reverted.\nMigration failed. "+ - "Migration failed. The rest of the migrations are canceled.", - err, reverted, n, console.NumberPlural(reverted, "migration was", "migrations were"), - ) - } - - reverted++ - } - - log.Printf(console.Green("%d %s reverted"), - n, console.NumberPlural(n, "migration was", "migrations were")) - fmt.Println(console.Green("Migrated down successfully")) - - return nil -} diff --git a/migrator/migrator_history.go b/migrator/migrator_history.go deleted file mode 100644 index de189d8..0000000 --- a/migrator/migrator_history.go +++ /dev/null @@ -1,41 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "fmt" - "github.com/raoptimus/db-migrator.go/console" -) - -func (s *Service) History(limit string) error { - limitInt, err := parseLimit(limit, 10) - if err != nil { - return err - } - hist, err := s.migration.GetMigrationHistory(limitInt) - if err != nil { - return err - } - n := hist.Len() - if n == 0 { - fmt.Println(console.Green("No migration has been done before.")) - return nil - } - - if limitInt > 0 { - fmt.Printf(console.Yellow("Showing the last %d %s: \n"), - n, console.NumberPlural(n, "migration", "migrations")) - } else { - fmt.Printf(console.Yellow("Total %d %s been applied before: \n"), - n, console.NumberPlural(n, "migration has", "migrations have")) - } - - printAllMigrations(hist, true) - - return nil -} diff --git a/migrator/migrator_history_new.go b/migrator/migrator_history_new.go deleted file mode 100644 index c144a80..0000000 --- a/migrator/migrator_history_new.go +++ /dev/null @@ -1,42 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "fmt" - "github.com/raoptimus/db-migrator.go/console" -) - -func (s *Service) HistoryNew(limit string) error { - limitInt, err := parseLimit(limit, 10) - if err != nil { - return err - } - hist, err := s.migration.GetNewMigrations(limitInt) - if err != nil { - return err - } - n := hist.Len() - if n == 0 { - fmt.Println(console.Green("No new migrations found. Your system is up-to-date.")) - return nil - } - - if limitInt > 0 && n > limitInt { - hist = hist[0:limitInt] - fmt.Printf(console.Yellow("Showing %d out of %d new %s: \n"), - limitInt, n, console.NumberPlural(n, "migration", "migrations")) - } else { - fmt.Printf(console.Yellow("Found %d new %s: \n"), - n, console.NumberPlural(n, "migration", "migrations")) - } - - printAllMigrations(hist, false) - - return nil -} diff --git a/migrator/migrator_redo.go b/migrator/migrator_redo.go deleted file mode 100644 index 5091d41..0000000 --- a/migrator/migrator_redo.go +++ /dev/null @@ -1,70 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "fmt" - "github.com/raoptimus/db-migrator.go/console" - "github.com/raoptimus/db-migrator.go/migrator/db" - "log" -) - -func (s *Service) Redo(limit string) error { - limitInt, err := parseLimit(limit, 1) - if err != nil { - return err - } - entityList, err := s.migration.GetMigrationHistory(limitInt) - if err != nil { - return err - } - n := entityList.Len() - if n == 0 { - log.Println(console.Green("No migration has been done before.")) - return nil - } - - fmt.Printf(console.Yellow("Total %d %s to be redone: \n"), - n, console.NumberPlural(n, "migration", "migrations")) - - printAllMigrations(entityList, false) - - question := fmt.Sprintf("Redo the above %d %s?", - n, console.NumberPlural(n, "migration", "migrations"), - ) - if s.options.Interactive && !console.Confirm(question) { - return nil - } - - var reverseHist db.MigrationEntityList - for _, entity := range entityList { - fileName, safely := s.fileBuilder.BuildDownFileName(entity.Version, true) - if err := s.migration.MigrateDown(entity, fileName, safely); err != nil { - return fmt.Errorf( - "%v\nMigration failed. The rest of the migrations are canceled.", err, - ) - } - - reverseHist = append(reverseHist, entity) - } - - for _, entity := range reverseHist { - fileName, safely := s.fileBuilder.BuildUpFileName(entity.Version, true) - if err := s.migration.MigrateUp(entity, fileName, safely); err != nil { - return fmt.Errorf( - "%v\nMigration failed. The rest of the migrations are canceled.", err, - ) - } - } - - log.Printf(console.Green("%d %s redone."), - n, console.NumberPlural(n, "migration was", "migrations were")) - fmt.Println(console.Green("Migration redone successfully.")) - - return nil -} diff --git a/migrator/migrator_up.go b/migrator/migrator_up.go deleted file mode 100644 index c5401d4..0000000 --- a/migrator/migrator_up.go +++ /dev/null @@ -1,69 +0,0 @@ -/** - * This file is part of the raoptimus/db-migrator.go library - * - * @copyright Copyright (c) Evgeniy Urvantsev - * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md - * @link https://github.com/raoptimus/db-migrator.go - */ -package migrator - -import ( - "fmt" - "github.com/raoptimus/db-migrator.go/console" - "log" -) - -func (s *Service) Up(limit string) error { - limitInt, err := parseLimit(limit, 0) - if err != nil { - return err - } - entityList, err := s.migration.GetNewMigrations(limitInt) - if err != nil { - return err - } - total := entityList.Len() - if total == 0 { - fmt.Println(console.Green("No new migrations found. Your system is up-to-date.")) - return nil - } - if limitInt > 0 && len(entityList) > limitInt { - entityList = entityList[:limitInt] - } - n := entityList.Len() - if n == total { - fmt.Printf(console.Yellow("Total %d new %s to be applied: \n"), - n, console.NumberPlural(n, "migration", "migrations")) - } else { - fmt.Printf(console.Yellow("Total %d out of %d new %s to be applied: \n"), - n, total, console.NumberPlural(total, "migration", "migrations")) - } - - printAllMigrations(entityList, false) - - applied := 0 - question := fmt.Sprintf("Apply the above %s?", - console.NumberPlural(n, "migration", "migrations"), - ) - if s.options.Interactive && !console.Confirm(question) { - return nil - } - - for _, entity := range entityList { - fileName, safely := s.fileBuilder.BuildUpFileName(entity.Version, true) - if err := s.migration.MigrateUp(entity, fileName, safely); err != nil { - return fmt.Errorf( - "%v\n%d from %d %s applied.\nMigration failed. The rest of the migrations are canceled.", - err, applied, n, console.NumberPlural(applied, "migration was", "migrations were"), - ) - } - - applied++ - } - - log.Printf(console.Green("%d %s applied"), - n, console.NumberPlural(n, "migration was", "migrations were")) - fmt.Println(console.Green("Migrated up successfully")) - - return nil -} diff --git a/pkg/console/console.go b/pkg/console/console.go new file mode 100644 index 0000000..f52b77d --- /dev/null +++ b/pkg/console/console.go @@ -0,0 +1,117 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package console + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +var ( + Black = color("\033[1;30m%s\033[0m") + Red = color("\033[1;31m%s\033[0m") + Green = color("\033[1;32m%s\033[0m") + Yellow = color("\033[1;33m%s\033[0m") + Purple = color("\033[1;34m%s\033[0m") + Magenta = color("\033[1;35m%s\033[0m") + Teal = color("\033[1;36m%s\033[0m") + White = color("\033[1;37m%s\033[0m") +) + +type Console struct{} + +var Std = New() + +func New() *Console { + return &Console{} +} + +func (c *Console) Confirm(s string) bool { + reader := bufio.NewReader(os.Stdin) + + for { + c.Infof("%s [y/n]: ", s) + + response, err := reader.ReadString('\n') + if err != nil { + c.Fatal(err) + } + + response = strings.ToLower(strings.TrimSpace(response)) + + if response == "y" || response == "yes" { + return true + } else if response == "n" || response == "no" { + return false + } + } +} + +func (c *Console) Info(message string) { + fmt.Print(Black(message)) +} +func (c *Console) InfoLn(message string) { + fmt.Println(Black(message)) +} +func (c *Console) Infof(message string, a ...any) { + fmt.Printf(Black(message), a...) +} + +func (c *Console) Success(message string) { + fmt.Print(Green(message)) +} +func (c *Console) SuccessLn(message string) { + fmt.Println(Green(message)) +} +func (c *Console) Successf(message string, a ...any) { + fmt.Printf(Green(message), a...) +} + +func (c *Console) Warn(message string) { + fmt.Print(Yellow(message)) +} +func (c *Console) WarnLn(message string) { + fmt.Println(Yellow(message)) +} +func (c *Console) Warnf(message string, a ...any) { + fmt.Printf(Yellow(message), a...) +} + +func (c *Console) Error(message string) { + fmt.Print(Red(message)) +} +func (c *Console) ErrorLn(message string) { + fmt.Println(Red(message)) +} +func (c *Console) Errorf(message string, a ...any) { + fmt.Printf(Red(message), a...) +} + +func (c *Console) Fatal(err error) { + c.Errorf("Exception: %v", err) + os.Exit(1) +} + +func (c *Console) NumberPlural(count int, one, many string) string { + if count > 1 { + return many + } + + return one +} + +func color(colorString string) func(...interface{}) string { + sprint := func(args ...interface{}) string { + return fmt.Sprintf(colorString, fmt.Sprint(args...)) + } + + return sprint +} diff --git a/pkg/console/dummy.go b/pkg/console/dummy.go new file mode 100644 index 0000000..63a2ea2 --- /dev/null +++ b/pkg/console/dummy.go @@ -0,0 +1,65 @@ +package console + +type Dummy struct { + confirm bool +} + +func NewDummy(confirm bool) *Dummy { + return &Dummy{confirm: confirm} +} + +func (c *Dummy) Confirm(s string) bool { + return c.confirm +} + +func (c *Dummy) Info(message string) { + return +} +func (c *Dummy) InfoLn(message string) { + return +} +func (c *Dummy) Infof(message string, a ...any) { + return +} + +func (c *Dummy) Success(message string) { + return +} +func (c *Dummy) SuccessLn(message string) { + return +} +func (c *Dummy) Successf(message string, a ...any) { + return +} + +func (c *Dummy) Warn(message string) { + return +} +func (c *Dummy) WarnLn(message string) { + return +} +func (c *Dummy) Warnf(message string, a ...any) { + return +} + +func (c *Dummy) Error(message string) { + return +} +func (c *Dummy) ErrorLn(message string) { + return +} +func (c *Dummy) Errorf(message string, a ...any) { + return +} + +func (c *Dummy) Fatal(err error) { + return +} + +func (c *Dummy) NumberPlural(count int, one, many string) string { + if count > 1 { + return many + } + + return one +} diff --git a/pkg/console/go.mod b/pkg/console/go.mod new file mode 100644 index 0000000..ea856d6 --- /dev/null +++ b/pkg/console/go.mod @@ -0,0 +1,3 @@ +module github.com/raoptimus/db-migrator.go/pkg/console + +go 1.20 diff --git a/pkg/iohelp/file.go b/pkg/iohelp/file.go new file mode 100644 index 0000000..5854b9f --- /dev/null +++ b/pkg/iohelp/file.go @@ -0,0 +1,64 @@ +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package iohelp + +import ( + "io" + "os" + + "github.com/pkg/errors" +) + +const fileModeExecutable = 0o755 + +type File struct{} + +var StdFile = NewFile() + +func NewFile() *File { + return &File{} +} + +func (f *File) Exists(fileName string) (bool, error) { + if _, err := os.Stat(fileName); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + + return true, nil +} + +func (f *File) Open(filename string) (io.ReadCloser, error) { + return os.Open(filename) +} + +func (f *File) ReadAll(filename string) ([]byte, error) { + ff, err := f.Open(filename) + if err != nil { + return nil, err + } + defer ff.Close() + + return io.ReadAll(ff) +} + +func (f *File) Create(filename string) error { + ff, err := os.Create(filename) + if err == nil { + err = ff.Close() + } + + if err != nil { + return errors.Wrapf(err, "creating file %s", filename) + } + + return nil +} diff --git a/pkg/iohelp/go.mod b/pkg/iohelp/go.mod new file mode 100644 index 0000000..7c36f1a --- /dev/null +++ b/pkg/iohelp/go.mod @@ -0,0 +1,5 @@ +module github.com/raoptimus/db-migrator.go/pkg/iohelp + +go 1.20 + +require github.com/pkg/errors v0.9.1 diff --git a/pkg/iohelp/go.sum b/pkg/iohelp/go.sum new file mode 100644 index 0000000..7c401c3 --- /dev/null +++ b/pkg/iohelp/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/sqlio/go.mod b/pkg/sqlio/go.mod new file mode 100644 index 0000000..2d62a45 --- /dev/null +++ b/pkg/sqlio/go.mod @@ -0,0 +1,11 @@ +module github.com/raoptimus/db-migrator.go/pkg/sqlio + +go 1.20 + +require github.com/stretchr/testify v1.8.3 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pkg/sqlio/go.sum b/pkg/sqlio/go.sum new file mode 100644 index 0000000..57c201b --- /dev/null +++ b/pkg/sqlio/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/migrator/multistmt/parse.go b/pkg/sqlio/scanner.go similarity index 51% rename from migrator/multistmt/parse.go rename to pkg/sqlio/scanner.go index 7c4ec33..82e7e53 100644 --- a/migrator/multistmt/parse.go +++ b/pkg/sqlio/scanner.go @@ -1,30 +1,73 @@ -package multistmt +/** + * This file is part of the raoptimus/db-migrator.go library + * + * @copyright Copyright (c) Evgeniy Urvantsev + * @license https://github.com/raoptimus/db-migrator.go/blob/master/LICENSE.md + * @link https://github.com/raoptimus/db-migrator.go + */ + +package sqlio import ( "bufio" "bytes" "fmt" "io" - "io/ioutil" - "os" "strings" ) -const maxMigrationSize = 10 * 1 << 20 +const ( + maxMigrationSize = 10 * 1 << 20 +) var ( multiStmtDelimiter = []byte(";") psqlPLFuncDelimiter = []byte("$$") - skip = 0 ) -// StartBufSize is the default starting size of the buffer used to scan and parse multi-statement migrations +// StartBufSize is the default starting size of the buffer used to scan and parse multi-statement migrations. var StartBufSize = 4096 -// Handler handles a single migration parsed from a multi-statement migration. -// It's given the single migration to handle and returns whether or not further statements -// from the multi-statement migration should be parsed and handled. -type Handler func(sqlQuery string) error +type Scanner struct { + scanner *bufio.Scanner + sql string + err error + done bool +} + +func NewScanner(r io.Reader) *Scanner { + s := bufio.NewScanner(r) + s.Buffer(make([]byte, 0, StartBufSize), maxMigrationSize) + s.Split(splitWithDelimiter()) + + return &Scanner{ + scanner: s, + } +} + +func (s *Scanner) SQL() string { + return s.sql +} + +func (s *Scanner) Err() error { + return s.err +} + +func (s *Scanner) Scan() bool { + if s.done { + return false + } + for s.scanner.Scan() { + s.sql = strings.Trim(s.scanner.Text(), " \n;") + if s.sql == "" { + continue + } + return true + } + + s.err = s.scanner.Err() + return false +} func splitWithDelimiter() func(d []byte, atEOF bool) (int, []byte, error) { return func(d []byte, atEOF bool) (int, []byte, error) { @@ -77,57 +120,3 @@ func splitWithDelimiter() func(d []byte, atEOF bool) (int, []byte, error) { } } } - -// Parse parses the given multi-statement migration -func Parse(reader io.Reader, callback Handler) error { - scanner := bufio.NewScanner(reader) - scanner.Buffer(make([]byte, 0, StartBufSize), maxMigrationSize) - scanner.Split(splitWithDelimiter()) - - var ( - sqlQuery string - ) - for scanner.Scan() { - sqlQuery = string(scanner.Bytes()) - sqlQuery = strings.Trim(sqlQuery, " \n") - if sqlQuery == "" { - continue - } - - if err := callback(sqlQuery); err != nil { - return err - } - } - - return scanner.Err() -} - -func ParseSQLFile(filename string, callback Handler) error { - f, err := os.Open(filename) - if err != nil { - return err - } - - return Parse(f, callback) -} - -func ReadSQLFile(filename string, callback Handler) error { - var ( - sqlBytes []byte - err error - ) - sqlBytes, err = ioutil.ReadFile(filename) - if err != nil { - return err - } - - return callback(string(sqlBytes)) -} - -func ReadOrParseSQLFile(filename string, multiSTMT bool, callback Handler) error { - if multiSTMT { - return ReadSQLFile(filename, callback) - } - - return ParseSQLFile(filename, callback) -} diff --git a/migrator/multistmt/parse_test.go b/pkg/sqlio/scanner_test.go similarity index 66% rename from migrator/multistmt/parse_test.go rename to pkg/sqlio/scanner_test.go index 3fca56a..ed87a4e 100644 --- a/migrator/multistmt/parse_test.go +++ b/pkg/sqlio/scanner_test.go @@ -1,9 +1,10 @@ -package multistmt +package sqlio import ( - "github.com/stretchr/testify/assert" "strings" "testing" + + "github.com/stretchr/testify/assert" ) func TestParse(t *testing.T) { @@ -22,20 +23,20 @@ func TestParse(t *testing.T) { }, { name: "single statement, one delimiter", - multiStmt: "single statement, one delimiter;", - expected: []string{"single statement, one delimiter;"}, + multiStmt: "single statement, one delimiter", + expected: []string{"single statement, one delimiter"}, expectedErr: nil, }, { name: "two statements, no trailing delimiter", multiStmt: "statement one; statement two", - expected: []string{"statement one;", "statement two"}, + expected: []string{"statement one", "statement two"}, expectedErr: nil, }, { name: "two statements, with trailing delimiter", multiStmt: "statement one; statement two;", - expected: []string{"statement one;", "statement two;"}, + expected: []string{"statement one", "statement two"}, expectedErr: nil, }, } @@ -43,11 +44,12 @@ func TestParse(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { stmts := make([]string, 0, len(tc.expected)) - err := Parse(strings.NewReader(tc.multiStmt), func(sqlQuery string) error { - stmts = append(stmts, sqlQuery) + scanner := NewScanner(strings.NewReader(tc.multiStmt)) - return nil - }) + for scanner.Scan() { + stmts = append(stmts, scanner.SQL()) + } + err := scanner.Err() assert.Equal(t, tc.expectedErr, err) assert.Equal(t, tc.expected, stmts) }) @@ -56,21 +58,19 @@ func TestParse(t *testing.T) { func TestParseDiscontinue(t *testing.T) { multiStmt := "statement one; statement two" - expected := []string{"statement one;", "statement two"} - + expected := []string{"statement one", "statement two"} stmts := make([]string, 0, len(expected)) - err := Parse(strings.NewReader(multiStmt), func(sqlQuery string) error { - stmts = append(stmts, sqlQuery) - - return nil - }) - + scanner := NewScanner(strings.NewReader(multiStmt)) + for scanner.Scan() { + stmts = append(stmts, scanner.SQL()) + } + err := scanner.Err() assert.Nil(t, err) assert.Equal(t, expected, stmts) } func TestParsePostgresFunctions(t *testing.T) { - expected := []string{`CREATE test;`, + expected := []string{`CREATE test`, `CREATE OR REPLACE FUNCTION test_index_update() RETURNS trigger AS $$ BEGIN something; @@ -80,18 +80,14 @@ BEGIN RETURN NEW; END; -$$ LANGUAGE plpgsql -;`, `CREATE TRIGGER test_index_update_trigger -;`} - multiStmt := strings.Join(expected, "") - +$$ LANGUAGE plpgsql`, `CREATE TRIGGER test_index_update_trigger`} + multiStmt := strings.Join(expected, "; ") stmts := make([]string, 0, len(expected)) - err := Parse(strings.NewReader(multiStmt), func(sqlQuery string) error { - stmts = append(stmts, sqlQuery) - - return nil - }) - + scanner := NewScanner(strings.NewReader(multiStmt)) + for scanner.Scan() { + stmts = append(stmts, scanner.SQL()) + } + err := scanner.Err() assert.Nil(t, err) assert.Equal(t, expected, stmts) } @@ -111,10 +107,11 @@ END; ; CREATE TRIGGER test_index_update_trigger` - err := Parse(strings.NewReader(multiStmt), func(sqlQuery string) error { - return nil - }) - + scanner := NewScanner(strings.NewReader(multiStmt)) + for scanner.Scan() { + scanner.SQL() + } + err := scanner.Err() assert.Error(t, err) } @@ -136,9 +133,10 @@ CREATE TRIGGER test_index_update_trigger` StartBufSize = 100 - err := Parse(strings.NewReader(multiStmt), func(sqlQuery string) error { - return nil - }) - + scanner := NewScanner(strings.NewReader(multiStmt)) + for scanner.Scan() { + scanner.SQL() + } + err := scanner.Err() assert.NoError(t, err) } diff --git a/pkg/timex/go.mod b/pkg/timex/go.mod new file mode 100644 index 0000000..7c9faf8 --- /dev/null +++ b/pkg/timex/go.mod @@ -0,0 +1,3 @@ +module github.com/raoptimus/db-migrator.go/pkg/timex + +go 1.20 diff --git a/pkg/timex/time.go b/pkg/timex/time.go new file mode 100644 index 0000000..cb61ffa --- /dev/null +++ b/pkg/timex/time.go @@ -0,0 +1,23 @@ +package timex + +import ( + "time" +) + +type Time interface { + Now() time.Time +} + +var StdTime = New(time.Now) + +type stdTime struct { + nowFunc func() time.Time +} + +func New(nowFunc func() time.Time) Time { + return &stdTime{nowFunc: nowFunc} +} + +func (s *stdTime) Now() time.Time { + return s.nowFunc() +}