├── .github └── workflows │ ├── docker-build.yml │ └── tests.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bin ├── .env ├── build ├── clean ├── dev ├── e2e ├── fulltest └── unit ├── compose.yml ├── docker ├── Dockerfile ├── Dockerfile.alpine ├── Dockerfile.bookworm ├── Dockerfile.build ├── Dockerfile.bullseye ├── Dockerfile.debian ├── Dockerfile.scratch.amd64 ├── Dockerfile.scratch.arm64 ├── Dockerfile.slim └── dev-entrypoint.sh ├── e2e ├── down_test.go ├── init_test.go ├── latest_test.go ├── mocks │ ├── 1 │ │ ├── 1688099609991_test_init │ │ │ ├── down.sql │ │ │ └── up.sql │ │ └── 1688099659468_test_second │ │ │ ├── down.sql │ │ │ └── up.sql │ └── 2 │ │ └── 1704497146399_multiples_queries │ │ ├── down.sql │ │ └── up.sql ├── new_test.go ├── test.go ├── unlock_test.go └── up_test.go ├── go.mod ├── go.sum ├── mocks ├── .env.mssql ├── .env.mysql ├── .env.psql ├── .env.sqlite ├── mysql.yml └── postgres.yml └── src ├── cli ├── commands.go ├── routing.go └── run.go ├── lib ├── db.go ├── disk.go ├── mocks │ └── disk.go ├── snake_case.go └── tests │ ├── disk_test.go │ └── snake_case_test.go ├── main.go ├── migration ├── controller.go ├── entity.go ├── migration_repository.go ├── module.go ├── reference_repository.go ├── service.go ├── sql │ ├── check_lock.sql │ ├── create_lock_table.sql │ ├── create_reference_table.sql │ ├── delete_reference.sql │ ├── insert_lock.sql │ ├── insert_reference.sql │ ├── is_locked.sql │ ├── list_references.sql │ ├── next_id.sql │ ├── select_last_reference.sql │ └── update_lock.sql └── tests │ ├── migration_repository_test.go │ ├── reference_repository_test.go │ └── service_test.go └── settings ├── controller.go ├── entity.go ├── models └── base.yml ├── module.go ├── service.go ├── settings_repository.go └── tests ├── mocks ├── migrator-wrong.yml └── migrator.yml ├── service_test.go └── settings_repository_test.go /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: '>=1.20' 20 | 21 | - name: Install Dependencies 22 | run: go mod download 23 | 24 | - name: Run Unit Tests 25 | run: make unit 26 | 27 | build_base: 28 | runs-on: ubuntu-latest 29 | needs: tests 30 | permissions: 31 | contents: read 32 | packages: write 33 | 34 | steps: 35 | - name: Checkout Repository 36 | uses: actions/checkout@v4 37 | 38 | - name: Login to Docker Hub 39 | uses: docker/login-action@v3 40 | with: 41 | username: ${{ secrets.DOCKERHUB_USERNAME }} 42 | password: ${{ secrets.DOCKERHUB_TOKEN }} 43 | 44 | - name: Set up QEMU 45 | uses: docker/setup-qemu-action@v3 46 | 47 | - name: Set up Docker Buildx 48 | uses: docker/setup-buildx-action@v3 49 | 50 | - name: Build base image build 51 | uses: docker/build-push-action@v5 52 | with: 53 | context: . 54 | file: ./docker/Dockerfile.build 55 | platforms: linux/amd64,linux/arm64 56 | push: true 57 | tags: guilhermewebdev/migrator:build 58 | 59 | build_latest: 60 | runs-on: ubuntu-latest 61 | needs: build_base 62 | permissions: 63 | contents: read 64 | packages: write 65 | 66 | steps: 67 | - name: Checkout Repository 68 | uses: actions/checkout@v4 69 | 70 | - name: Login to Docker Hub 71 | uses: docker/login-action@v3 72 | with: 73 | username: ${{ secrets.DOCKERHUB_USERNAME }} 74 | password: ${{ secrets.DOCKERHUB_TOKEN }} 75 | 76 | - name: Set up QEMU 77 | uses: docker/setup-qemu-action@v3 78 | 79 | - name: Set up Docker Buildx 80 | uses: docker/setup-buildx-action@v3 81 | 82 | - name: Extract metadata (tags, labels) for Docker 83 | id: meta 84 | uses: docker/metadata-action@v5 85 | with: 86 | images: ${{ secrets.DOCKERHUB_USERNAME }}/migrator 87 | 88 | - name: Build and push 89 | uses: docker/build-push-action@v5 90 | with: 91 | context: . 92 | file: ./docker/Dockerfile 93 | platforms: linux/amd64,linux/arm64 94 | push: ${{ github.event_name != 'pull_request' }} 95 | tags: ${{ steps.meta.outputs.tags }} 96 | labels: ${{ steps.meta.outputs.labels }} 97 | 98 | build: 99 | runs-on: ubuntu-latest 100 | needs: build_base 101 | strategy: 102 | fail-fast: false 103 | matrix: 104 | include: 105 | - flavor: slim 106 | file: slim 107 | arch: linux/amd64,linux/arm64 108 | - flavor: alpine 109 | file: alpine 110 | arch: linux/amd64,linux/arm64 111 | - flavor: bullseye 112 | file: bullseye 113 | arch: linux/amd64,linux/arm64 114 | - flavor: bookworm 115 | file: bookworm 116 | arch: linux/amd64,linux/arm64 117 | - flavor: scratch 118 | file: scratch.amd64 119 | arch: linux/amd64 120 | - flavor: scratch 121 | file: scratch.arm64 122 | arch: linux/arm64 123 | - flavor: debian 124 | file: debian 125 | arch: linux/amd64,linux/arm64 126 | permissions: 127 | contents: read 128 | packages: write 129 | 130 | steps: 131 | - name: Checkout Repository 132 | uses: actions/checkout@v4 133 | 134 | - name: Set up QEMU 135 | uses: docker/setup-qemu-action@v3 136 | 137 | - name: Set up Docker Buildx 138 | uses: docker/setup-buildx-action@v3 139 | 140 | - name: Login to Docker Hub 141 | uses: docker/login-action@v3 142 | with: 143 | username: ${{ secrets.DOCKERHUB_USERNAME }} 144 | password: ${{ secrets.DOCKERHUB_TOKEN }} 145 | 146 | - name: Extract metadata (tags, labels) for Docker 147 | id: meta 148 | uses: docker/metadata-action@v5 149 | with: 150 | images: ${{ secrets.DOCKERHUB_USERNAME }}/migrator 151 | flavor: | 152 | latest=auto 153 | prefix=${{ matrix.flavor }}-,onlatest=true 154 | 155 | - name: Build and push 156 | uses: docker/build-push-action@v5 157 | with: 158 | context: . 159 | file: ./docker/Dockerfile.${{ matrix.file }} 160 | push: ${{ github.event_name != 'pull_request' }} 161 | platforms: ${{ matrix.arch }} 162 | tags: | 163 | ${{ steps.meta.outputs.tags }} 164 | ${{ secrets.DOCKERHUB_USERNAME }}/migrator:${{ matrix.flavor }} 165 | labels: ${{ steps.meta.outputs.labels }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: '>=1.20' 21 | 22 | - name: Install Dependencies 23 | run: go mod download 24 | 25 | - name: Run Unit Tests 26 | run: make unit 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | migrator 2 | migrations/ 3 | migrate 4 | conf-temp.yml 5 | tmp/ 6 | .cache/ 7 | .compose.override.yml 8 | .go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Guilherme Isaías SIlva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | ./bin/build 3 | run: 4 | ./bin/dev 5 | clean: 6 | ./bin/clean 7 | unit: 8 | ./bin/unit 9 | e2e: 10 | ./bin/e2e 11 | fulltest: 12 | ./bin/fulltest 13 | install: 14 | sudo cp ./bin/migrate /usr/local/bin/ 15 | uninstall: 16 | sudo rm /usr/local/bin/migrate 17 | images: 18 | docker build -t migrator:build -f docker/Dockerfile.build . 19 | docker build -t migrator:alpine -f docker/Dockerfile.alpine . 20 | docker build -t migrator:bullseye -t migrator:latest -f docker/Dockerfile.bullseye . 21 | docker build -t migrator:scratch -f docker/Dockerfile.scratch . 22 | docker build -t migrator:bookworm -f docker/Dockerfile.bookworm . 23 | docker build -t migrator:debian -f docker/Dockerfile.debian . 24 | docker build -t migrator:slim -f docker/Dockerfile.slim . 25 | docker build -t migrator:latest -f docker/Dockerfile . 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrator - Database Migration Command Line Tool 2 | 3 | ## Overview 4 | 5 | **Migrator** is a command-line tool designed to simplify the management of databases using migrations. It enables users to create, execute, and roll back migrations, ensuring a smooth and controlled evolution of the database schema. 6 | 7 | ## Version 8 | 9 | Current version: 0.3 10 | 11 | ## Author 12 | 13 | Guilherme Isaías 14 | 15 | ## Installation 16 | 17 | ### Building from source code 18 | 19 | #### Prerequisites 20 | 21 | Make sure your system meets the following requirements: 22 | 23 | Go: Ensure that Go is installed on your machine. You can download and install it from [the official Go website](https://go.dev/doc/install). 24 | 25 | Docker (optional): If you plan to build and run the migrator tool in a Docker container, make sure Docker is installed on your system. You can find instructions for installing Docker [here](https://docs.docker.com/engine/install/). 26 | 27 | #### Building and Installing 28 | 1. Clone the repository to your local machine: 29 | 30 | ```bash 31 | git clone https://github.com/guilhermewebdev/migrator.git 32 | ``` 33 | 34 | Change into the project directory: 35 | 36 | ```bash 37 | cd migrator 38 | ``` 39 | 40 | Run the build command using the provided 41 | Makefile: 42 | 43 | ```bash 44 | make build 45 | ``` 46 | 47 | This will compile the migrator tool. 48 | Install the migrator tool globally on your system: 49 | 50 | ```bash 51 | make install 52 | ``` 53 | 54 | This will copy the compiled executable to /usr/local/bin/, making it accessible system-wide. 55 | 56 | ## Docker 57 | 58 | You can also use a pre-built Docker image for Migrator available on Docker Hub. The image is hosted at [guilhermewebdev/migrator](https://hub.docker.com/r/guilhermewebdev/migrator). To run Migrator using Docker, use the following command: 59 | 60 | ```bash 61 | docker run -it guilhermewebdev/migrator:latest migrate [global options] command [command options] [arguments...] 62 | ``` 63 | 64 | Replace `[global options]`, `command`, `[command options]`, and `[arguments...]` with the specific options and commands you want to execute. 65 | 66 | ### Example using Docker 67 | 68 | ```bash 69 | # Create a new migration using Docker 70 | docker run -it guilhermewebdev/migrator:latest migrate new migration_name 71 | ``` 72 | 73 | This will execute the specified Migrator command inside a Docker container based on the provided image. 74 | 75 | Note: Ensure that you have Docker installed on your machine. You can find instructions for installing Docker [here](https://docs.docker.com/engine/install/). 76 | 77 | ## Usage 78 | 79 | ```shell 80 | migrate [global options] command [command options] [arguments...] 81 | ``` 82 | 83 | ## Commands 84 | 85 | 1. **init** 86 | - Create a new config file. 87 | 88 | 2. **new** 89 | - Creates a new migration. 90 | 91 | 3. **up** 92 | - Executes the next migration. 93 | 94 | 4. **down** 95 | - Rolls back the last migration. 96 | 97 | 5. **unlock** 98 | - Unlocks migrations. 99 | 100 | 6. **latest** 101 | - Performs missing migrations. 102 | 103 | 7. **settings** 104 | - Show settings. 105 | 106 | 8. **help, h** 107 | - Shows a list of commands or help for one command. 108 | 109 | ## Global Options 110 | 111 | - `--conf-file FILE, -c FILE` 112 | - Load configuration from FILE (default: "migrator.yml"). 113 | 114 | - `--migrations value, -m value` 115 | - Select the migrations directory (default: "./migrations"). 116 | 117 | - `--dsn value, -d value` 118 | - Database connection string. 119 | 120 | - `--driver value, -r value` 121 | - Database driver (mysql, postgres, sqlserver, sqlite, sqlite3, or oracle). 122 | 123 | - `--table value, -t value` 124 | - Migrations table name (default: "migrations"). 125 | 126 | - `--help, -h` 127 | - Show help. 128 | 129 | - `--version, -v` 130 | - Print the version. 131 | 132 | ## Example 133 | 134 | ```shell 135 | # Create a new migration 136 | migrate -c ./db/migrator.yml new 137 | 138 | # Execute the next migration 139 | migrate --driver mysql up 140 | 141 | # Rollback the last migration 142 | migrate down 143 | 144 | # Unlock migrations 145 | migrate unlock 146 | 147 | # Perform missing migrations 148 | migrate latest 149 | ``` 150 | 151 | ## Environment Variable Configuration 152 | 153 | You can apply configurations to Migrator using environment variables. The following variables are supported: 154 | 155 | - `DB_DSN`: Database connection string. 156 | - `DB_DRIVER`: Database driver (mysql, postgres, sqlserver, sqlite, sqlite3, or oracle). 157 | - `MIGRATIONS_DIR`: Select the migrations directory (default: "./migrations"). 158 | - `MIGRATIONS_TABLE`: Migrations table name (default: "migrations"). 159 | 160 | To set these variables, you can use your shell's syntax. For example, in Bash: 161 | 162 | ```bash 163 | export DB_DSN="your_database_connection_string" 164 | export DB_DRIVER="your_database_driver" 165 | export MIGRATIONS_DIR="your_migrations_directory" 166 | export MIGRATIONS_TABLE="your_migrations_table_name" 167 | ``` 168 | 169 | ## Configuration File 170 | 171 | By default, Migrate looks for a configuration file named "migrator.yml" for global settings. You can specify an alternative configuration file using the `--conf-file` option. 172 | 173 | The `migrator.yml` file is used to configure settings for the Migrator command-line tool. Below is an example of the configuration file syntax along with explanations of each parameter: 174 | 175 | ```yaml 176 | # Example migrator.yml Configuration File 177 | 178 | # Directory where migration files are stored 179 | migrations_dir: ./migrations 180 | 181 | # Name of the table to track migrations in the database 182 | migrations_table_name: migrations 183 | 184 | # Database connection string (DSN) 185 | db_dsn: "postgres://user:pass@postgres:5432/test?sslmode=disable" 186 | 187 | # Database driver (Supported drivers: mysql, postgres, sqlserver, sqlite, sqlite3, oracle) 188 | db_driver: postgres 189 | ``` 190 | 191 | **Explanation:** 192 | 193 | 1. `migrations_dir`: Specifies the directory where migration files are located. In the example, migrations are expected to be in the `./migrations` directory. You can customize this path according to your project structure. 194 | 195 | 2. `migrations_table_name`: Defines the name of the table used to track migrations in the database. The default is set to "migrations," but you can modify it based on your preferences. 196 | 197 | 3. `db_dsn`: Represents the Database Source Name (DSN), which contains information about the database connection. In the example, a PostgreSQL database connection string is provided. Update this with the appropriate credentials and connection details for your database. 198 | 199 | 4. `db_driver`: Specifies the database driver to be used (e.g., mysql, postgres, sqlserver, sqlite, sqlite3, oracle). In the example, the driver is set to "postgres." Choose the appropriate driver based on your database system. 200 | 201 | Ensure that the information in the `migrator.yml` file accurately reflects your database setup. You can customize these parameters to suit your project's requirements. If needed, refer to the [Global Options](#global-options) section in the README for additional options that can be specified when running Migrate commands. 202 | 203 | --- 204 | 205 | For additional information on each command and its options, use: 206 | 207 | ```shell 208 | migrate [command] --help 209 | ``` 210 | 211 | Thank you for using Migrator! If you have any questions or feedback, please contact Guilherme Isaías at . -------------------------------------------------------------------------------- /bin/.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT_DIR=$(realpath "$(dirname $0)/..") 4 | SRC_DIR="$ROOT_DIR/src" -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT_DIR=$(realpath "$(dirname $0)/..") 4 | source $ROOT_DIR/bin/.env 5 | ROOT_DIR=$ROOT_DIR go build -v -o $ROOT_DIR/bin/migrate $@ $SRC_DIR 6 | du -h $ROOT_DIR/bin/migrate -------------------------------------------------------------------------------- /bin/clean: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT_DIR=$(realpath "$(dirname $0)/..") 4 | source $ROOT_DIR/bin/.env 5 | git clean -fX $ROOT_DIR/* -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT_DIR=$(realpath "$(dirname $0)/..") 4 | source $ROOT_DIR/bin/.env 5 | ROOT_DIR=$ROOT_DIR go run -v $SRC_DIR $@ 6 | -------------------------------------------------------------------------------- /bin/e2e: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clear 4 | ROOT_DIR=$(realpath "$(dirname $0)/..") 5 | source $ROOT_DIR/bin/.env 6 | REPORTS=$ROOT_DIR/tmp/reports/e2e 7 | mkdir -p $REPORTS 8 | ROOT_DIR=$ROOT_DIR go test -benchmem \ 9 | -trace $REPORTS/trace.out \ 10 | -skip $ROOT_DIR/docs \ 11 | -v \ 12 | $ROOT_DIR/e2e \ 13 | $@ 14 | -------------------------------------------------------------------------------- /bin/fulltest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clear 4 | ROOT_DIR=$(realpath "$(dirname $0)/..") 5 | source $ROOT_DIR/bin/.env 6 | REPORTS=$ROOT_DIR/tmp/reports/unit 7 | mkdir -p $REPORTS 8 | ROOT_DIR=$ROOT_DIR go test -benchmem \ 9 | -coverprofile $REPORTS/cover.out \ 10 | -skip $ROOT_DIR/docs \ 11 | -v \ 12 | $SRC_DIR/... \ 13 | $ROOT_DIR/e2e/... \ 14 | $@ 15 | -------------------------------------------------------------------------------- /bin/unit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clear 4 | ROOT_DIR=$(realpath "$(dirname $0)/..") 5 | source $ROOT_DIR/bin/.env 6 | REPORTS=$ROOT_DIR/tmp/reports/unit 7 | mkdir -p $REPORTS 8 | ROOT_DIR=$ROOT_DIR go test \ 9 | -benchmem \ 10 | -coverprofile $REPORTS/cover.out \ 11 | -skip $ROOT_DIR/e2e \ 12 | -skip $ROOT_DIR/docs \ 13 | -v \ 14 | $SRC_DIR/... \ 15 | $@ 16 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | migrator: 4 | image: golang 5 | user: ${UID:-1000}:${GID:-1000} 6 | volumes: 7 | - .:/usr/src/migrator:z 8 | working_dir: /usr/src/migrator 9 | hostname: migrator 10 | entrypoint: ./docker/dev-entrypoint.sh 11 | environment: 12 | GOCACHE: /usr/src/migrator/.cache/go-build 13 | GOPATH: /usr/src/migrator/.go 14 | mysql: 15 | image: mysql 16 | volumes: 17 | - mysql_data:/var/lib/mysql 18 | environment: 19 | MYSQL_ROOT_PASSWORD: pass 20 | MYSQL_DATABASE: test 21 | MYSQL_USER: user 22 | MYSQL_PASSWORD: pass 23 | postgres: 24 | image: postgres 25 | volumes: 26 | - postgres_data:/var/lib/postgres/data 27 | environment: 28 | POSTGRES_DB: test 29 | POSTGRES_USER: user 30 | POSTGRES_PASSWORD: pass 31 | volumes: 32 | mysql_data: 33 | postgres_data: 34 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | FROM debian:latest 3 | COPY --from=build /migrator/bin/migrate /bin/migrate 4 | RUN groupadd --gid 1000 migrator && \ 5 | useradd -Mr --uid 1000 --gid 1000 -s /bin/bash migrator 6 | USER migrator -------------------------------------------------------------------------------- /docker/Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | FROM alpine 3 | COPY --from=build /migrator/bin/migrate /bin/migrate 4 | RUN addgroup -S -g 1000 migrator && \ 5 | adduser -SHD -s /bin/sh -u 1000 -G migrator migrator 6 | USER migrator -------------------------------------------------------------------------------- /docker/Dockerfile.bookworm: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | FROM debian:bookworm 3 | COPY --from=build /migrator/bin/migrate /bin/migrate 4 | RUN groupadd --gid 1000 migrator && \ 5 | useradd -Mr --uid 1000 --gid 1000 -s /bin/bash migrator 6 | USER migrator 7 | -------------------------------------------------------------------------------- /docker/Dockerfile.build: -------------------------------------------------------------------------------- 1 | FROM golang 2 | WORKDIR /migrator 3 | COPY . . 4 | ENV SRC_DIR /migrator 5 | RUN ./bin/build -------------------------------------------------------------------------------- /docker/Dockerfile.bullseye: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | FROM debian:bullseye 3 | COPY --from=build /migrator/bin/migrate /bin/migrate 4 | RUN groupadd --gid 1000 migrator && \ 5 | useradd -Mr --uid 1000 --gid 1000 -s /bin/bash migrator 6 | USER migrator -------------------------------------------------------------------------------- /docker/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | FROM debian:stable 3 | COPY --from=build /migrator/bin/migrate /bin/migrate 4 | RUN groupadd --gid 1000 migrator && \ 5 | useradd -Mr --uid 1000 --gid 1000 -s /bin/bash migrator 6 | USER migrator -------------------------------------------------------------------------------- /docker/Dockerfile.scratch.amd64: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | RUN echo "migrator:x:1000:1000::/:/migrate" > /etc/passwd_migrator 3 | FROM scratch 4 | COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 5 | COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 6 | COPY --from=build /migrator/bin/migrate /migrate 7 | COPY --from=build /etc/passwd_migrator /etc/passwd 8 | ENTRYPOINT [ "/migrate" ] 9 | USER migrator -------------------------------------------------------------------------------- /docker/Dockerfile.scratch.arm64: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | RUN echo "migrator:x:1000:1000::/:/migrate" > /etc/passwd_migrator 3 | FROM scratch 4 | COPY --from=build /lib/ld-linux-aarch64.so.1 /lib/ld-linux-aarch64.so.1 5 | COPY --from=build /lib/aarch64-linux-gnu/libc.so.6 /lib/aarch64-linux-gnu/libc.so.6 6 | COPY --from=build /migrator/bin/migrate /migrate 7 | COPY --from=build /etc/passwd_migrator /etc/passwd 8 | ENTRYPOINT [ "/migrate" ] 9 | USER migrator -------------------------------------------------------------------------------- /docker/Dockerfile.slim: -------------------------------------------------------------------------------- 1 | FROM guilhermewebdev/migrator:build as build 2 | FROM debian:stable-slim 3 | COPY --from=build /migrator/bin/migrate /bin/migrate 4 | RUN groupadd --gid 1000 migrator && \ 5 | useradd -Mr --uid 1000 --gid 1000 -s /bin/bash migrator 6 | USER migrator -------------------------------------------------------------------------------- /docker/dev-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH=$PATH:/usr/src/migrator/bin:$GOPATH/bin 4 | 5 | exec $@ -------------------------------------------------------------------------------- /e2e/down_test.go: -------------------------------------------------------------------------------- 1 | package e2e_tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/guilhermewebdev/migrator/src/cli" 8 | ) 9 | 10 | func TestDown(t *testing.T) { 11 | os.Setenv("MIGRATIONS_DIR", os.Getenv("ROOT_DIR")+"/e2e/mocks/1") 12 | env(func(envs env_set) { 13 | t.Log("\nTesting with envs: ", envs, "\n") 14 | if err := cli.Run([]string{"migrator", "up"}); err != nil { 15 | t.Fatal(err) 16 | } 17 | if err := cli.Run([]string{"migrator", "down"}); err != nil { 18 | t.Fatal(err) 19 | } 20 | }) 21 | } 22 | 23 | func TestDown_WithoutMigrations(t *testing.T) { 24 | os.Setenv("MIGRATIONS_DIR", os.Getenv("ROOT_DIR")+"/e2e/mocks/1") 25 | env(func(envs env_set) { 26 | t.Log("\nTesting with envs: ", envs, "\n") 27 | err := cli.Run([]string{"migrator", "down"}) 28 | if err == nil || err.Error() != "No migrations to rollback" { 29 | t.Fatal(err) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /e2e/init_test.go: -------------------------------------------------------------------------------- 1 | package e2e_tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/guilhermewebdev/migrator/src/cli" 8 | ) 9 | 10 | func TestInit(t *testing.T) { 11 | conf_file := os.Getenv("ROOT_DIR") + "/tmp/tmp-migrator.yml" 12 | defer os.Remove(conf_file) 13 | if err := cli.Run([]string{"migrator", "-c", conf_file, "init"}); err != nil { 14 | t.Fatal(err) 15 | } 16 | if !file_exists(conf_file) { 17 | t.Fatalf("%s file was not created", conf_file) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /e2e/latest_test.go: -------------------------------------------------------------------------------- 1 | package e2e_tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/guilhermewebdev/migrator/src/cli" 8 | ) 9 | 10 | func TestLatest(t *testing.T) { 11 | os.Setenv("MIGRATIONS_DIR", os.Getenv("ROOT_DIR")+"/e2e/mocks/1") 12 | env(func(envs env_set) { 13 | t.Log("\nTesting with envs: ", envs, "\n") 14 | if err := cli.Run([]string{"migrator", "latest"}); err != nil { 15 | t.Fatal(err) 16 | } 17 | }) 18 | } 19 | 20 | func TestLatest_WhenMigrationsWereRan(t *testing.T) { 21 | os.Setenv("MIGRATIONS_DIR", os.Getenv("ROOT_DIR")+"/e2e/mocks/1") 22 | env(func(envs env_set) { 23 | t.Log("\nTesting with envs: ", envs, "\n") 24 | for i := 1; i <= 3; i++ { 25 | if err := cli.Run([]string{"migrator", "up"}); err != nil { 26 | t.Fatal(err) 27 | } 28 | } 29 | if err := cli.Run([]string{"migrator", "latest"}); err != nil { 30 | t.Fatal(err) 31 | } 32 | }) 33 | } 34 | 35 | func TestMultiplesQueriesInSameMigraition(t *testing.T) { 36 | os.Setenv("MIGRATIONS_DIR", os.Getenv("ROOT_DIR")+"/e2e/mocks/2") 37 | env(func(envs env_set) { 38 | t.Log("\nTesting with envs: ", envs, "\n") 39 | if err := cli.Run([]string{"migrator", "latest"}); err != nil { 40 | t.Fatal(err) 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /e2e/mocks/1/1688099609991_test_init/down.sql: -------------------------------------------------------------------------------- 1 | SELECT -1 as test; 2 | -------------------------------------------------------------------------------- /e2e/mocks/1/1688099609991_test_init/up.sql: -------------------------------------------------------------------------------- 1 | SELECT 1 as test; -------------------------------------------------------------------------------- /e2e/mocks/1/1688099659468_test_second/down.sql: -------------------------------------------------------------------------------- 1 | SELECT -2 as test; 2 | -------------------------------------------------------------------------------- /e2e/mocks/1/1688099659468_test_second/up.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 as test; -------------------------------------------------------------------------------- /e2e/mocks/2/1704497146399_multiples_queries/down.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE testing; 2 | DROP TABLE testing; -------------------------------------------------------------------------------- /e2e/mocks/2/1704497146399_multiples_queries/up.sql: -------------------------------------------------------------------------------- 1 | SELECT 1 as test; 2 | SELECT 1 as test; 3 | SELECT 1 as test; -------------------------------------------------------------------------------- /e2e/new_test.go: -------------------------------------------------------------------------------- 1 | package e2e_tests 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/guilhermewebdev/migrator/src/cli" 8 | ) 9 | 10 | func TestNew(t *testing.T) { 11 | os.Setenv("MIGRATIONS_DIR", os.Getenv("ROOT_DIR")+"/tmp/migrations") 12 | if err := cli.Run([]string{"migrator", "new", "migration_test"}); err != nil { 13 | t.Fatal(err) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /e2e/test.go: -------------------------------------------------------------------------------- 1 | package e2e_tests 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type env_set map[string]string 13 | 14 | func set_env(envs env_set) { 15 | for k, v := range envs { 16 | os.Setenv(k, v) 17 | } 18 | } 19 | 20 | func file_exists(filename string) bool { 21 | _, err := os.Stat(filename) 22 | return !os.IsNotExist(err) 23 | } 24 | 25 | var test_envs = []env_set{ 26 | { 27 | "DB_DSN": "user:pass@tcp(mysql:3306)/test?multiStatements=true", 28 | "DB_DRIVER": "mysql", 29 | }, 30 | { 31 | "DB_DSN": "postgres://user:pass@postgres:5432/test?sslmode=disable", 32 | "DB_DRIVER": "postgres", 33 | }, 34 | { 35 | "DB_DSN": os.Getenv("ROOT_DIR") + "/tmp/test.sqlite3", 36 | "DB_DRIVER": "sqlite3", 37 | }, 38 | { 39 | "DB_DSN": os.Getenv("ROOT_DIR") + "/tmp/test.sqlite", 40 | "DB_DRIVER": "sqlite", 41 | }, 42 | } 43 | 44 | func new_sqlite(file_path string) { 45 | file, err := os.OpenFile(file_path, os.O_CREATE|os.O_RDWR, 0600) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | file.Close() 50 | } 51 | 52 | func env(test func(env_set)) { 53 | table_name := "table_" + strings.Replace(uuid.NewString(), "-", "_", 4) 54 | os.Setenv("MIGRATIONS_TABLE", table_name) 55 | new_sqlite(os.Getenv("ROOT_DIR") + "/tmp/test.sqlite") 56 | new_sqlite(os.Getenv("ROOT_DIR") + "/tmp/test.sqlite3") 57 | count := 1 58 | for _, envs := range test_envs { 59 | set_env(envs) 60 | test(envs) 61 | count++ 62 | } 63 | } 64 | 65 | func capture_output(f func() error) (string, error) { 66 | orig := os.Stdout 67 | r, w, _ := os.Pipe() 68 | os.Stdout = w 69 | err := f() 70 | os.Stdout = orig 71 | w.Close() 72 | out, _ := io.ReadAll(r) 73 | return string(out), err 74 | } 75 | -------------------------------------------------------------------------------- /e2e/unlock_test.go: -------------------------------------------------------------------------------- 1 | package e2e_tests 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/guilhermewebdev/migrator/src/cli" 7 | ) 8 | 9 | func TestUnlock(t *testing.T) { 10 | env(func(envs env_set) { 11 | t.Log("Testing with envs: ", envs) 12 | if err := cli.Run([]string{"migrator", "unlock"}); err != nil { 13 | t.Fatal(err) 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /e2e/up_test.go: -------------------------------------------------------------------------------- 1 | package e2e_tests 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/guilhermewebdev/migrator/src/cli" 9 | ) 10 | 11 | func TestUp(t *testing.T) { 12 | os.Setenv("MIGRATIONS_DIR", os.Getenv("ROOT_DIR")+"/e2e/mocks/1") 13 | env(func(envs env_set) { 14 | output, err := capture_output(func() error { 15 | t.Log("Testing with envs: ", envs) 16 | return cli.Run([]string{"migrator", "up"}) 17 | }) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | expected_log := "The migration \"1688099609991_test_init\" was ran." 22 | output = strings.Trim(output, "\n") 23 | if output != expected_log { 24 | t.Error(output, "is not", expected_log) 25 | t.Fatal("Invalid migration order execution") 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/guilhermewebdev/migrator 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/denisenkom/go-mssqldb v0.12.3 7 | github.com/go-sql-driver/mysql v1.7.1 8 | github.com/google/uuid v1.3.0 9 | github.com/lib/pq v1.10.9 10 | github.com/sijms/go-ora v1.3.2 11 | github.com/urfave/cli/v2 v2.25.7 12 | golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 13 | gopkg.in/yaml.v2 v2.4.0 14 | ) 15 | 16 | require ( 17 | github.com/dustin/go-humanize v1.0.1 // indirect 18 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 19 | github.com/mattn/go-isatty v0.0.16 // indirect 20 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 21 | golang.org/x/mod v0.11.0 // indirect 22 | golang.org/x/sys v0.15.0 // indirect 23 | golang.org/x/tools v0.2.0 // indirect 24 | lukechampine.com/uint128 v1.2.0 // indirect 25 | modernc.org/cc/v3 v3.40.0 // indirect 26 | modernc.org/ccgo/v3 v3.16.13 // indirect 27 | modernc.org/libc v1.22.5 // indirect 28 | modernc.org/mathutil v1.5.0 // indirect 29 | modernc.org/memory v1.5.0 // indirect 30 | modernc.org/opt v0.1.3 // indirect 31 | modernc.org/strutil v1.1.3 // indirect 32 | modernc.org/token v1.0.1 // indirect 33 | ) 34 | 35 | require ( 36 | github.com/DATA-DOG/go-sqlmock v1.5.0 37 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 38 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect 39 | github.com/golang-sql/sqlexp v0.1.0 // indirect 40 | github.com/mattn/go-sqlite3 v1.14.17 41 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 42 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 43 | golang.org/x/crypto v0.17.0 // indirect 44 | modernc.org/sqlite v1.23.1 45 | ) 46 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= 2 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= 3 | github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= 4 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 5 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 6 | github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 7 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= 11 | github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= 12 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 13 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 14 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 15 | github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= 16 | github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 17 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 18 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 19 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 20 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 21 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 22 | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= 23 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 24 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 25 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 26 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 27 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 28 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 29 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 30 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 31 | github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= 32 | github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 33 | github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= 34 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 38 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 39 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 40 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 41 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 42 | github.com/sijms/go-ora v1.3.2 h1:v9Ca63acRbrE5vYlHpABzlOvt8bI1Sj5PCVDwaAJjp8= 43 | github.com/sijms/go-ora v1.3.2/go.mod h1:ZGVmJgxUfyGIVmYgA7MVGEq6BX5aoFECRMtHW5DEcs4= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 46 | github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= 47 | github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 48 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 49 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 50 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 51 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 52 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 53 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 54 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 55 | golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= 56 | golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 57 | golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= 58 | golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 61 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 62 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 63 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 69 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 71 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 72 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 74 | golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= 75 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 79 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 80 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 81 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= 84 | lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 85 | modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= 86 | modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= 87 | modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= 88 | modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= 89 | modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= 90 | modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= 91 | modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= 92 | modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= 93 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 94 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 95 | modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= 96 | modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 97 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 98 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 99 | modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= 100 | modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= 101 | modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= 102 | modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= 103 | modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= 104 | modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= 105 | modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 106 | modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= 107 | -------------------------------------------------------------------------------- /mocks/.env.mssql: -------------------------------------------------------------------------------- 1 | export DB_DSN="sqlserver://sa:pass@mssql:1433?database=test" 2 | export DB_DRIVER=sqlserver 3 | -------------------------------------------------------------------------------- /mocks/.env.mysql: -------------------------------------------------------------------------------- 1 | export DB_DSN="user:pass@tcp(mysql:3306)/test" 2 | export DB_DRIVER=mysql -------------------------------------------------------------------------------- /mocks/.env.psql: -------------------------------------------------------------------------------- 1 | export DB_DSN="postgres://user:pass@postgres:5432/test?sslmode=disable" 2 | export DB_DRIVER=postgres -------------------------------------------------------------------------------- /mocks/.env.sqlite: -------------------------------------------------------------------------------- 1 | export DB_DSN="postgres://user:pass@postgres:5432/test?sslmode=disable" 2 | export DB_DRIVER=/opt/databases/mydb.sq3 -------------------------------------------------------------------------------- /mocks/mysql.yml: -------------------------------------------------------------------------------- 1 | migrations_dir: ./migrations 2 | migrations_table_name: migrations 3 | db_dsn: "user:pass@tcp(mysql:3306)/test" 4 | db_driver: mysql -------------------------------------------------------------------------------- /mocks/postgres.yml: -------------------------------------------------------------------------------- 1 | migrations_dir: ./migrations 2 | migrations_table_name: migrations 3 | db_dsn: "postgres://user:pass@postgres:5432/test?sslmode=disable" 4 | db_driver: postgres -------------------------------------------------------------------------------- /src/cli/commands.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/guilhermewebdev/migrator/src/lib" 7 | "github.com/guilhermewebdev/migrator/src/migration" 8 | stgs "github.com/guilhermewebdev/migrator/src/settings" 9 | "gopkg.in/yaml.v2" 10 | ) 11 | 12 | func create_migration_command(settings stgs.Settings, migration_name string) error { 13 | migrations := migration.NewMigrationModule(settings, nil) 14 | response, err := migrations.Create(migration_name) 15 | if err != nil { 16 | return err 17 | } 18 | fmt.Println(response) 19 | return nil 20 | } 21 | 22 | func up_command(pool lib.DB, settings stgs.Settings) error { 23 | migrations := migration.NewMigrationModule(settings, pool) 24 | response, err := migrations.Up() 25 | if err != nil { 26 | return err 27 | } 28 | fmt.Println(response) 29 | return nil 30 | } 31 | 32 | func unlock_command(pool lib.DB, settings stgs.Settings) error { 33 | migrations := migration.NewMigrationModule(settings, pool) 34 | response, err := migrations.Unlock() 35 | if err != nil { 36 | return err 37 | } 38 | fmt.Println(response) 39 | return nil 40 | } 41 | 42 | func down_command(pool lib.DB, settings stgs.Settings) error { 43 | migrations := migration.NewMigrationModule(settings, pool) 44 | response, err := migrations.Down() 45 | if err != nil { 46 | return err 47 | } 48 | fmt.Println(response) 49 | return nil 50 | } 51 | 52 | func latest_command(pool lib.DB, settings stgs.Settings) error { 53 | migrations := migration.NewMigrationModule(settings, pool) 54 | response, err := migrations.Latest() 55 | if err != nil { 56 | return err 57 | } 58 | fmt.Println(response) 59 | return nil 60 | } 61 | 62 | func settings_command(settings stgs.Settings) error { 63 | yamlData, err := yaml.Marshal(&settings) 64 | if err != nil { 65 | return err 66 | } 67 | fmt.Println(string(yamlData)) 68 | return nil 69 | } 70 | 71 | func init_command(settings_file_path string) error { 72 | module := stgs.NewSettingsModule() 73 | response, err := module.Init(settings_file_path) 74 | if err != nil { 75 | return err 76 | } 77 | fmt.Println(response) 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /src/cli/routing.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/guilhermewebdev/migrator/src/lib" 8 | stgs "github.com/guilhermewebdev/migrator/src/settings" 9 | lib_cli "github.com/urfave/cli/v2" 10 | ) 11 | 12 | type context struct { 13 | s stgs.Settings 14 | c *lib_cli.Context 15 | pool lib.DB 16 | } 17 | 18 | func get_settings_file_name(ctx *lib_cli.Context) string { 19 | file_name_from_args := ctx.String("conf-file") 20 | var settings_file string = "./migrator.yml" 21 | if len(file_name_from_args) > 0 { 22 | settings_file = file_name_from_args 23 | } 24 | return settings_file 25 | } 26 | 27 | func load_settings(ctx *lib_cli.Context) (stgs.Settings, error) { 28 | settings_file := get_settings_file_name(ctx) 29 | settings, err := stgs.NewSettingsModule().Get(settings_file) 30 | if err != nil { 31 | return stgs.Settings{}, err 32 | } 33 | migrations_dir_from_args := ctx.String("migrations") 34 | if len(migrations_dir_from_args) > 0 { 35 | settings.MigrationsDir = migrations_dir_from_args 36 | } 37 | dsn_from_args := ctx.String("dsn") 38 | if len(dsn_from_args) > 0 { 39 | settings.DB_DSN = dsn_from_args 40 | } 41 | driver_from_args := ctx.String("driver") 42 | if len(driver_from_args) > 0 { 43 | settings.DB_Driver = driver_from_args 44 | } 45 | table_name_from_args := ctx.String("table") 46 | if len(table_name_from_args) > 0 { 47 | settings.MigrationsTableName = table_name_from_args 48 | } 49 | return settings, nil 50 | } 51 | 52 | func call(action func(context) error) lib_cli.ActionFunc { 53 | return func(ctx *lib_cli.Context) error { 54 | settings, err := load_settings(ctx) 55 | if err != nil { 56 | return err 57 | } 58 | return action(context{settings, ctx, nil}) 59 | } 60 | } 61 | 62 | func db(action func(context) error) lib_cli.ActionFunc { 63 | return func(ctx *lib_cli.Context) error { 64 | settings, err := load_settings(ctx) 65 | if err != nil { 66 | return err 67 | } 68 | pool, err := lib.ConnectDB(lib.ConnectionParams{ 69 | DSN: settings.DB_DSN, 70 | Driver: settings.DB_Driver, 71 | }) 72 | if err != nil { 73 | return err 74 | } 75 | defer func() { 76 | err := pool.Close() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | }() 81 | return action(context{settings, ctx, pool}) 82 | } 83 | } 84 | 85 | func BuildRouter() *lib_cli.App { 86 | app := &lib_cli.App{ 87 | Name: "Migrator", 88 | Usage: "Manage your databases with migrations", 89 | Version: "0.3", 90 | Compiled: time.Now().UTC(), 91 | EnableBashCompletion: true, 92 | HelpName: "migrate", 93 | DefaultCommand: "help", 94 | Flags: []lib_cli.Flag{ 95 | &lib_cli.StringFlag{ 96 | Name: "conf-file", 97 | Aliases: []string{"c"}, 98 | Usage: "Load configuration from `FILE` (default: \"migrator.yml\")", 99 | EnvVars: []string{"CONF_FILE"}, 100 | }, 101 | &lib_cli.StringFlag{ 102 | Name: "migrations", 103 | Aliases: []string{"m"}, 104 | Usage: "Select the migrations directory (default: \"./migrations\")", 105 | EnvVars: []string{"MIGRATIONS_DIR"}, 106 | }, 107 | &lib_cli.StringFlag{ 108 | Name: "dsn", 109 | Aliases: []string{"d"}, 110 | Usage: "Database connection string", 111 | EnvVars: []string{"DB_DSN"}, 112 | }, 113 | &lib_cli.StringFlag{ 114 | Name: "driver", 115 | Aliases: []string{"r"}, 116 | Usage: "Database driver (mysql, postgres, sqlserver, sqlite, sqlite3 or oracle)", 117 | EnvVars: []string{"DB_DRIVER"}, 118 | }, 119 | &lib_cli.StringFlag{ 120 | Name: "table", 121 | Aliases: []string{"t"}, 122 | Usage: "Migrations table name(default: \"migrations\") ", 123 | EnvVars: []string{"MIGRATIONS_TABLE"}, 124 | }, 125 | }, 126 | Authors: []*lib_cli.Author{ 127 | { 128 | Name: "Guilherme Isaías", 129 | Email: "guilherme@cibernetica.dev", 130 | }, 131 | }, 132 | Commands: []*lib_cli.Command{ 133 | { 134 | Name: "init", 135 | Usage: "Create a new config file", 136 | Action: func(ctx *lib_cli.Context) error { 137 | return init_command(get_settings_file_name(ctx)) 138 | }, 139 | }, 140 | { 141 | Name: "new", 142 | Usage: "Creates a new migration", 143 | Action: call(func(ctx context) error { 144 | return create_migration_command(ctx.s, ctx.c.Args().First()) 145 | }), 146 | }, 147 | { 148 | Name: "up", 149 | Usage: "Execute the next migration", 150 | Action: db(func(ctx context) error { 151 | return up_command(ctx.pool, ctx.s) 152 | }), 153 | }, 154 | { 155 | Name: "down", 156 | Usage: "Rollback the last migration", 157 | Action: db(func(ctx context) error { 158 | return down_command(ctx.pool, ctx.s) 159 | }), 160 | }, 161 | { 162 | Name: "unlock", 163 | Usage: "Unlock migrations", 164 | Action: db(func(ctx context) error { 165 | return unlock_command(ctx.pool, ctx.s) 166 | }), 167 | }, 168 | { 169 | Name: "latest", 170 | Usage: "Perform missing migrations", 171 | Action: db(func(ctx context) error { 172 | return latest_command(ctx.pool, ctx.s) 173 | }), 174 | }, 175 | { 176 | Name: "settings", 177 | Usage: "Show settings", 178 | Action: call(func(ctx context) error { 179 | return settings_command(ctx.s) 180 | }), 181 | }, 182 | }, 183 | } 184 | return app 185 | } 186 | -------------------------------------------------------------------------------- /src/cli/run.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "time" 6 | ) 7 | 8 | func Run(args []string) error { 9 | os.Setenv("TZ", "UTC") 10 | loc, err := time.LoadLocation("UTC") 11 | if err != nil { 12 | return err 13 | } 14 | time.Local = loc 15 | app := BuildRouter() 16 | if err := app.Run(args); err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/db.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | _ "github.com/denisenkom/go-mssqldb" 8 | _ "github.com/go-sql-driver/mysql" 9 | _ "github.com/lib/pq" 10 | _ "github.com/mattn/go-sqlite3" 11 | _ "github.com/sijms/go-ora" 12 | _ "modernc.org/sqlite" 13 | ) 14 | 15 | type ConnectionParams struct { 16 | DSN string 17 | Driver string 18 | } 19 | 20 | type DB_Row interface { 21 | Err() error 22 | Scan(dest ...any) error 23 | } 24 | 25 | type DB_Rows interface { 26 | Close() error 27 | ColumnTypes() ([]*sql.ColumnType, error) 28 | Columns() ([]string, error) 29 | Err() error 30 | Next() bool 31 | NextResultSet() bool 32 | Scan(dest ...any) error 33 | } 34 | 35 | type DB interface { 36 | Query(query string, args ...any) (*sql.Rows, error) 37 | Exec(query string, args ...any) (sql.Result, error) 38 | QueryRow(query string, args ...any) *sql.Row 39 | Close() error 40 | } 41 | 42 | func ConnectDB(p ConnectionParams) (DB, error) { 43 | if p.DSN == "" { 44 | return &sql.DB{}, fmt.Errorf("Invalid DSN string") 45 | } 46 | if p.Driver == "" { 47 | return &sql.DB{}, fmt.Errorf("Invalid Driver string") 48 | } 49 | pool, err := sql.Open(p.Driver, p.DSN) 50 | return pool, err 51 | } 52 | -------------------------------------------------------------------------------- /src/lib/disk.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type Disk interface { 10 | Create(file_path string) error 11 | List(dir string) ([]string, error) 12 | Read(file_path string) (string, error) 13 | SearchFileInParentDirectories(file_name string) (string, error) 14 | Write(file_name string, content string) error 15 | } 16 | 17 | type DiskImpl struct{} 18 | 19 | func (d *DiskImpl) Create(file_path string) error { 20 | path_name := filepath.Dir(file_path) 21 | directory, _ := filepath.Abs(path_name) 22 | if _, err := os.Stat(path_name); err != nil { 23 | if err := os.MkdirAll(directory, fs.ModePerm); err != nil { 24 | return err 25 | } 26 | } 27 | full_file_name, err := filepath.Abs(file_path) 28 | if err != nil { 29 | return err 30 | } 31 | file, err := os.Create(full_file_name) 32 | defer file.Close() 33 | if err != nil { 34 | return err 35 | } 36 | file.Chmod(0644) 37 | return nil 38 | } 39 | 40 | func (d *DiskImpl) List(dir string) ([]string, error) { 41 | entries, err := os.ReadDir(dir) 42 | if err != nil { 43 | return []string{}, nil 44 | } 45 | names := []string{} 46 | for _, entry := range entries { 47 | names = append(names, entry.Name()) 48 | } 49 | return names, nil 50 | } 51 | 52 | func (d *DiskImpl) Read(file_path string) (string, error) { 53 | full_file_name, _ := filepath.Abs(file_path) 54 | data, err := os.ReadFile(full_file_name) 55 | if err != nil { 56 | return "", err 57 | } 58 | return string(data), nil 59 | } 60 | 61 | func (r *DiskImpl) SearchFileInParentDirectories(file_name string) (string, error) { 62 | current_dir, err := os.Getwd() 63 | if err != nil { 64 | return "", err 65 | } 66 | for { 67 | file_path := filepath.Join(current_dir, file_name) 68 | _, err := os.Stat(file_path) 69 | if err == nil { 70 | return file_path, nil 71 | } 72 | if current_dir == filepath.Dir(current_dir) { 73 | break 74 | } 75 | current_dir = filepath.Dir(current_dir) 76 | } 77 | return "", nil 78 | } 79 | 80 | func (r *DiskImpl) Write(file_path string, content string) error { 81 | return os.WriteFile(file_path, []byte(content), 0644) 82 | } 83 | -------------------------------------------------------------------------------- /src/lib/mocks/disk.go: -------------------------------------------------------------------------------- 1 | package lib_mocks 2 | 3 | type DiskMock struct { 4 | Creations []string 5 | Lists []string 6 | Reads []string 7 | Writes [][]string 8 | CreationMock error 9 | ListMock []string 10 | ListErrorMock error 11 | ReadMock string 12 | ReadErrorMock error 13 | SearchFileInParentDirectories_file_name string 14 | SearchFileInParentDirectories_return string 15 | SearchFileInParentDirectories_error error 16 | WriteError error 17 | } 18 | 19 | func (disk *DiskMock) Create(file_path string) error { 20 | disk.Creations = append(disk.Creations, file_path) 21 | return disk.CreationMock 22 | } 23 | 24 | func (disk *DiskMock) List(dir string) ([]string, error) { 25 | disk.Lists = append(disk.Lists, dir) 26 | return disk.ListMock, disk.ListErrorMock 27 | } 28 | 29 | func (disk *DiskMock) Read(file_path string) (string, error) { 30 | disk.Reads = append(disk.Reads, file_path) 31 | return disk.ReadMock, disk.ReadErrorMock 32 | } 33 | 34 | func (disk *DiskMock) SearchFileInParentDirectories(file_name string) (string, error) { 35 | disk.SearchFileInParentDirectories_file_name = file_name 36 | return disk.SearchFileInParentDirectories_return, disk.SearchFileInParentDirectories_error 37 | } 38 | 39 | func (disk *DiskMock) Write(file_name string, content string) error { 40 | writing := []string{file_name, content} 41 | disk.Writes = append(disk.Writes, writing) 42 | return disk.WriteError 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/snake_case.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | func SnakeCase(text string) string { 9 | var result strings.Builder 10 | for i, char := range text { 11 | if unicode.IsSpace(char) { 12 | result.WriteRune('_') 13 | } else if unicode.IsUpper(char) { 14 | if i > 0 && unicode.IsLower(rune(text[i-1])) { 15 | result.WriteRune('_') 16 | } 17 | result.WriteRune(unicode.ToLower(char)) 18 | } else { 19 | result.WriteRune(char) 20 | } 21 | } 22 | return result.String() 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/tests/disk_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/guilhermewebdev/migrator/src/lib" 8 | ) 9 | 10 | func clear() { 11 | os.RemoveAll("./test") 12 | os.RemoveAll("./mocks") 13 | } 14 | 15 | func createMocks() { 16 | var repo lib.Disk = &lib.DiskImpl{} 17 | repo.Create("./mocks/file.test") 18 | os.WriteFile("./mocks/file.test", []byte("hello"), 0644) 19 | } 20 | 21 | func setup() func() { 22 | clear() 23 | createMocks() 24 | return func() { 25 | clear() 26 | } 27 | } 28 | 29 | func TestCreateFile(t *testing.T) { 30 | defer setup()() 31 | var repo lib.Disk = &lib.DiskImpl{} 32 | err := repo.Create("./test/file.sql") 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | if _, err := os.Stat("./test/file.sql"); err != nil { 37 | t.Error(err) 38 | } 39 | } 40 | 41 | func TestCreateDeepFile(t *testing.T) { 42 | defer setup()() 43 | var repo lib.Disk = &lib.DiskImpl{} 44 | err := repo.Create("./test/testing/test/test/tes/file.sql") 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | if _, err := os.Stat("./test/testing/test/test/tes/file.sql"); err != nil { 49 | t.Error(err) 50 | } 51 | } 52 | 53 | func TestCreateTheSameFileMultiplesTimes(t *testing.T) { 54 | defer setup()() 55 | var repo lib.Disk = &lib.DiskImpl{} 56 | err := repo.Create("./test/testing/test/test/tes/file.sql") 57 | repo.Create("./test/testing/test/test/tes/file.sql") 58 | repo.Create("./test/testing/test/test/tes/file.sql") 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | if _, err := os.Stat("./test/testing/test/test/tes/file.sql"); err != nil { 63 | t.Error(err) 64 | } 65 | } 66 | 67 | func TestListDirectories(t *testing.T) { 68 | defer setup()() 69 | var repo lib.Disk = &lib.DiskImpl{} 70 | names, err := repo.List("./mocks") 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | if len(names) < 1 { 75 | t.Fail() 76 | } 77 | var this_file_was_found bool 78 | for _, name := range names { 79 | if name == "file.test" { 80 | this_file_was_found = true 81 | } 82 | } 83 | if !this_file_was_found { 84 | t.Fail() 85 | } 86 | } 87 | 88 | func TestReadFile(t *testing.T) { 89 | defer setup()() 90 | var repo lib.Disk = &lib.DiskImpl{} 91 | data, err := repo.Read("./mocks/file.test") 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | if data != "hello" { 96 | t.Fail() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/lib/tests/snake_case_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/guilhermewebdev/migrator/src/lib" 7 | ) 8 | 9 | func TestSnakeCase(t *testing.T) { 10 | expect_mapping := map[string]string{ 11 | "Testing snake case": "testing_snake_case", 12 | "TestingSnakeCase": "testing_snake_case", 13 | "testingSnakeCase": "testing_snake_case", 14 | "TESTING_SNAKE_CASE": "testing_snake_case", 15 | "TESTING SNAKE CASE": "testing_snake_case", 16 | } 17 | for text, expecting := range expect_mapping { 18 | exemple := lib.SnakeCase(text) 19 | if exemple != expecting { 20 | t.Log(exemple, " is not ", expecting, " no exemplo ", text) 21 | t.Fail() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/guilhermewebdev/migrator/src/cli" 8 | ) 9 | 10 | func main() { 11 | if err := cli.Run(os.Args); err != nil { 12 | log.Fatal(err) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/migration/controller.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import "fmt" 4 | 5 | type Controller interface { 6 | Create(name string) (string, error) 7 | Up() (string, error) 8 | Unlock() (string, error) 9 | Down() (string, error) 10 | Latest() (string, error) 11 | } 12 | 13 | type ControllerImpl struct { 14 | Service Service 15 | } 16 | 17 | func (c *ControllerImpl) Create(name string) (string, error) { 18 | if name == "" { 19 | return "", fmt.Errorf("You should inform the migration name") 20 | } 21 | if len(name) > 100 { 22 | return "", fmt.Errorf("Migration name must not exceed 100 characters") 23 | } 24 | if err := c.Service.Create(name); err != nil { 25 | return "Failed to create the migration", err 26 | } 27 | return "Migration was created successfully", nil 28 | } 29 | 30 | func (c *ControllerImpl) Up() (string, error) { 31 | migration, err := c.Service.Up() 32 | if err != nil { 33 | return "Failed to execute migration", err 34 | } 35 | empty := Migration{} 36 | if migration == empty { 37 | return "No migrations to apply", nil 38 | } 39 | return "The migration \"" + migration.Name + "\" was ran.", nil 40 | } 41 | 42 | func (c *ControllerImpl) Unlock() (string, error) { 43 | if err := c.Service.Unlock(); err != nil { 44 | return "Failed to unlock migrations", err 45 | } 46 | return "The migrations are unlocked", nil 47 | } 48 | 49 | func (c *ControllerImpl) Down() (string, error) { 50 | migration, err := c.Service.Down() 51 | if err != nil { 52 | return "Failed to execute migration", err 53 | } 54 | empty := Migration{} 55 | if migration == empty { 56 | return "No migrations to apply", nil 57 | } 58 | return "The migration \"" + migration.Name + "\" was rolled back.", nil 59 | } 60 | 61 | func (c *ControllerImpl) Latest() (string, error) { 62 | migrations, err := c.Service.Latest() 63 | message := fmt.Sprintf("[PERFORMED %d MIGRATIONS]:\n", len(migrations)) 64 | for _, migration := range migrations { 65 | message = message + migration.Name + "\n" 66 | } 67 | message = message + "=======" + "\n" 68 | if err != nil { 69 | message = message + err.Error() 70 | return message, fmt.Errorf(message) 71 | } 72 | return message, nil 73 | } 74 | -------------------------------------------------------------------------------- /src/migration/entity.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import "time" 4 | 5 | type Migration struct { 6 | Name string 7 | Path string 8 | UpQuery string 9 | DownQuery string 10 | } 11 | 12 | type Reference struct { 13 | ID string 14 | Name string 15 | Date time.Time 16 | Order int 17 | } 18 | 19 | type Relation struct { 20 | Migration Migration 21 | Reference Reference 22 | } 23 | -------------------------------------------------------------------------------- /src/migration/migration_repository.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "path" 5 | 6 | "github.com/guilhermewebdev/migrator/src/lib" 7 | "github.com/guilhermewebdev/migrator/src/settings" 8 | ) 9 | 10 | type MigrationRepository interface { 11 | Create(name string) error 12 | List() ([]Migration, error) 13 | Read(key string) (Migration, error) 14 | } 15 | 16 | type MigrationRepositoryImpl struct { 17 | Disk lib.Disk 18 | Settings settings.Settings 19 | } 20 | 21 | func (r *MigrationRepositoryImpl) Create(name string) error { 22 | new_migration_path := path.Join(r.Settings.MigrationsDir, name) 23 | if err := r.Disk.Create(new_migration_path + "/up.sql"); err != nil { 24 | return err 25 | } 26 | if err := r.Disk.Create(new_migration_path + "/down.sql"); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | func (r *MigrationRepositoryImpl) List() ([]Migration, error) { 33 | keys, err := r.Disk.List(r.Settings.MigrationsDir) 34 | if err != nil { 35 | return nil, err 36 | } 37 | var migrations []Migration 38 | for _, key := range keys { 39 | migration, err := r.Read(key) 40 | if err != nil { 41 | return migrations, err 42 | } 43 | migrations = append(migrations, migration) 44 | } 45 | return migrations, nil 46 | } 47 | 48 | func (r *MigrationRepositoryImpl) Read(key string) (Migration, error) { 49 | empty := Migration{} 50 | dir := path.Join(r.Settings.MigrationsDir, key) 51 | up, err := r.Disk.Read(path.Join(dir, "up.sql")) 52 | if err != nil { 53 | return empty, err 54 | } 55 | down, err := r.Disk.Read(path.Join(dir, "down.sql")) 56 | if err != nil { 57 | return empty, err 58 | } 59 | return Migration{ 60 | Name: key, 61 | Path: path.Join(r.Settings.MigrationsDir, key), 62 | UpQuery: up, 63 | DownQuery: down, 64 | }, nil 65 | } 66 | -------------------------------------------------------------------------------- /src/migration/module.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "github.com/guilhermewebdev/migrator/src/lib" 5 | stgs "github.com/guilhermewebdev/migrator/src/settings" 6 | ) 7 | 8 | type MigrationModule interface { 9 | Controller() Controller 10 | } 11 | 12 | type MigrationModuleImpl struct { 13 | controller Controller 14 | } 15 | 16 | func (mod *MigrationModuleImpl) Controller() Controller { 17 | return mod.controller 18 | } 19 | 20 | func NewMigrationModule(settings stgs.Settings, pool lib.DB) Controller { 21 | var disk lib.Disk = &lib.DiskImpl{} 22 | var migrations MigrationRepository = &MigrationRepositoryImpl{ 23 | Disk: disk, 24 | Settings: settings, 25 | } 26 | var db ReferenceRepository = &ReferenceRepositoryImpl{ 27 | Settings: settings, 28 | DB: pool, 29 | } 30 | var service Service = &ServiceImpl{ 31 | Migrations: migrations, 32 | References: db, 33 | } 34 | var controller Controller = &ControllerImpl{ 35 | Service: service, 36 | } 37 | return controller 38 | } 39 | -------------------------------------------------------------------------------- /src/migration/reference_repository.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | _ "embed" 7 | "fmt" 8 | "strings" 9 | "text/template" 10 | "time" 11 | 12 | "github.com/guilhermewebdev/migrator/src/lib" 13 | "github.com/guilhermewebdev/migrator/src/settings" 14 | "golang.org/x/exp/maps" 15 | ) 16 | 17 | type ReferenceRepository interface { 18 | List() ([]Reference, error) 19 | Up(migration Migration) error 20 | Down(migration Migration) error 21 | Lock() error 22 | Unlock() error 23 | IsLocked() (bool, error) 24 | Prepare() error 25 | GetLast() (Reference, error) 26 | } 27 | 28 | type scannable interface { 29 | Scan(dest ...any) error 30 | } 31 | 32 | type ReferenceRepositoryImpl struct { 33 | DB lib.DB 34 | Settings settings.Settings 35 | } 36 | 37 | var ( 38 | //go:embed sql/list_references.sql 39 | list_references_sql string 40 | 41 | //go:embed sql/create_reference_table.sql 42 | create_reference_table_sql string 43 | 44 | //go:embed sql/create_lock_table.sql 45 | create_lock_table_sql string 46 | 47 | //go:embed sql/insert_reference.sql 48 | insert_reference_sql string 49 | 50 | //go:embed sql/is_locked.sql 51 | is_locked_sql string 52 | 53 | //go:embed sql/next_id.sql 54 | next_id_sql string 55 | 56 | //go:embed sql/check_lock.sql 57 | check_lock_sql string 58 | 59 | //go:embed sql/insert_lock.sql 60 | insert_lock_sql string 61 | 62 | //go:embed sql/update_lock.sql 63 | update_lock_sql string 64 | 65 | //go:embed sql/select_last_reference.sql 66 | select_last_reference_sql string 67 | 68 | //go:embed sql/delete_reference.sql 69 | delete_reference_sql string 70 | ) 71 | 72 | const DB_TIMESTAMP_FORMAT = "2006-01-02 15:04:05" 73 | 74 | type P map[string]any 75 | 76 | func (r *ReferenceRepositoryImpl) format(query string, values ...P) (string, error) { 77 | var formatted bytes.Buffer 78 | variables := P{} 79 | for _, value := range values { 80 | maps.Copy(variables, value) 81 | } 82 | variables["migrations_table"] = r.Settings.MigrationsTableName 83 | variables["migrations_lock_table"] = r.Settings.MigrationsTableName + "_lock" 84 | tmpl, err := template.New("query").Parse(query) 85 | if err != nil { 86 | return "", err 87 | } 88 | if err := tmpl.Execute(&formatted, variables); err != nil { 89 | return "", err 90 | } 91 | return formatted.String(), nil 92 | } 93 | 94 | func (r *ReferenceRepositoryImpl) exec(query string, values ...P) (sql.Result, error) { 95 | formatted, err := r.format(query, values...) 96 | if err != nil { 97 | return nil, err 98 | } 99 | return r.DB.Exec(formatted) 100 | } 101 | 102 | func (r *ReferenceRepositoryImpl) query(query string, values ...P) (lib.DB_Rows, error) { 103 | formatted, err := r.format(query, values...) 104 | if err != nil { 105 | return &sql.Rows{}, err 106 | } 107 | return r.DB.Query(formatted) 108 | } 109 | 110 | func (r *ReferenceRepositoryImpl) query_row(query string, values ...P) (lib.DB_Row, error) { 111 | formatted, err := r.format(query, values...) 112 | if err != nil { 113 | return &sql.Row{}, err 114 | } 115 | row := r.DB.QueryRow(formatted) 116 | return row, row.Err() 117 | } 118 | 119 | func (r *ReferenceRepositoryImpl) scan_ref(row scannable) (Reference, error) { 120 | var reference Reference 121 | var date []byte 122 | err := row.Scan( 123 | &reference.ID, 124 | &reference.Name, 125 | &date, 126 | ) 127 | if err != nil { 128 | return reference, err 129 | } 130 | without_z := strings.Replace(string(date), "Z", "", 1) 131 | without_t := strings.Replace(without_z, "T", " ", 1) 132 | reference.Date, err = time.Parse(DB_TIMESTAMP_FORMAT, without_t) 133 | return reference, err 134 | } 135 | 136 | func (r *ReferenceRepositoryImpl) Prepare() error { 137 | if _, err := r.exec(create_reference_table_sql); err != nil { 138 | return err 139 | } 140 | if _, err := r.exec(create_lock_table_sql); err != nil { 141 | return err 142 | } 143 | return nil 144 | } 145 | 146 | func (r *ReferenceRepositoryImpl) genReferenceId() (int, error) { 147 | row, err := r.query_row(next_id_sql) 148 | if err != nil { 149 | return 0, err 150 | } 151 | var next_id int 152 | row.Scan(&next_id) 153 | return next_id, row.Err() 154 | } 155 | 156 | func (r *ReferenceRepositoryImpl) List() ([]Reference, error) { 157 | var references []Reference 158 | query := list_references_sql 159 | rows, err := r.query(query) 160 | if err != nil { 161 | return references, err 162 | } 163 | for rows.Next() { 164 | reference, err := r.scan_ref(rows) 165 | if err != nil { 166 | return references, err 167 | } 168 | references = append(references, reference) 169 | } 170 | if !rows.NextResultSet() { 171 | return references, rows.Err() 172 | } 173 | return references, nil 174 | } 175 | 176 | func (r *ReferenceRepositoryImpl) Up(m Migration) error { 177 | if _, err := r.DB.Exec(m.UpQuery); err != nil { 178 | return err 179 | } 180 | id, err := r.genReferenceId() 181 | if err != nil { 182 | return err 183 | } 184 | if _, err = r.exec(insert_reference_sql, P{ 185 | "id": id, 186 | "migration_key": m.Name, 187 | "created_at": time.Now().UTC().Format(DB_TIMESTAMP_FORMAT), 188 | }); err != nil { 189 | return err 190 | } 191 | return nil 192 | } 193 | 194 | func (r *ReferenceRepositoryImpl) Down(m Migration) error { 195 | if _, err := r.DB.Exec(m.DownQuery); err != nil { 196 | return err 197 | } 198 | if _, err := r.exec(delete_reference_sql, P{ 199 | "migration_key": m.Name, 200 | }); err != nil { 201 | return err 202 | } 203 | return nil 204 | } 205 | 206 | func (r *ReferenceRepositoryImpl) IsLocked() (bool, error) { 207 | row, err := r.query_row(is_locked_sql) 208 | if err != nil { 209 | return false, err 210 | } 211 | var is_locked bool 212 | row.Scan(&is_locked) 213 | return is_locked, row.Err() 214 | } 215 | 216 | func (r *ReferenceRepositoryImpl) setLock(is_locked bool) error { 217 | var exists_data bool 218 | row, err := r.query_row(check_lock_sql) 219 | if err != nil { 220 | return err 221 | } 222 | if err := row.Scan(&exists_data); err != nil { 223 | return err 224 | } 225 | var query string 226 | if exists_data { 227 | query = update_lock_sql 228 | } else { 229 | query = insert_lock_sql 230 | } 231 | _, err = r.exec(query, P{"is_locked": is_locked}) 232 | return err 233 | } 234 | 235 | func (r *ReferenceRepositoryImpl) Lock() error { 236 | return r.setLock(true) 237 | } 238 | 239 | func (r *ReferenceRepositoryImpl) Unlock() error { 240 | return r.setLock(false) 241 | } 242 | 243 | func (r *ReferenceRepositoryImpl) GetLast() (Reference, error) { 244 | row, err := r.query_row(select_last_reference_sql) 245 | if err != nil { 246 | return Reference{}, err 247 | } 248 | reference, err := r.scan_ref(row) 249 | if err != nil && err.Error() == "sql: no rows in result set" { 250 | return Reference{}, fmt.Errorf("No migrations to rollback") 251 | } 252 | return reference, err 253 | } 254 | -------------------------------------------------------------------------------- /src/migration/service.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sort" 7 | "time" 8 | 9 | "github.com/guilhermewebdev/migrator/src/lib" 10 | ) 11 | 12 | type Service interface { 13 | Create(name string) error 14 | Up() (Migration, error) 15 | Unlock() error 16 | Down() (Migration, error) 17 | Latest() ([]Migration, error) 18 | } 19 | 20 | type ServiceImpl struct { 21 | Migrations MigrationRepository 22 | References ReferenceRepository 23 | } 24 | 25 | func (s *ServiceImpl) semaphore() func() { 26 | if err := s.References.Prepare(); err != nil { 27 | log.Fatal(err) 28 | } 29 | is_locked, err := s.References.IsLocked() 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | if is_locked { 34 | log.Fatal("The migrations are locked. Unlock it to continue.") 35 | } 36 | if err = s.References.Lock(); err != nil { 37 | log.Fatal(err) 38 | } 39 | return func() { 40 | if err = s.References.Unlock(); err != nil { 41 | log.Fatal(err) 42 | } 43 | } 44 | } 45 | 46 | func (s *ServiceImpl) relateMigrationWithReference() ([]Relation, error) { 47 | references, err := s.References.List() 48 | if err != nil { 49 | return []Relation{}, err 50 | } 51 | migrations, err := s.Migrations.List() 52 | if err != nil { 53 | return []Relation{}, err 54 | } 55 | if len(references) > len(migrations) { 56 | return []Relation{}, fmt.Errorf("Migrations are corrupted") 57 | } 58 | if len(migrations) == 0 { 59 | return []Relation{}, nil 60 | } 61 | var relations []Relation 62 | references_related := 0 63 | for i, migration := range migrations { 64 | relation := Relation{ 65 | Migration: migration, 66 | Reference: Reference{}, 67 | } 68 | if len(references) > i && references[i].Name == migration.Name { 69 | relation.Reference = references[i] 70 | references_related += 1 71 | } else { 72 | for _, reference := range references { 73 | if reference.Name == migration.Name { 74 | relation.Reference = reference 75 | references_related += 1 76 | break 77 | } 78 | } 79 | } 80 | relations = append(relations, relation) 81 | } 82 | if references_related < len(references) { 83 | return []Relation{}, fmt.Errorf("Migrations are corrupted") 84 | } 85 | sort.Slice(relations, func(i, j int) bool { 86 | return relations[i].Migration.Name < relations[j].Migration.Name 87 | }) 88 | return relations, nil 89 | } 90 | 91 | func (s *ServiceImpl) getNextMigration() (Migration, error) { 92 | relations, err := s.relateMigrationWithReference() 93 | if err != nil { 94 | return Migration{}, err 95 | } 96 | for _, relation := range relations { 97 | if relation.Reference.Name == "" { 98 | return relation.Migration, nil 99 | } 100 | } 101 | return Migration{}, nil 102 | } 103 | 104 | func (s *ServiceImpl) listMissingMigrations() ([]Migration, error) { 105 | relations, err := s.relateMigrationWithReference() 106 | if err != nil { 107 | return []Migration{}, err 108 | } 109 | migrations := []Migration{} 110 | for _, relation := range relations { 111 | if relation.Reference.Name == "" && relation.Reference.ID == "" { 112 | migrations = append(migrations, relation.Migration) 113 | } 114 | } 115 | return migrations, nil 116 | } 117 | 118 | func (s *ServiceImpl) Create(name string) error { 119 | snake_case_name := lib.SnakeCase(name) 120 | now := time.Now().UnixMilli() 121 | migration_name := fmt.Sprint(now) + "_" + snake_case_name 122 | return s.Migrations.Create(migration_name) 123 | } 124 | 125 | func (s *ServiceImpl) Up() (Migration, error) { 126 | defer s.semaphore()() 127 | empty := Migration{} 128 | migration, err := s.getNextMigration() 129 | if err != nil { 130 | return empty, err 131 | } 132 | if migration == empty { 133 | return empty, nil 134 | } 135 | err = s.References.Up(migration) 136 | if err != nil { 137 | return empty, err 138 | } 139 | return migration, nil 140 | } 141 | 142 | func (s *ServiceImpl) Unlock() error { 143 | if err := s.References.Prepare(); err != nil { 144 | return err 145 | } 146 | return s.References.Unlock() 147 | } 148 | 149 | func (s *ServiceImpl) Down() (Migration, error) { 150 | defer s.semaphore()() 151 | empty := Migration{} 152 | last_reference, err := s.References.GetLast() 153 | if err != nil { 154 | return empty, err 155 | } 156 | migration, err := s.Migrations.Read(last_reference.Name) 157 | if err != nil { 158 | return empty, err 159 | } 160 | err = s.References.Down(migration) 161 | return migration, err 162 | } 163 | 164 | func (s *ServiceImpl) Latest() ([]Migration, error) { 165 | defer s.semaphore()() 166 | performed_migrations := []Migration{} 167 | missing_migrations, err := s.listMissingMigrations() 168 | if err != nil { 169 | return performed_migrations, err 170 | } 171 | for _, migration := range missing_migrations { 172 | err = s.References.Up(migration) 173 | if err != nil { 174 | return performed_migrations, err 175 | } 176 | performed_migrations = append(performed_migrations, migration) 177 | } 178 | return performed_migrations, nil 179 | } 180 | -------------------------------------------------------------------------------- /src/migration/sql/check_lock.sql: -------------------------------------------------------------------------------- 1 | SELECT COUNT(id) AS lock_count 2 | FROM {{.migrations_lock_table}}; -------------------------------------------------------------------------------- /src/migration/sql/create_lock_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS {{.migrations_lock_table}} ( 2 | id INT NOT NULL, 3 | is_locked BOOLEAN NOT NULL DEFAULT false, 4 | PRIMARY KEY (id) 5 | ); -------------------------------------------------------------------------------- /src/migration/sql/create_reference_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS {{.migrations_table}} ( 2 | id INT PRIMARY KEY NOT NULL, 3 | migration_key VARCHAR(128) NOT NULL, 4 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 5 | ); 6 | -------------------------------------------------------------------------------- /src/migration/sql/delete_reference.sql: -------------------------------------------------------------------------------- 1 | DELETE 2 | FROM {{.migrations_table}} 3 | WHERE migration_key = '{{.migration_key}}'; -------------------------------------------------------------------------------- /src/migration/sql/insert_lock.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO {{.migrations_lock_table}} (id, is_locked) 2 | SELECT 1, {{.is_locked}}; 3 | -------------------------------------------------------------------------------- /src/migration/sql/insert_reference.sql: -------------------------------------------------------------------------------- 1 | INSERT 2 | INTO {{.migrations_table}} (id, migration_key, created_at) 3 | VALUES ({{.id}}, '{{.migration_key}}', '{{.created_at}}'); -------------------------------------------------------------------------------- /src/migration/sql/is_locked.sql: -------------------------------------------------------------------------------- 1 | SELECT COALESCE(is_locked, false) AS is_locked 2 | FROM {{.migrations_lock_table}} 3 | LIMIT 1; -------------------------------------------------------------------------------- /src/migration/sql/list_references.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM {{.migrations_table}} 3 | ORDER BY created_at ASC; -------------------------------------------------------------------------------- /src/migration/sql/next_id.sql: -------------------------------------------------------------------------------- 1 | SELECT MAX(id) + 1 AS next_id 2 | FROM {{.migrations_table}}; -------------------------------------------------------------------------------- /src/migration/sql/select_last_reference.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM {{.migrations_table}} 3 | ORDER BY migration_key DESC 4 | LIMIT 1; -------------------------------------------------------------------------------- /src/migration/sql/update_lock.sql: -------------------------------------------------------------------------------- 1 | UPDATE {{.migrations_lock_table}} 2 | SET is_locked = {{.is_locked}}; -------------------------------------------------------------------------------- /src/migration/tests/migration_repository_test.go: -------------------------------------------------------------------------------- 1 | package migration_test 2 | 3 | import ( 4 | "log" 5 | "regexp" 6 | "testing" 7 | 8 | lib_mocks "github.com/guilhermewebdev/migrator/src/lib/mocks" 9 | "github.com/guilhermewebdev/migrator/src/migration" 10 | "github.com/guilhermewebdev/migrator/src/settings" 11 | ) 12 | 13 | func get_settings() settings.Settings { 14 | settings := settings.Settings{ 15 | MigrationsDir: "./migrations", 16 | MigrationsTableName: "migrations", 17 | } 18 | return settings 19 | } 20 | 21 | func TestMigrationRepository_Create(t *testing.T) { 22 | t.Parallel() 23 | disk := lib_mocks.DiskMock{} 24 | var repo migration.MigrationRepository = &migration.MigrationRepositoryImpl{ 25 | Disk: &disk, 26 | Settings: get_settings(), 27 | } 28 | if err := repo.Create("test"); err != nil { 29 | t.Fatal(err) 30 | } 31 | if matched, err := regexp.Match( 32 | "test/(up|down).sql$", 33 | []byte(disk.Creations[0]), 34 | ); err != nil || !matched { 35 | log.Fatal(err, matched, disk.Creations) 36 | } 37 | } 38 | 39 | func TestMigrationRepository_List(t *testing.T) { 40 | t.Parallel() 41 | disk := lib_mocks.DiskMock{ 42 | ListMock: []string{ 43 | "test", 44 | }, 45 | ReadMock: "SELECT * FROM table;", 46 | } 47 | var repo migration.MigrationRepository = &migration.MigrationRepositoryImpl{ 48 | Disk: &disk, 49 | Settings: get_settings(), 50 | } 51 | migrations, err := repo.List() 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | if len(migrations) != len(disk.ListMock) { 56 | t.Fatal(migrations, disk) 57 | } 58 | if migrations[0].UpQuery != disk.ReadMock { 59 | t.Fatal(migrations, disk) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/migration/tests/reference_repository_test.go: -------------------------------------------------------------------------------- 1 | package migration_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/DATA-DOG/go-sqlmock" 7 | mod "github.com/guilhermewebdev/migrator/src/migration" 8 | ) 9 | 10 | func TestReferenceRepository_List(t *testing.T) { 11 | t.Parallel() 12 | db, mock, err := sqlmock.New() 13 | if err != nil { 14 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 15 | } 16 | defer db.Close() 17 | mocked_rows := sqlmock.NewRows([]string{ 18 | "ID", "migration_key", "created_at", 19 | }).AddRow(2, "migration_test", "2006-01-02 15:04:05") 20 | mock.ExpectQuery("^SELECT \\* FROM migrations"). 21 | WillReturnRows(mocked_rows) 22 | repo := mod.ReferenceRepositoryImpl{ 23 | Settings: get_settings(), 24 | DB: db, 25 | } 26 | list, err := repo.List() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if len(list) != 1 { 31 | t.Fatal(list) 32 | } 33 | if err := mock.ExpectationsWereMet(); err != nil { 34 | t.Errorf("there were unfulfilled expectations: %s", err) 35 | } 36 | } 37 | 38 | func TestReferenceRepository_Up(t *testing.T) { 39 | t.Parallel() 40 | db, mock, err := sqlmock.New() 41 | if err != nil { 42 | t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) 43 | } 44 | defer db.Close() 45 | mock.ExpectExec("^CREATE").WillReturnResult(sqlmock.NewResult(1, 0)) 46 | mock.ExpectQuery("^SELECT MAX"). 47 | WillReturnRows(sqlmock.NewRows([]string{"next_id"}).AddRow(2)) 48 | mock.ExpectExec("^INSERT").WillReturnResult(sqlmock.NewResult(1, 1)) 49 | migration := mod.Migration{ 50 | UpQuery: "CREATE TABLE test;", 51 | } 52 | repo := mod.ReferenceRepositoryImpl{ 53 | Settings: get_settings(), 54 | DB: db, 55 | } 56 | err = repo.Up(migration) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/migration/tests/service_test.go: -------------------------------------------------------------------------------- 1 | package migration_test 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | mod "github.com/guilhermewebdev/migrator/src/migration" 9 | ) 10 | 11 | type referenceRepositoryMock struct { 12 | listMock []mod.Reference 13 | listMockError error 14 | migrationsRan []mod.Migration 15 | migrationRunMockError error 16 | referenceMock mod.Reference 17 | referenceMockError error 18 | lockStatus bool 19 | lockMockError error 20 | prepareMockError error 21 | } 22 | 23 | func (r *referenceRepositoryMock) Prepare() error { 24 | return r.prepareMockError 25 | } 26 | 27 | func (r *referenceRepositoryMock) List() ([]mod.Reference, error) { 28 | return r.listMock, r.listMockError 29 | } 30 | 31 | func (r *referenceRepositoryMock) Up(migration mod.Migration) error { 32 | r.migrationsRan = append(r.migrationsRan, migration) 33 | return r.migrationRunMockError 34 | } 35 | 36 | func (r *referenceRepositoryMock) Down(migration mod.Migration) error { 37 | r.migrationsRan = append(r.migrationsRan, migration) 38 | return r.migrationRunMockError 39 | } 40 | 41 | func (r *referenceRepositoryMock) GetLast() (mod.Reference, error) { 42 | return r.referenceMock, r.referenceMockError 43 | } 44 | 45 | func (r *referenceRepositoryMock) Lock() error { 46 | r.lockStatus = true 47 | return r.lockMockError 48 | } 49 | 50 | func (r *referenceRepositoryMock) Unlock() error { 51 | r.lockStatus = false 52 | return r.lockMockError 53 | } 54 | 55 | func (r *referenceRepositoryMock) IsLocked() (bool, error) { 56 | return r.lockStatus, r.lockMockError 57 | } 58 | 59 | type migrationsRepositoryMock struct { 60 | creations []string 61 | creationMockError error 62 | listMock []mod.Migration 63 | listMockError error 64 | migrationMock mod.Migration 65 | migrationErrorMock error 66 | } 67 | 68 | func (r *migrationsRepositoryMock) Create(name string) error { 69 | r.creations = append(r.creations, name) 70 | return r.creationMockError 71 | } 72 | 73 | func (r *migrationsRepositoryMock) List() ([]mod.Migration, error) { 74 | return r.listMock, r.listMockError 75 | } 76 | 77 | func (r *migrationsRepositoryMock) Read(name string) (mod.Migration, error) { 78 | return r.migrationMock, r.migrationErrorMock 79 | } 80 | 81 | func TestService_Create(t *testing.T) { 82 | t.Parallel() 83 | migrations := &migrationsRepositoryMock{} 84 | references := &referenceRepositoryMock{} 85 | var service mod.Service = &mod.ServiceImpl{ 86 | Migrations: migrations, 87 | References: references, 88 | } 89 | err := service.Create("create_user") 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | pattern := `^[0-9]{1,}_[A-z_]{1,}$` 94 | matched0, err := regexp.MatchString(pattern, migrations.creations[0]) 95 | if err != nil || !matched0 { 96 | t.Log(migrations.creations, matched0) 97 | t.Fatal() 98 | } 99 | } 100 | 101 | func TestService_Up_WithPendingMigration(t *testing.T) { 102 | t.Parallel() 103 | pending_migration := mod.Migration{ 104 | Name: "testing", 105 | Path: "testing", 106 | } 107 | migrations := &migrationsRepositoryMock{ 108 | listMock: []mod.Migration{pending_migration}, 109 | } 110 | references := &referenceRepositoryMock{} 111 | var service mod.Service = &mod.ServiceImpl{ 112 | Migrations: migrations, 113 | References: references, 114 | } 115 | migration, err := service.Up() 116 | if err != nil { 117 | t.Error(err) 118 | } 119 | if migration != pending_migration { 120 | t.Fatal() 121 | } 122 | } 123 | 124 | func TestService_Up_WhenAllMigrationsAreRan(t *testing.T) { 125 | t.Parallel() 126 | migrations := &migrationsRepositoryMock{ 127 | listMock: []mod.Migration{{ 128 | Name: "testing", 129 | Path: "testing", 130 | }}, 131 | } 132 | references := &referenceRepositoryMock{ 133 | listMock: []mod.Reference{{ 134 | Name: "testing", 135 | }}, 136 | } 137 | var service mod.Service = &mod.ServiceImpl{ 138 | Migrations: migrations, 139 | References: references, 140 | } 141 | ran, err := service.Up() 142 | if err != nil { 143 | t.Error(err) 144 | } 145 | expected := mod.Migration{} 146 | if ran != expected { 147 | t.Fatal() 148 | } 149 | } 150 | 151 | func TestService_Up_WhenHasNoMigrations(t *testing.T) { 152 | t.Parallel() 153 | migrations := &migrationsRepositoryMock{} 154 | references := &referenceRepositoryMock{} 155 | var service mod.Service = &mod.ServiceImpl{ 156 | Migrations: migrations, 157 | References: references, 158 | } 159 | ran, err := service.Up() 160 | if err != nil { 161 | t.Error(err) 162 | } 163 | expected := mod.Migration{} 164 | if ran != expected { 165 | t.Fatal() 166 | } 167 | } 168 | 169 | func TestService_Up_WhenMigrationsAreCorrupted(t *testing.T) { 170 | t.Parallel() 171 | expected_migration := mod.Migration{} 172 | migrations := &migrationsRepositoryMock{ 173 | listMock: []mod.Migration{ 174 | { 175 | Name: "testing", 176 | Path: "testing", 177 | }, 178 | }, 179 | } 180 | references := &referenceRepositoryMock{ 181 | listMock: []mod.Reference{{ 182 | Name: "wrong_testing", 183 | }}, 184 | } 185 | var service mod.Service = &mod.ServiceImpl{ 186 | Migrations: migrations, 187 | References: references, 188 | } 189 | ran, err := service.Up() 190 | if err == nil { 191 | t.Fatal("No one error are raised") 192 | } 193 | if ran != expected_migration { 194 | t.Fatal(ran, " is not ", expected_migration) 195 | } 196 | if err.Error() != "Migrations are corrupted" { 197 | t.Fatal(err) 198 | } 199 | } 200 | 201 | func TestService_Up_WhenMigrationsAreDisorderly(t *testing.T) { 202 | t.Parallel() 203 | migrations := &migrationsRepositoryMock{ 204 | listMock: []mod.Migration{ 205 | { 206 | Name: "0_testing", 207 | Path: "testing", 208 | }, 209 | { 210 | Name: "1_testing", 211 | Path: "testing", 212 | }, 213 | { 214 | Name: "2_testing", 215 | Path: "testing", 216 | }, 217 | }, 218 | } 219 | references := &referenceRepositoryMock{ 220 | listMock: []mod.Reference{ 221 | { 222 | Name: "0_testing", 223 | }, 224 | { 225 | Name: "2_testing", 226 | }, 227 | }, 228 | } 229 | var service mod.Service = &mod.ServiceImpl{ 230 | Migrations: migrations, 231 | References: references, 232 | } 233 | ran, err := service.Up() 234 | if err != nil { 235 | t.Fatal(err) 236 | } 237 | expected := mod.Migration{ 238 | Name: "1_testing", 239 | Path: "testing", 240 | } 241 | if ran != expected { 242 | t.Fatal(ran, " is not ", expected) 243 | } 244 | } 245 | 246 | func TestService_Unlock(t *testing.T) { 247 | t.Parallel() 248 | migrations := &migrationsRepositoryMock{} 249 | references := &referenceRepositoryMock{} 250 | var service mod.Service = &mod.ServiceImpl{ 251 | Migrations: migrations, 252 | References: references, 253 | } 254 | if err := service.Unlock(); err != nil { 255 | t.Fatal(err) 256 | } 257 | } 258 | 259 | func TestService_Down(t *testing.T) { 260 | t.Parallel() 261 | expected_migration := mod.Migration{Name: "2_testing", Path: "testing"} 262 | migrations := &migrationsRepositoryMock{ 263 | migrationMock: expected_migration, 264 | } 265 | references := &referenceRepositoryMock{ 266 | referenceMock: mod.Reference{ 267 | Name: "2_testing", 268 | }, 269 | } 270 | var service mod.Service = &mod.ServiceImpl{ 271 | Migrations: migrations, 272 | References: references, 273 | } 274 | migration, err := service.Down() 275 | if err != nil { 276 | t.Fatal(err) 277 | } 278 | if migration != expected_migration { 279 | t.Fatal(migration, " is not ", expected_migration) 280 | } 281 | } 282 | 283 | func TestService_Down_WithoutReferences(t *testing.T) { 284 | t.Parallel() 285 | migrations := &migrationsRepositoryMock{ 286 | migrationMock: mod.Migration{Name: "2_testing", Path: "testing"}, 287 | } 288 | references := &referenceRepositoryMock{ 289 | referenceMockError: fmt.Errorf("No migrations to rollback"), 290 | } 291 | var service mod.Service = &mod.ServiceImpl{ 292 | Migrations: migrations, 293 | References: references, 294 | } 295 | migration, err := service.Down() 296 | if err == nil || err.Error() != "No migrations to rollback" { 297 | t.Fatal(err) 298 | } 299 | empty := mod.Migration{} 300 | if migration != empty { 301 | t.Fatal(migration, " is not ", empty) 302 | } 303 | } 304 | 305 | func TestService_Latest(t *testing.T) { 306 | t.Parallel() 307 | migrations := &migrationsRepositoryMock{ 308 | listMock: []mod.Migration{ 309 | {Name: "1_testing", Path: "testing_1"}, 310 | {Name: "2_testing", Path: "testing_2"}, 311 | }, 312 | } 313 | references := &referenceRepositoryMock{ 314 | referenceMockError: fmt.Errorf("No migrations to rollback"), 315 | } 316 | var service mod.Service = &mod.ServiceImpl{ 317 | Migrations: migrations, 318 | References: references, 319 | } 320 | performed_migrations, err := service.Latest() 321 | if err != nil { 322 | t.Fatal(err) 323 | } 324 | if len(performed_migrations) != 2 { 325 | t.Fatal("The migrations should have length 1 ", performed_migrations) 326 | } 327 | } 328 | 329 | func TestService_WhenMigrationsAlreadyWereRan(t *testing.T) { 330 | t.Parallel() 331 | migrations := &migrationsRepositoryMock{} 332 | references := &referenceRepositoryMock{ 333 | referenceMockError: fmt.Errorf("No migrations to rollback"), 334 | } 335 | var service mod.Service = &mod.ServiceImpl{ 336 | Migrations: migrations, 337 | References: references, 338 | } 339 | performed_migrations, err := service.Latest() 340 | if err != nil { 341 | t.Fatal(err) 342 | } 343 | if len(performed_migrations) != 0 { 344 | t.Fatal("The migrations should have length 1 ", performed_migrations) 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/settings/controller.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import "fmt" 4 | 5 | type Controller interface { 6 | Get(file_name string) (Settings, error) 7 | Init(file_path string) (string, error) 8 | } 9 | 10 | type ControllerImpl struct { 11 | Service Service 12 | } 13 | 14 | func (c *ControllerImpl) Get(file_name string) (Settings, error) { 15 | settings, err := c.Service.Get(file_name) 16 | if err != nil { 17 | return Settings{}, err 18 | } 19 | return settings, err 20 | } 21 | 22 | func (c *ControllerImpl) Init(file_path string) (string, error) { 23 | if err := c.Service.Init(file_path); err != nil { 24 | return "", err 25 | } 26 | return fmt.Sprintf("%s conf file was created", file_path), nil 27 | } 28 | -------------------------------------------------------------------------------- /src/settings/entity.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | type Settings struct { 4 | MigrationsDir string `yaml:"migrations_dir"` 5 | MigrationsTableName string `yaml:"migrations_table_name"` 6 | DB_DSN string `yaml:"db_dsn"` 7 | DB_Driver string `yaml:"db_driver"` 8 | } 9 | -------------------------------------------------------------------------------- /src/settings/models/base.yml: -------------------------------------------------------------------------------- 1 | # Migrator settings 2 | # Generated by version Migrator v0.3 3 | # Change the information below accordingly with your necessity 4 | 5 | # Directory where migration files are stored 6 | migrations_dir: ./migrations 7 | 8 | # Name of the table to track migrations in the database 9 | migrations_table_name: migrations 10 | 11 | # Database connection string (DSN) 12 | # Uncomment a specific DSN that you need 13 | 14 | # Postgres DSN 15 | db_dsn: postgres://$DB_USER:$DB_PASS@$DB_HOST:$DB_PORT/$DB_NAME 16 | 17 | # MySQL DSN 18 | # db_dsn: $DB_USER:$DB_PASS@tcp($DB_HOST:$DB_PORT)/$DB_NAME 19 | 20 | # MS SQL Server DSN 21 | # db_dsn: sqlserver://$DB_USER:$DB_PASS@$DB_HOST:$DB_PORT?database=$DB_NAME 22 | 23 | # SQLite DSN 24 | # db_dsn: $CWD/db.sqlite3 25 | 26 | # Database driver (Supported drivers: mysql, postgres, sqlserver, sqlite, sqlite3, oracle) 27 | db_driver: postgres -------------------------------------------------------------------------------- /src/settings/module.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import "github.com/guilhermewebdev/migrator/src/lib" 4 | 5 | func NewSettingsModule() Controller { 6 | var disk lib.Disk = &lib.DiskImpl{} 7 | var repository SettingsRepository = &SettingsRepositoryImpl{ 8 | Disk: disk, 9 | } 10 | var service Service = &ServiceImpl{ 11 | Settings: repository, 12 | } 13 | var controller = &ControllerImpl{ 14 | Service: service, 15 | } 16 | return controller 17 | } 18 | -------------------------------------------------------------------------------- /src/settings/service.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | type Service interface { 8 | Get(settings_file_name string) (Settings, error) 9 | Init(settings_file_name string) error 10 | } 11 | 12 | type ServiceImpl struct { 13 | Settings SettingsRepository 14 | } 15 | 16 | var ( 17 | //go:embed models/base.yml 18 | base_settings_file string 19 | ) 20 | 21 | func (s *ServiceImpl) get_default_settings() Settings { 22 | return Settings{ 23 | MigrationsDir: "./migrations", 24 | MigrationsTableName: "migrations", 25 | DB_DSN: "", 26 | DB_Driver: "", 27 | } 28 | } 29 | 30 | func (s *ServiceImpl) combine_settings(stgs ...Settings) Settings { 31 | var final_settings Settings 32 | for _, current := range stgs { 33 | if current.MigrationsDir != "" { 34 | final_settings.MigrationsDir = current.MigrationsDir 35 | } 36 | if current.MigrationsTableName != "" { 37 | final_settings.MigrationsTableName = current.MigrationsTableName 38 | } 39 | if current.DB_DSN != "" { 40 | final_settings.DB_DSN = current.DB_DSN 41 | } 42 | if current.DB_Driver != "" { 43 | final_settings.DB_Driver = current.DB_Driver 44 | } 45 | } 46 | return final_settings 47 | } 48 | 49 | func (s *ServiceImpl) Get(settings_file_name string) (Settings, error) { 50 | initial := s.get_default_settings() 51 | env_settings, err := s.Settings.GetFromEnv() 52 | if err != nil { 53 | return env_settings, err 54 | } 55 | file_settings, err := s.Settings.GetFromFile(settings_file_name) 56 | if err != nil { 57 | return file_settings, err 58 | } 59 | settings := s.combine_settings(initial, env_settings, file_settings) 60 | return settings, nil 61 | } 62 | 63 | func (s *ServiceImpl) Init(settings_file_name string) error { 64 | if err := s.Settings.CreateFile(settings_file_name); err != nil { 65 | return err 66 | } 67 | if err := s.Settings.WriteFile(settings_file_name, base_settings_file); err != nil { 68 | return err 69 | } 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /src/settings/settings_repository.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/guilhermewebdev/migrator/src/lib" 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | type SettingsRepository interface { 11 | GetFromEnv() (Settings, error) 12 | GetFromFile(file_name string) (Settings, error) 13 | CreateFile(file_path string) error 14 | WriteFile(file_path string, content string) error 15 | } 16 | 17 | type SettingsRepositoryImpl struct { 18 | Disk lib.Disk 19 | } 20 | 21 | func (r *SettingsRepositoryImpl) GetFromEnv() (Settings, error) { 22 | var settings Settings = Settings{ 23 | DB_DSN: os.Getenv("DB_DSN"), 24 | DB_Driver: os.Getenv("DB_DRIVER"), 25 | MigrationsDir: os.Getenv("MIGRATIONS_DIR"), 26 | MigrationsTableName: os.Getenv("MIGRATIONS_TABLE"), 27 | } 28 | return settings, nil 29 | } 30 | 31 | func (r *SettingsRepositoryImpl) get_settings_file_content(file_path string) (Settings, error) { 32 | data, err := r.Disk.Read(file_path) 33 | settings := Settings{} 34 | if err != nil { 35 | return settings, err 36 | } 37 | data_with_envs := os.ExpandEnv(data) 38 | err = yaml.Unmarshal([]byte(data_with_envs), &settings) 39 | if err != nil { 40 | return settings, err 41 | } 42 | return settings, nil 43 | } 44 | 45 | func (r *SettingsRepositoryImpl) GetFromFile(file_name string) (Settings, error) { 46 | file_path, err := r.Disk.SearchFileInParentDirectories(file_name) 47 | empty := Settings{} 48 | if err != nil || file_path == "" { 49 | return empty, err 50 | } 51 | return r.get_settings_file_content(file_path) 52 | } 53 | 54 | func (r *SettingsRepositoryImpl) CreateFile(file_path string) error { 55 | return r.Disk.Create(file_path) 56 | } 57 | 58 | func (r *SettingsRepositoryImpl) WriteFile(file_path string, content string) error { 59 | return r.Disk.Write(file_path, content) 60 | } 61 | -------------------------------------------------------------------------------- /src/settings/tests/mocks/migrator-wrong.yml: -------------------------------------------------------------------------------- 1 | =34f 2 | ''f -------------------------------------------------------------------------------- /src/settings/tests/mocks/migrator.yml: -------------------------------------------------------------------------------- 1 | migrations_dir: ./test 2 | migrations_table_name: testing_table_name 3 | db_dsn: testing_dsn 4 | db_driver: testing_driver -------------------------------------------------------------------------------- /src/settings/tests/service_test.go: -------------------------------------------------------------------------------- 1 | package settings_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | stgs "github.com/guilhermewebdev/migrator/src/settings" 8 | ) 9 | 10 | type settingsRepositoryMock struct { 11 | getFromEnvResponse stgs.Settings 12 | getFromFileResponse stgs.Settings 13 | getFromEnvError error 14 | getFromFileError error 15 | createFileError error 16 | fileCreated string 17 | fileWritingName string 18 | fileWritingContent string 19 | writeFileError error 20 | } 21 | 22 | func (s *settingsRepositoryMock) GetFromEnv() (stgs.Settings, error) { 23 | return s.getFromEnvResponse, s.getFromEnvError 24 | } 25 | 26 | func (s *settingsRepositoryMock) GetFromFile(_ string) (stgs.Settings, error) { 27 | return s.getFromFileResponse, s.getFromFileError 28 | } 29 | 30 | func (s *settingsRepositoryMock) CreateFile(file_name string) error { 31 | s.fileCreated = file_name 32 | return s.createFileError 33 | } 34 | 35 | func (s *settingsRepositoryMock) WriteFile(file_name string, content string) error { 36 | s.fileWritingContent = content 37 | s.fileWritingName = file_name 38 | return s.writeFileError 39 | } 40 | 41 | func TestGetSettings_Default(t *testing.T) { 42 | var repository stgs.SettingsRepository = &settingsRepositoryMock{ 43 | getFromEnvResponse: stgs.Settings{}, 44 | getFromFileResponse: stgs.Settings{}, 45 | getFromEnvError: nil, 46 | getFromFileError: nil, 47 | } 48 | service := &stgs.ServiceImpl{ 49 | Settings: repository, 50 | } 51 | settings, err := service.Get("migrator.yml") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | expected := stgs.Settings{ 56 | MigrationsDir: "./migrations", 57 | MigrationsTableName: "migrations", 58 | DB_DSN: "", 59 | DB_Driver: "", 60 | } 61 | if settings != expected { 62 | t.Fatal(expected, "is not", settings) 63 | } 64 | } 65 | 66 | func TestGetSettings_WhenMigrationsDirComesFromFile(t *testing.T) { 67 | var repository stgs.SettingsRepository = &settingsRepositoryMock{ 68 | getFromEnvResponse: stgs.Settings{}, 69 | getFromFileResponse: stgs.Settings{ 70 | MigrationsDir: "./m", 71 | }, 72 | getFromEnvError: nil, 73 | getFromFileError: nil, 74 | } 75 | service := &stgs.ServiceImpl{ 76 | Settings: repository, 77 | } 78 | settings, err := service.Get("migrator.yml") 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | expected := stgs.Settings{ 83 | MigrationsDir: "./m", 84 | MigrationsTableName: "migrations", 85 | DB_DSN: "", 86 | DB_Driver: "", 87 | } 88 | if settings != expected { 89 | t.Fatal(expected, "is not", settings) 90 | } 91 | } 92 | 93 | func TestGetSettings_WhenMigrationsDirComesFromEnv(t *testing.T) { 94 | var repository stgs.SettingsRepository = &settingsRepositoryMock{ 95 | getFromEnvResponse: stgs.Settings{ 96 | MigrationsDir: "./m", 97 | }, 98 | getFromFileResponse: stgs.Settings{}, 99 | getFromEnvError: nil, 100 | getFromFileError: nil, 101 | } 102 | service := &stgs.ServiceImpl{ 103 | Settings: repository, 104 | } 105 | settings, err := service.Get("migrator.yml") 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | expected := stgs.Settings{ 110 | MigrationsDir: "./m", 111 | MigrationsTableName: "migrations", 112 | DB_DSN: "", 113 | DB_Driver: "", 114 | } 115 | if settings != expected { 116 | t.Fatal(expected, "is not", settings) 117 | } 118 | } 119 | 120 | func TestGetSettings_WhenDB_DSNComesFromEnv(t *testing.T) { 121 | var repository stgs.SettingsRepository = &settingsRepositoryMock{ 122 | getFromEnvResponse: stgs.Settings{ 123 | MigrationsDir: "./m", 124 | DB_DSN: "postgres://host..", 125 | }, 126 | getFromFileResponse: stgs.Settings{}, 127 | getFromEnvError: nil, 128 | getFromFileError: nil, 129 | } 130 | service := &stgs.ServiceImpl{ 131 | Settings: repository, 132 | } 133 | settings, err := service.Get("migrator.yml") 134 | if err != nil { 135 | t.Fatal(err) 136 | } 137 | expected := stgs.Settings{ 138 | MigrationsDir: "./m", 139 | MigrationsTableName: "migrations", 140 | DB_DSN: "postgres://host..", 141 | DB_Driver: "", 142 | } 143 | if settings != expected { 144 | t.Fatal(expected, "is not", settings) 145 | } 146 | } 147 | 148 | func TestInit(t *testing.T) { 149 | var repository stgs.SettingsRepository = &settingsRepositoryMock{ 150 | writeFileError: nil, 151 | createFileError: nil, 152 | } 153 | service := &stgs.ServiceImpl{ 154 | Settings: repository, 155 | } 156 | err := service.Init("migrator.yml") 157 | if err != nil { 158 | t.Fatal(err) 159 | } 160 | } 161 | 162 | func TestInit_WhenWriteFileError(t *testing.T) { 163 | expected := fmt.Errorf("Some error") 164 | var repository stgs.SettingsRepository = &settingsRepositoryMock{ 165 | writeFileError: expected, 166 | createFileError: nil, 167 | } 168 | service := &stgs.ServiceImpl{ 169 | Settings: repository, 170 | } 171 | err := service.Init("migrator.yml") 172 | if err != expected { 173 | t.Fatal(err) 174 | } 175 | } 176 | 177 | func TestInit_WhenCreateFileError(t *testing.T) { 178 | expected := fmt.Errorf("Some error") 179 | var repository stgs.SettingsRepository = &settingsRepositoryMock{ 180 | writeFileError: nil, 181 | createFileError: expected, 182 | } 183 | service := &stgs.ServiceImpl{ 184 | Settings: repository, 185 | } 186 | err := service.Init("migrator.yml") 187 | if err != expected { 188 | t.Fatal(err) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/settings/tests/settings_repository_test.go: -------------------------------------------------------------------------------- 1 | package settings_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/guilhermewebdev/migrator/src/lib" 8 | lib_mocks "github.com/guilhermewebdev/migrator/src/lib/mocks" 9 | "github.com/guilhermewebdev/migrator/src/settings" 10 | ) 11 | 12 | func TestGetSettingsFromEnv(t *testing.T) { 13 | var repository settings.SettingsRepository = &settings.SettingsRepositoryImpl{ 14 | Disk: &lib_mocks.DiskMock{}, 15 | } 16 | os.Setenv("DB_DSN", "a") 17 | os.Setenv("DB_DRIVER", "b") 18 | os.Setenv("MIGRATIONS_DIR", "c") 19 | os.Setenv("MIGRATIONS_TABLE", "d") 20 | expected := settings.Settings{ 21 | MigrationsDir: "c", 22 | MigrationsTableName: "d", 23 | DB_DSN: "a", 24 | DB_Driver: "b", 25 | } 26 | settings, err := repository.GetFromEnv() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if settings != expected { 31 | t.Fatal(settings, "is not", expected) 32 | } 33 | } 34 | 35 | func TestGetSettingsFromFile(t *testing.T) { 36 | var repository settings.SettingsRepository = &settings.SettingsRepositoryImpl{ 37 | Disk: &lib.DiskImpl{}, 38 | } 39 | os.Setenv("DB_DSN", "a") 40 | os.Setenv("DB_DRIVER", "b") 41 | os.Setenv("MIGRATIONS_DIR", "c") 42 | os.Setenv("MIGRATIONS_TABLE", "d") 43 | expected := settings.Settings{ 44 | MigrationsDir: "./test", 45 | MigrationsTableName: "testing_table_name", 46 | DB_DSN: "testing_dsn", 47 | DB_Driver: "testing_driver", 48 | } 49 | settings, err := repository.GetFromFile("./mocks/migrator.yml") 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | if settings != expected { 54 | t.Fatal(settings, "is not", expected) 55 | } 56 | } 57 | 58 | func TestGetSettingsFromFile_WhenFileIsInvalid(t *testing.T) { 59 | var repository settings.SettingsRepository = &settings.SettingsRepositoryImpl{ 60 | Disk: &lib.DiskImpl{}, 61 | } 62 | expected := settings.Settings{} 63 | settings, err := repository.GetFromFile("./mocks/migrator-wrong.yml") 64 | if err == nil { 65 | t.Fatal("Error should be raised") 66 | } 67 | if settings != expected { 68 | t.Fatal(settings, "is not", expected) 69 | } 70 | } 71 | 72 | func TestGetSettingsFromFile_WhenFileNotExists(t *testing.T) { 73 | var repository settings.SettingsRepository = &settings.SettingsRepositoryImpl{ 74 | Disk: &lib_mocks.DiskMock{}, 75 | } 76 | expected := settings.Settings{} 77 | settings, err := repository.GetFromFile("migrator.yml") 78 | if err != nil { 79 | t.Fatal("Error should be raised") 80 | } 81 | if settings != expected { 82 | t.Fatal(settings, "is not", expected) 83 | } 84 | } 85 | 86 | func TestCreateFile(t *testing.T) { 87 | disk := &lib_mocks.DiskMock{} 88 | var repository settings.SettingsRepository = &settings.SettingsRepositoryImpl{ 89 | Disk: disk, 90 | } 91 | err := repository.CreateFile("migrator.yml") 92 | if err != nil { 93 | t.Fatal(err) 94 | } 95 | if disk.Creations[0] != "migrator.yml" { 96 | t.Fatal("File was not created") 97 | } 98 | if len(disk.Creations) != 1 { 99 | t.Fatal("Different than 1 files were created") 100 | } 101 | } 102 | 103 | func TestWriteFile(t *testing.T) { 104 | disk := &lib_mocks.DiskMock{} 105 | var repository settings.SettingsRepository = &settings.SettingsRepositoryImpl{ 106 | Disk: disk, 107 | } 108 | err := repository.WriteFile("migrator.yml", "testing content") 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | if disk.Writes[0][0] != "migrator.yml" { 113 | t.Fatal("File was not created") 114 | } 115 | if disk.Writes[0][1] != "testing content" { 116 | t.Fatal("File was not wrote") 117 | } 118 | } 119 | --------------------------------------------------------------------------------