├── .github └── workflows │ ├── ci.yml │ ├── publish.yml │ └── rails_main_testing.yml ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── rails6.1.gemfile ├── rails6.1.gemfile.lock ├── rails7.0.gemfile ├── rails7.0.gemfile.lock ├── rails7.1.gemfile ├── rails7.1.gemfile.lock ├── rails7.2.gemfile ├── rails7.2.gemfile.lock └── rails_main.gemfile ├── lib ├── migration_tools.rb └── migration_tools │ ├── migration_extension.rb │ ├── tasks.rb │ └── version.rb ├── migration_tools.gemspec └── test ├── helper.rb ├── migrations ├── 0_alpha.rb ├── 1_beta.rb ├── 2_delta.rb └── 3_kappa.rb └── test_migration_tools.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | on: push 4 | jobs: 5 | specs: 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | matrix: 10 | ruby-version: 11 | - '3.1' 12 | - '3.2' 13 | gemfile: 14 | - rails6.1 15 | - rails7.0 16 | - rails7.1 17 | - rails7.2 18 | env: 19 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby-version }} 25 | bundler-cache: true 26 | - run: bundle exec rake test 27 | 28 | specs_successful: 29 | name: Specs passing? 30 | needs: specs 31 | if: always() 32 | runs-on: ubuntu-latest 33 | steps: 34 | - run: | 35 | if ${{ needs.specs.result == 'success' }} 36 | then 37 | echo "All specs pass" 38 | else 39 | echo "Some specs failed" 40 | false 41 | fi 42 | 43 | lint: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: ruby/setup-ruby@v1 48 | with: 49 | bundler-cache: true 50 | - run: bundle exec standardrb 51 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to RubyGems.org 2 | 3 | on: 4 | push: 5 | branches: main 6 | paths: lib/migration_tools/version.rb 7 | workflow_dispatch: 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | environment: rubygems-publish 13 | if: github.repository_owner == 'zendesk' 14 | permissions: 15 | id-token: write 16 | contents: write 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | bundler-cache: false 23 | 24 | - name: Install dependencies 25 | run: bundle install 26 | - uses: rubygems/release-gem@v1 27 | -------------------------------------------------------------------------------- /.github/workflows/rails_main_testing.yml: -------------------------------------------------------------------------------- 1 | name: Test against Rails main 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" # Run every day at 00:00 UTC 6 | workflow_dispatch: 7 | push: 8 | 9 | jobs: 10 | specs: 11 | name: Ruby${{ matrix.ruby }} rails_main test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby: 17 | - '3.3' 18 | env: 19 | BUNDLE_GEMFILE: gemfiles/rails_main.gemfile 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - run: bundle exec rake test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eprj 2 | .DS_Store 3 | .rvmrc 4 | .project 5 | .loadpath 6 | .sass-cache 7 | !.rvmrc.example 8 | Thumbs.db 9 | log/**/* 10 | log/* 11 | tmp/**/* 12 | tmp/* 13 | pkg/* 14 | pkg 15 | .idea 16 | .rake_tasks\~ 17 | .cap_tasks\~ 18 | *.tmproj 19 | mkmf.log 20 | .bundle/config 21 | .bundle/environment.rb 22 | /gemfiles/rails_main.gemfile.lock 23 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.4 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | migration_tools (1.10.0) 5 | activerecord (>= 6.1.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (7.0.8) 11 | activesupport (= 7.0.8) 12 | activerecord (7.0.8) 13 | activemodel (= 7.0.8) 14 | activesupport (= 7.0.8) 15 | activesupport (7.0.8) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (>= 1.6, < 2) 18 | minitest (>= 5.1) 19 | tzinfo (~> 2.0) 20 | ast (2.4.2) 21 | bump (0.10.0) 22 | concurrent-ruby (1.2.2) 23 | i18n (1.14.1) 24 | concurrent-ruby (~> 1.0) 25 | json (2.6.3) 26 | language_server-protocol (3.17.0.3) 27 | lint_roller (1.1.0) 28 | mini_portile2 (2.8.5) 29 | minitest (5.20.0) 30 | minitest-rg (5.3.0) 31 | minitest (~> 5.0) 32 | mocha (2.1.0) 33 | ruby2_keywords (>= 0.0.5) 34 | parallel (1.23.0) 35 | parser (3.2.2.4) 36 | ast (~> 2.4.1) 37 | racc 38 | racc (1.7.3) 39 | rainbow (3.1.1) 40 | rake (13.1.0) 41 | regexp_parser (2.8.2) 42 | rexml (3.2.6) 43 | rubocop (1.57.2) 44 | json (~> 2.3) 45 | language_server-protocol (>= 3.17.0) 46 | parallel (~> 1.10) 47 | parser (>= 3.2.2.4) 48 | rainbow (>= 2.2.2, < 4.0) 49 | regexp_parser (>= 1.8, < 3.0) 50 | rexml (>= 3.2.5, < 4.0) 51 | rubocop-ast (>= 1.28.1, < 2.0) 52 | ruby-progressbar (~> 1.7) 53 | unicode-display_width (>= 2.4.0, < 3.0) 54 | rubocop-ast (1.30.0) 55 | parser (>= 3.2.1.0) 56 | rubocop-performance (1.19.1) 57 | rubocop (>= 1.7.0, < 2.0) 58 | rubocop-ast (>= 0.4.0) 59 | ruby-progressbar (1.13.0) 60 | ruby2_keywords (0.0.5) 61 | sqlite3 (1.6.8) 62 | mini_portile2 (~> 2.8.0) 63 | standard (1.32.0) 64 | language_server-protocol (~> 3.17.0.2) 65 | lint_roller (~> 1.0) 66 | rubocop (~> 1.57.2) 67 | standard-custom (~> 1.0.0) 68 | standard-performance (~> 1.2) 69 | standard-custom (1.0.2) 70 | lint_roller (~> 1.0) 71 | rubocop (~> 1.50) 72 | standard-performance (1.2.1) 73 | lint_roller (~> 1.1) 74 | rubocop-performance (~> 1.19.1) 75 | tzinfo (2.0.6) 76 | concurrent-ruby (~> 1.0) 77 | unicode-display_width (2.5.0) 78 | 79 | PLATFORMS 80 | ruby 81 | 82 | DEPENDENCIES 83 | bump 84 | migration_tools! 85 | minitest 86 | minitest-rg 87 | mocha 88 | rake 89 | sqlite3 90 | standard 91 | 92 | BUNDLED WITH 93 | 2.4.22 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migration Tools [![Build Status](https://github.com/zendesk/migration_tools/workflows/CI/badge.svg)](https://github.com/zendesk/migration_tools/actions?query=workflow%3ACI) 2 | 3 | Rake tasks for grouping migrations. 4 | 5 | ## Groups 6 | 7 | The migration tools allow you to specify a group in your migrations. This is used to allow you to run your migrations in groups, as opposed to all at once. This is useful if you want to run a certain group of migrations before a deploy, another group during deploy and a third group after deploy. 8 | 9 | We use this technique to be able to QA new production code in an isolated environment that runs against the production database. It also reduces the number of moving parts come deploy time, which is helpful when you're doing zero downtime deploys. 10 | 11 | You specify which group a migration belongs to inside the migration, like so: 12 | 13 | ```ruby 14 | class CreateHello < ActiveRecord::Migration 15 | group :before 16 | 17 | def self.up 18 | ... 19 | end 20 | end 21 | ``` 22 | 23 | The names of the possible groups are predefined to avoid turning this solution in to a generic hammer from hell. You can use the following groups: before, during, after, change. We define these as: 24 | 25 | *before* this is for migrations that are safe to run before a deploy of new code, e.g. adding columns/tables 26 | 27 | *during* this is for migrations that require the data structure and code to deploy "synchronously" 28 | 29 | *after* this is for migrations that should run after the new code has been pushed and is running 30 | 31 | *change* this is a special group that you run whenever you want to change DB data which you'd otherwise do in script/console 32 | 33 | ## Commands 34 | 35 | The list commands 36 | 37 | ``` 38 | $ rake db:migrate:list - shows pending migrations by group 39 | $ rake db:migrate:list:before - shows pending migrations for the before group 40 | $ rake db:migrate:list:during - shows pending migrations for the during group 41 | $ rake db:migrate:list:after - shows pending migrations for the after group 42 | $ rake db:migrate:list:change - shows pending migrations for the change group 43 | ``` 44 | 45 | The group commands 46 | 47 | ``` 48 | $ GROUP=before rake db:migrate:group - runs the migrations in the specified group 49 | $ rake db:migrate:group:before - runs pending migrations for the before group 50 | $ rake db:migrate:group:during - runs pending migrations for the during group 51 | $ rake db:migrate:group:after - runs pending migrations for the after group 52 | $ rake db:migrate:group:change - runs pending migrations for the change group 53 | ``` 54 | Note that rake db:migrate is entirely unaffected by this. 55 | 56 | ### Releasing a new version 57 | A new version is published to RubyGems.org every time a change to `version.rb` is pushed to the `main` branch. 58 | In short, follow these steps: 59 | 1. Update `version.rb`, 60 | 2. update version in all `Gemfile.lock` files, 61 | 3. merge this change into `main`, and 62 | 4. look at [the action](https://github.com/zendesk/migration_tools/actions/workflows/publish.yml) for output. 63 | 64 | To create a pre-release from a non-main branch: 65 | 1. change the version in `version.rb` to something like `1.2.0.pre.1` or `2.0.0.beta.2`, 66 | 2. push this change to your branch, 67 | 3. go to [Actions → “Publish to RubyGems.org” on GitHub](https://github.com/zendesk/migration_tools/actions/workflows/publish.yml), 68 | 4. click the “Run workflow” button, 69 | 5. pick your branch from a dropdown. 70 | 71 | ## License 72 | 73 | Copyright 2015 Zendesk 74 | 75 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 76 | You may obtain a copy of the License at 77 | 78 | http://www.apache.org/licenses/LICENSE-2.0 79 | 80 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 81 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "bundler/gem_tasks" 3 | require "bump/tasks" 4 | 5 | require "rake/testtask" 6 | Rake::TestTask.new(:test) do |test| 7 | test.libs << "lib" 8 | test.pattern = "test/**/test_*.rb" 9 | test.verbose = true 10 | end 11 | 12 | task default: :test 13 | -------------------------------------------------------------------------------- /gemfiles/rails6.1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord", "~> 6.1.0" 4 | gem "sqlite3", "~> 1.4" 5 | 6 | gemspec path: ".." 7 | -------------------------------------------------------------------------------- /gemfiles/rails6.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | migration_tools (1.10.0) 5 | activerecord (>= 6.1.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (6.1.7.6) 11 | activesupport (= 6.1.7.6) 12 | activerecord (6.1.7.6) 13 | activemodel (= 6.1.7.6) 14 | activesupport (= 6.1.7.6) 15 | activesupport (6.1.7.6) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (>= 1.6, < 2) 18 | minitest (>= 5.1) 19 | tzinfo (~> 2.0) 20 | zeitwerk (~> 2.3) 21 | ast (2.4.2) 22 | bump (0.10.0) 23 | concurrent-ruby (1.2.2) 24 | i18n (1.14.1) 25 | concurrent-ruby (~> 1.0) 26 | json (2.6.3) 27 | language_server-protocol (3.17.0.3) 28 | lint_roller (1.1.0) 29 | mini_portile2 (2.8.5) 30 | minitest (5.20.0) 31 | minitest-rg (5.3.0) 32 | minitest (~> 5.0) 33 | mocha (2.1.0) 34 | ruby2_keywords (>= 0.0.5) 35 | parallel (1.23.0) 36 | parser (3.2.2.4) 37 | ast (~> 2.4.1) 38 | racc 39 | racc (1.7.3) 40 | rainbow (3.1.1) 41 | rake (13.1.0) 42 | regexp_parser (2.8.2) 43 | rexml (3.2.6) 44 | rubocop (1.57.2) 45 | json (~> 2.3) 46 | language_server-protocol (>= 3.17.0) 47 | parallel (~> 1.10) 48 | parser (>= 3.2.2.4) 49 | rainbow (>= 2.2.2, < 4.0) 50 | regexp_parser (>= 1.8, < 3.0) 51 | rexml (>= 3.2.5, < 4.0) 52 | rubocop-ast (>= 1.28.1, < 2.0) 53 | ruby-progressbar (~> 1.7) 54 | unicode-display_width (>= 2.4.0, < 3.0) 55 | rubocop-ast (1.30.0) 56 | parser (>= 3.2.1.0) 57 | rubocop-performance (1.19.1) 58 | rubocop (>= 1.7.0, < 2.0) 59 | rubocop-ast (>= 0.4.0) 60 | ruby-progressbar (1.13.0) 61 | ruby2_keywords (0.0.5) 62 | sqlite3 (1.6.8) 63 | mini_portile2 (~> 2.8.0) 64 | standard (1.32.0) 65 | language_server-protocol (~> 3.17.0.2) 66 | lint_roller (~> 1.0) 67 | rubocop (~> 1.57.2) 68 | standard-custom (~> 1.0.0) 69 | standard-performance (~> 1.2) 70 | standard-custom (1.0.2) 71 | lint_roller (~> 1.0) 72 | rubocop (~> 1.50) 73 | standard-performance (1.2.1) 74 | lint_roller (~> 1.1) 75 | rubocop-performance (~> 1.19.1) 76 | tzinfo (2.0.6) 77 | concurrent-ruby (~> 1.0) 78 | unicode-display_width (2.5.0) 79 | zeitwerk (2.6.12) 80 | 81 | PLATFORMS 82 | ruby 83 | 84 | DEPENDENCIES 85 | activerecord (~> 6.1.0) 86 | bump 87 | migration_tools! 88 | minitest 89 | minitest-rg 90 | mocha 91 | rake 92 | sqlite3 (~> 1.4) 93 | standard 94 | 95 | BUNDLED WITH 96 | 2.4.22 97 | -------------------------------------------------------------------------------- /gemfiles/rails7.0.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord", "~> 7.0.0" 4 | gem "sqlite3", "~> 1.4" 5 | 6 | gemspec path: ".." 7 | -------------------------------------------------------------------------------- /gemfiles/rails7.0.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | migration_tools (1.10.0) 5 | activerecord (>= 6.1.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (7.0.8) 11 | activesupport (= 7.0.8) 12 | activerecord (7.0.8) 13 | activemodel (= 7.0.8) 14 | activesupport (= 7.0.8) 15 | activesupport (7.0.8) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (>= 1.6, < 2) 18 | minitest (>= 5.1) 19 | tzinfo (~> 2.0) 20 | ast (2.4.2) 21 | bump (0.10.0) 22 | concurrent-ruby (1.2.2) 23 | i18n (1.14.1) 24 | concurrent-ruby (~> 1.0) 25 | json (2.6.3) 26 | language_server-protocol (3.17.0.3) 27 | lint_roller (1.1.0) 28 | mini_portile2 (2.8.5) 29 | minitest (5.20.0) 30 | minitest-rg (5.3.0) 31 | minitest (~> 5.0) 32 | mocha (2.1.0) 33 | ruby2_keywords (>= 0.0.5) 34 | parallel (1.23.0) 35 | parser (3.2.2.4) 36 | ast (~> 2.4.1) 37 | racc 38 | racc (1.7.3) 39 | rainbow (3.1.1) 40 | rake (13.1.0) 41 | regexp_parser (2.8.2) 42 | rexml (3.2.6) 43 | rubocop (1.57.2) 44 | json (~> 2.3) 45 | language_server-protocol (>= 3.17.0) 46 | parallel (~> 1.10) 47 | parser (>= 3.2.2.4) 48 | rainbow (>= 2.2.2, < 4.0) 49 | regexp_parser (>= 1.8, < 3.0) 50 | rexml (>= 3.2.5, < 4.0) 51 | rubocop-ast (>= 1.28.1, < 2.0) 52 | ruby-progressbar (~> 1.7) 53 | unicode-display_width (>= 2.4.0, < 3.0) 54 | rubocop-ast (1.30.0) 55 | parser (>= 3.2.1.0) 56 | rubocop-performance (1.19.1) 57 | rubocop (>= 1.7.0, < 2.0) 58 | rubocop-ast (>= 0.4.0) 59 | ruby-progressbar (1.13.0) 60 | ruby2_keywords (0.0.5) 61 | sqlite3 (1.6.8) 62 | mini_portile2 (~> 2.8.0) 63 | standard (1.32.0) 64 | language_server-protocol (~> 3.17.0.2) 65 | lint_roller (~> 1.0) 66 | rubocop (~> 1.57.2) 67 | standard-custom (~> 1.0.0) 68 | standard-performance (~> 1.2) 69 | standard-custom (1.0.2) 70 | lint_roller (~> 1.0) 71 | rubocop (~> 1.50) 72 | standard-performance (1.2.1) 73 | lint_roller (~> 1.1) 74 | rubocop-performance (~> 1.19.1) 75 | tzinfo (2.0.6) 76 | concurrent-ruby (~> 1.0) 77 | unicode-display_width (2.5.0) 78 | 79 | PLATFORMS 80 | ruby 81 | 82 | DEPENDENCIES 83 | activerecord (~> 7.0.0) 84 | bump 85 | migration_tools! 86 | minitest 87 | minitest-rg 88 | mocha 89 | rake 90 | sqlite3 (~> 1.4) 91 | standard 92 | 93 | BUNDLED WITH 94 | 2.4.22 95 | -------------------------------------------------------------------------------- /gemfiles/rails7.1.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord", "~> 7.1.0" 4 | gem "sqlite3", "~> 1.4" 5 | 6 | gemspec path: ".." 7 | -------------------------------------------------------------------------------- /gemfiles/rails7.1.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | migration_tools (1.10.0) 5 | activerecord (>= 6.1.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (7.1.2) 11 | activesupport (= 7.1.2) 12 | activerecord (7.1.2) 13 | activemodel (= 7.1.2) 14 | activesupport (= 7.1.2) 15 | timeout (>= 0.4.0) 16 | activesupport (7.1.2) 17 | base64 18 | bigdecimal 19 | concurrent-ruby (~> 1.0, >= 1.0.2) 20 | connection_pool (>= 2.2.5) 21 | drb 22 | i18n (>= 1.6, < 2) 23 | minitest (>= 5.1) 24 | mutex_m 25 | tzinfo (~> 2.0) 26 | ast (2.4.2) 27 | base64 (0.2.0) 28 | bigdecimal (3.1.4) 29 | bump (0.10.0) 30 | concurrent-ruby (1.2.2) 31 | connection_pool (2.4.1) 32 | drb (2.2.0) 33 | ruby2_keywords 34 | i18n (1.14.1) 35 | concurrent-ruby (~> 1.0) 36 | json (2.6.3) 37 | language_server-protocol (3.17.0.3) 38 | lint_roller (1.1.0) 39 | mini_portile2 (2.8.5) 40 | minitest (5.20.0) 41 | minitest-rg (5.3.0) 42 | minitest (~> 5.0) 43 | mocha (2.1.0) 44 | ruby2_keywords (>= 0.0.5) 45 | mutex_m (0.2.0) 46 | parallel (1.23.0) 47 | parser (3.2.2.4) 48 | ast (~> 2.4.1) 49 | racc 50 | racc (1.7.3) 51 | rainbow (3.1.1) 52 | rake (13.1.0) 53 | regexp_parser (2.8.2) 54 | rexml (3.2.6) 55 | rubocop (1.57.2) 56 | json (~> 2.3) 57 | language_server-protocol (>= 3.17.0) 58 | parallel (~> 1.10) 59 | parser (>= 3.2.2.4) 60 | rainbow (>= 2.2.2, < 4.0) 61 | regexp_parser (>= 1.8, < 3.0) 62 | rexml (>= 3.2.5, < 4.0) 63 | rubocop-ast (>= 1.28.1, < 2.0) 64 | ruby-progressbar (~> 1.7) 65 | unicode-display_width (>= 2.4.0, < 3.0) 66 | rubocop-ast (1.30.0) 67 | parser (>= 3.2.1.0) 68 | rubocop-performance (1.19.1) 69 | rubocop (>= 1.7.0, < 2.0) 70 | rubocop-ast (>= 0.4.0) 71 | ruby-progressbar (1.13.0) 72 | ruby2_keywords (0.0.5) 73 | sqlite3 (1.6.8) 74 | mini_portile2 (~> 2.8.0) 75 | standard (1.32.0) 76 | language_server-protocol (~> 3.17.0.2) 77 | lint_roller (~> 1.0) 78 | rubocop (~> 1.57.2) 79 | standard-custom (~> 1.0.0) 80 | standard-performance (~> 1.2) 81 | standard-custom (1.0.2) 82 | lint_roller (~> 1.0) 83 | rubocop (~> 1.50) 84 | standard-performance (1.2.1) 85 | lint_roller (~> 1.1) 86 | rubocop-performance (~> 1.19.1) 87 | timeout (0.4.1) 88 | tzinfo (2.0.6) 89 | concurrent-ruby (~> 1.0) 90 | unicode-display_width (2.5.0) 91 | 92 | PLATFORMS 93 | ruby 94 | 95 | DEPENDENCIES 96 | activerecord (~> 7.1.0) 97 | bump 98 | migration_tools! 99 | minitest 100 | minitest-rg 101 | mocha 102 | rake 103 | sqlite3 (~> 1.4) 104 | standard 105 | 106 | BUNDLED WITH 107 | 2.4.22 108 | -------------------------------------------------------------------------------- /gemfiles/rails7.2.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord", "~> 7.2.0" 4 | gem "sqlite3", "~> 1.4" 5 | 6 | gemspec path: ".." 7 | -------------------------------------------------------------------------------- /gemfiles/rails7.2.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | migration_tools (1.10.0) 5 | activerecord (>= 6.1.0) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (7.2.0) 11 | activesupport (= 7.2.0) 12 | activerecord (7.2.0) 13 | activemodel (= 7.2.0) 14 | activesupport (= 7.2.0) 15 | timeout (>= 0.4.0) 16 | activesupport (7.2.0) 17 | base64 18 | bigdecimal 19 | concurrent-ruby (~> 1.0, >= 1.3.1) 20 | connection_pool (>= 2.2.5) 21 | drb 22 | i18n (>= 1.6, < 2) 23 | logger (>= 1.4.2) 24 | minitest (>= 5.1) 25 | securerandom (>= 0.3) 26 | tzinfo (~> 2.0, >= 2.0.5) 27 | ast (2.4.2) 28 | base64 (0.2.0) 29 | bigdecimal (3.1.8) 30 | bump (0.10.0) 31 | concurrent-ruby (1.3.4) 32 | connection_pool (2.4.1) 33 | drb (2.2.1) 34 | i18n (1.14.5) 35 | concurrent-ruby (~> 1.0) 36 | json (2.7.2) 37 | language_server-protocol (3.17.0.3) 38 | lint_roller (1.1.0) 39 | logger (1.6.0) 40 | minitest (5.25.1) 41 | minitest-rg (5.3.0) 42 | minitest (~> 5.0) 43 | mocha (2.4.5) 44 | ruby2_keywords (>= 0.0.5) 45 | parallel (1.26.3) 46 | parser (3.3.4.2) 47 | ast (~> 2.4.1) 48 | racc 49 | racc (1.8.1) 50 | rainbow (3.1.1) 51 | rake (13.2.1) 52 | regexp_parser (2.9.2) 53 | rexml (3.3.5) 54 | strscan 55 | rubocop (1.65.1) 56 | json (~> 2.3) 57 | language_server-protocol (>= 3.17.0) 58 | parallel (~> 1.10) 59 | parser (>= 3.3.0.2) 60 | rainbow (>= 2.2.2, < 4.0) 61 | regexp_parser (>= 2.4, < 3.0) 62 | rexml (>= 3.2.5, < 4.0) 63 | rubocop-ast (>= 1.31.1, < 2.0) 64 | ruby-progressbar (~> 1.7) 65 | unicode-display_width (>= 2.4.0, < 3.0) 66 | rubocop-ast (1.32.1) 67 | parser (>= 3.3.1.0) 68 | rubocop-performance (1.21.1) 69 | rubocop (>= 1.48.1, < 2.0) 70 | rubocop-ast (>= 1.31.1, < 2.0) 71 | ruby-progressbar (1.13.0) 72 | ruby2_keywords (0.0.5) 73 | securerandom (0.3.1) 74 | sqlite3 (1.7.3) 75 | standard (1.40.0) 76 | language_server-protocol (~> 3.17.0.2) 77 | lint_roller (~> 1.0) 78 | rubocop (~> 1.65.0) 79 | standard-custom (~> 1.0.0) 80 | standard-performance (~> 1.4) 81 | standard-custom (1.0.2) 82 | lint_roller (~> 1.0) 83 | rubocop (~> 1.50) 84 | standard-performance (1.4.0) 85 | lint_roller (~> 1.1) 86 | rubocop-performance (~> 1.21.0) 87 | strscan (3.1.0) 88 | timeout (0.4.1) 89 | tzinfo (2.0.6) 90 | concurrent-ruby (~> 1.0) 91 | unicode-display_width (2.5.0) 92 | 93 | PLATFORMS 94 | ruby 95 | 96 | DEPENDENCIES 97 | activerecord (~> 7.2.0) 98 | bump 99 | migration_tools! 100 | minitest 101 | minitest-rg 102 | mocha 103 | rake 104 | sqlite3 (~> 1.4) 105 | standard 106 | 107 | BUNDLED WITH 108 | 2.5.11 109 | -------------------------------------------------------------------------------- /gemfiles/rails_main.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "activerecord", github: "rails/rails", branch: "main" 4 | gem "sqlite3", "~> 2.0" 5 | 6 | gemspec path: ".." 7 | -------------------------------------------------------------------------------- /lib/migration_tools.rb: -------------------------------------------------------------------------------- 1 | require "benchmark" 2 | require "active_record" 3 | require "active_record/migration" 4 | require "active_support/core_ext/object/blank" 5 | 6 | require "migration_tools/migration_extension" 7 | require "migration_tools/tasks" 8 | 9 | module MigrationTools 10 | def self.forced? 11 | defined?(@forced) && !!@forced 12 | end 13 | 14 | def self.forced! 15 | @forced = true 16 | end 17 | 18 | MIGRATION_GROUPS = ["before", "during", "after", "change"] 19 | end 20 | -------------------------------------------------------------------------------- /lib/migration_tools/migration_extension.rb: -------------------------------------------------------------------------------- 1 | module MigrationTools 2 | module MigrationExtension 3 | attr_accessor :migration_group 4 | 5 | def group(arg = nil) 6 | unless MigrationTools::MIGRATION_GROUPS.member?(arg.to_s) 7 | raise "Invalid group \"#{arg}\" - valid groups are #{MigrationTools::MIGRATION_GROUPS.inspect}" 8 | end 9 | 10 | self.migration_group = arg.to_s 11 | end 12 | 13 | def migrate_with_forced_groups(direction) 14 | if MigrationTools.forced? && migration_group.blank? 15 | raise "Cowardly refusing to run migration without a group. Read https://github.com/zendesk/migration_tools/blob/master/README.md" 16 | end 17 | migrate_without_forced_groups(direction) 18 | end 19 | end 20 | end 21 | 22 | ActiveRecord::Migration.class_eval do 23 | extend MigrationTools::MigrationExtension 24 | class << self 25 | alias_method :migrate_without_forced_groups, :migrate 26 | alias_method :migrate, :migrate_with_forced_groups 27 | end 28 | 29 | def migration_group 30 | self.class.migration_group 31 | end 32 | end 33 | ActiveRecord::MigrationProxy.delegate :migration_group, to: :migration 34 | -------------------------------------------------------------------------------- /lib/migration_tools/tasks.rb: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rake/tasklib" 3 | 4 | module MigrationTools 5 | class Tasks < ::Rake::TaskLib 6 | def initialize 7 | define_migrate_list 8 | define_migrate_group 9 | define_convenience_tasks 10 | end 11 | 12 | def group 13 | return @group if defined?(@group) && @group 14 | 15 | @group = ENV["GROUP"].to_s 16 | raise "Invalid group \"#{@group}\"" if !@group.empty? && !MIGRATION_GROUPS.member?(@group) 17 | @group 18 | end 19 | 20 | def group=(group) 21 | @group = nil 22 | @pending_migrations = nil 23 | ENV["GROUP"] = group 24 | end 25 | 26 | def migrations_paths 27 | ActiveRecord::Migrator.migrations_paths 28 | end 29 | 30 | def migrator(target_version = nil) 31 | if ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR == 1 32 | migrate_up(ActiveRecord::MigrationContext.new( 33 | migrations_paths, 34 | ActiveRecord::Base.connection.schema_migration 35 | ).migrations, target_version) 36 | elsif ActiveRecord.gem_version >= Gem::Version.new("7.2") 37 | migrate_up(ActiveRecord::MigrationContext.new( 38 | migrations_paths, 39 | ActiveRecord::Base.connection_pool.schema_migration 40 | ).migrations, target_version) 41 | else 42 | migrate_up(ActiveRecord::MigrationContext.new( 43 | migrations_paths, 44 | ActiveRecord::SchemaMigration 45 | ).migrations, target_version) 46 | end 47 | end 48 | 49 | def migrate_up(migrations, target_version) 50 | if ActiveRecord::VERSION::MAJOR == 7 && ActiveRecord::VERSION::MINOR == 1 51 | ActiveRecord::Migrator.new(:up, migrations, 52 | ActiveRecord::Base.connection.schema_migration, 53 | ActiveRecord::Base.connection.internal_metadata, 54 | target_version) 55 | elsif ActiveRecord.gem_version >= Gem::Version.new("7.2") 56 | ActiveRecord::Migrator.new(:up, migrations, 57 | ActiveRecord::Base.connection_pool.schema_migration, 58 | ActiveRecord::Base.connection_pool.internal_metadata, 59 | target_version) 60 | else 61 | ActiveRecord::Migrator.new(:up, migrations, ActiveRecord::SchemaMigration, target_version) 62 | end 63 | end 64 | 65 | def pending_migrations 66 | return @pending_migrations if defined?(@pending_migrations) && @pending_migrations 67 | @pending_migrations = migrator.pending_migrations 68 | @pending_migrations = @pending_migrations.select { |proxy| group.empty? || proxy.migration_group == group } 69 | 70 | @pending_migrations 71 | end 72 | 73 | def define_migrate_list 74 | namespace :db do 75 | namespace :migrate do 76 | desc "Lists pending migrations" 77 | task list: :environment do 78 | if pending_migrations.empty? 79 | notify "Your database schema is up to date", group 80 | else 81 | notify "You have #{pending_migrations.size} pending migrations", group 82 | pending_migrations.each do |migration| 83 | notify " %4d %s %s" % [migration.version, migration.migration_group.to_s[0..5].center(6), migration.name] 84 | end 85 | end 86 | end 87 | end 88 | end 89 | end 90 | 91 | def define_migrate_group 92 | namespace :db do 93 | namespace :migrate do 94 | desc "Runs pending migrations for a given group" 95 | task group: :environment do 96 | if group.empty? 97 | notify "Please specify a migration group" 98 | elsif pending_migrations.empty? 99 | notify "Your database schema is up to date" 100 | else 101 | pending_migrations.each do |migration| 102 | migrator(migration.version).run 103 | end 104 | 105 | schema_format = if ActiveRecord::VERSION::MAJOR >= 7 106 | ActiveRecord.schema_format 107 | else 108 | ActiveRecord::Base.schema_format 109 | end 110 | 111 | Rake::Task["db:schema:dump"].invoke if schema_format == :ruby 112 | Rake::Task["db:structure:dump"].invoke if schema_format == :sql 113 | end 114 | end 115 | end 116 | end 117 | end 118 | 119 | def define_convenience_tasks 120 | namespace :db do 121 | namespace :migrate do 122 | [:list, :group].each do |ns| 123 | namespace ns do 124 | MigrationTools::MIGRATION_GROUPS.each do |migration_group| 125 | desc "#{(ns == :list) ? "Lists" : "Executes"} the migrations for group #{migration_group}" 126 | task migration_group => :environment do 127 | self.group = migration_group.to_s 128 | Rake::Task["db:migrate:#{ns}"].invoke 129 | Rake::Task["db:migrate:#{ns}"].reenable 130 | end 131 | end 132 | end 133 | end 134 | end 135 | 136 | namespace :abort_if_pending_migrations do 137 | MigrationTools::MIGRATION_GROUPS.each do |migration_group| 138 | desc "Raises an error if there are pending #{migration_group} migrations" 139 | task migration_group do 140 | self.group = migration_group.to_s 141 | Rake::Task["db:migrate:list"].invoke 142 | Rake::Task["db:migrate:list"].reenable 143 | if pending_migrations.any? 144 | abort "Run \"rake db:migrate\" to update your database then try again." 145 | end 146 | end 147 | end 148 | end 149 | end 150 | end 151 | 152 | def notify(string, group = "") 153 | if group.empty? 154 | puts string 155 | else 156 | puts "#{string} for group \"#{group}\"" 157 | end 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/migration_tools/version.rb: -------------------------------------------------------------------------------- 1 | module MigrationTools 2 | VERSION = "1.10.0" 3 | end 4 | -------------------------------------------------------------------------------- /migration_tools.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/migration_tools/version" 2 | 3 | Gem::Specification.new "migration_tools", MigrationTools::VERSION do |s| 4 | s.description = "Rake tasks for Rails that add groups to migrations" 5 | s.summary = "Encourage migrations that do not require downtime" 6 | s.homepage = "https://github.com/zendesk/migration_tools" 7 | s.email = "morten@zendesk.com" 8 | s.authors = ["Morten Primdahl"] 9 | s.files = `git ls-files lib`.split("\n") 10 | s.license = "Apache-2.0" 11 | 12 | s.required_ruby_version = ">= 3.1.0" 13 | 14 | s.add_runtime_dependency "activerecord", ">= 6.1.0" 15 | 16 | s.add_development_dependency "rake" 17 | s.add_development_dependency "bump" 18 | s.add_development_dependency "mocha" 19 | s.add_development_dependency "minitest" 20 | s.add_development_dependency "minitest-rg" 21 | s.add_development_dependency "sqlite3" 22 | s.add_development_dependency "standard" 23 | end 24 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "minitest/autorun" 3 | require "minitest/spec" 4 | require "minitest/rg" 5 | require "mocha/minitest" 6 | require "active_support/all" 7 | require "migration_tools" 8 | 9 | rails_version = "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}".to_f 10 | MIGRATION_CLASS = ActiveRecord::Migration[rails_version] 11 | 12 | dir = File.expand_path("../migrations", __FILE__) 13 | ActiveRecord::Migrator.migrations_paths.replace([dir]) 14 | Dir.glob(File.join(dir, "*.rb")).sort.each { |f| require f } 15 | 16 | ActiveRecord::Migration.verbose = false 17 | -------------------------------------------------------------------------------- /test/migrations/0_alpha.rb: -------------------------------------------------------------------------------- 1 | class Alpha < MIGRATION_CLASS 2 | group :before 3 | 4 | def self.up 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/migrations/1_beta.rb: -------------------------------------------------------------------------------- 1 | class Beta < MIGRATION_CLASS 2 | group :before 3 | 4 | def self.up 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/migrations/2_delta.rb: -------------------------------------------------------------------------------- 1 | class Delta < MIGRATION_CLASS 2 | group :change 3 | 4 | def self.up 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/migrations/3_kappa.rb: -------------------------------------------------------------------------------- 1 | class Kappa < MIGRATION_CLASS 2 | def self.up 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/test_migration_tools.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path "../helper", __FILE__ 2 | 3 | describe MigrationTools do 4 | before do 5 | ENV["GROUP"] = nil 6 | 7 | ActiveRecord::Base.establish_connection( 8 | adapter: "sqlite3", 9 | database: ":memory:" 10 | ) 11 | 12 | Rake::Task.clear 13 | Rake::Task.define_task("environment") 14 | Rake::Task.define_task("db:schema:dump") 15 | 16 | @task = MigrationTools::Tasks.new 17 | end 18 | 19 | def migrations 20 | [Alpha, Beta, Delta, Kappa] 21 | end 22 | 23 | def proxies 24 | @proxies ||= migrations.map { |m| migration_proxy(m) } 25 | end 26 | 27 | def migration_proxy(m) 28 | name = m.name 29 | version = migrations.index(m) 30 | 31 | proxy = ActiveRecord::MigrationProxy.new(name, version, nil, nil) 32 | proxy.instance_variable_set(:@migration, m.new) 33 | proxy 34 | end 35 | 36 | it "grouping" do 37 | assert_equal [Alpha, Beta], migrations.select { |m| m.migration_group == "before" } 38 | assert_equal [Delta], migrations.select { |m| m.migration_group == "change" } 39 | assert_equal [Kappa], migrations.select { |m| m.migration_group.nil? } 40 | end 41 | 42 | it "runtime_checking" do 43 | eval("class Kappa < MIGRATION_CLASS; group 'drunk'; end", binding, __FILE__, __LINE__) 44 | fail "You should not be able to specify custom groups" 45 | rescue RuntimeError => e 46 | assert e.message.index('Invalid group "drunk" - valid groups are ["before", "during", "after", "change"]') 47 | end 48 | 49 | it "migration_proxy_delegation" do 50 | proxy = ActiveRecord::MigrationProxy.new(:name, :version, :filename, :scope) 51 | proxy.expects(:migration).returns(Delta) 52 | assert_equal "change", proxy.migration_group 53 | end 54 | 55 | it "forcing" do 56 | assert !MigrationTools.forced? 57 | Kappa.migrate("up") 58 | 59 | MigrationTools.forced! 60 | assert MigrationTools.forced? 61 | 62 | Alpha.migrate("up") 63 | 64 | begin 65 | Kappa.migrate("up") 66 | fail "You should not be able to run migrations without groups in forced mode" 67 | rescue RuntimeError => e 68 | assert e.message =~ /Cowardly refusing/ 69 | end 70 | end 71 | 72 | it "task_presence" do 73 | assert Rake::Task["db:migrate:list"] 74 | assert Rake::Task["db:migrate:group"] 75 | assert Rake::Task["db:migrate:group:before"] 76 | assert Rake::Task["db:migrate:group:during"] 77 | assert Rake::Task["db:migrate:group:after"] 78 | assert Rake::Task["db:migrate:group:change"] 79 | end 80 | 81 | it "migrate_list_without_pending_without_group" do 82 | 0.upto(3).each { |i| @task.migrator(i).run } 83 | 84 | @task.expects(:notify).with("Your database schema is up to date", "").once 85 | 86 | Rake::Task["db:migrate:list"].invoke 87 | end 88 | 89 | it "migrate_list_without_pending_with_group" do 90 | @task.migrator(0).run 91 | @task.migrator(1).run 92 | 93 | @task.expects(:notify).with("Your database schema is up to date", "before").once 94 | 95 | ENV["GROUP"] = "before" 96 | Rake::Task["db:migrate:list"].invoke 97 | end 98 | 99 | it "migrate_list_with_pending_without_group" do 100 | @task.expects(:notify).with("You have 4 pending migrations", "").once 101 | @task.expects(:notify).with(" 0 before Alpha").once 102 | @task.expects(:notify).with(" 1 before Beta").once 103 | @task.expects(:notify).with(" 2 change Delta").once 104 | @task.expects(:notify).with(" 3 Kappa").once 105 | 106 | Rake::Task["db:migrate:list"].invoke 107 | end 108 | 109 | it "migrate_list_with_pending_with_group" do 110 | ENV["GROUP"] = "before" 111 | 112 | @task.expects(:notify).with("You have 2 pending migrations", "before").once 113 | @task.expects(:notify).with(" 0 before Alpha").once 114 | @task.expects(:notify).with(" 1 before Beta").once 115 | 116 | Rake::Task["db:migrate:list"].invoke 117 | end 118 | 119 | it "abort_if_pending_migrations_with_group_without_migrations" do 120 | @task.stubs(:notify) 121 | 122 | begin 123 | Rake::Task["db:abort_if_pending_migrations:after"].invoke 124 | rescue SystemExit 125 | fail "aborted where it shouldn't" 126 | end 127 | end 128 | 129 | require "active_support/testing/stream" 130 | include ActiveSupport::Testing::Stream 131 | 132 | it "abort_if_pending_migrations_with_group_with_migrations" do 133 | assert_raises(SystemExit, "did not abort") do 134 | silence_stream($stdout) do 135 | silence_stream($stderr) do 136 | Rake::Task["db:abort_if_pending_migrations:before"].invoke 137 | end 138 | end 139 | end 140 | end 141 | 142 | it "migrate_group_with_group_without_pending" do 143 | @task.migrator(0).run 144 | @task.migrator(1).run 145 | 146 | @task.expects(:notify).with("Your database schema is up to date").once 147 | 148 | ENV["GROUP"] = "before" 149 | Rake::Task["db:migrate:group"].invoke 150 | end 151 | 152 | it "migrate_group_with_pending" do 153 | ENV["GROUP"] = "before" 154 | 155 | assert_equal 4, @task.migrator.pending_migrations.count 156 | 157 | Rake::Task["db:migrate:group"].invoke 158 | 159 | assert_equal 2, @task.migrator.pending_migrations.count 160 | end 161 | 162 | it "migrate_with_invalid_group" do 163 | ENV["GROUP"] = "drunk" 164 | 165 | begin 166 | Rake::Task["db:migrate:group"].invoke 167 | fail "Should throw an error" 168 | rescue RuntimeError => e 169 | assert e.message =~ /Invalid group/ 170 | end 171 | end 172 | 173 | it "convenience_list_method" do 174 | @task.expects(:notify).with("You have 2 pending migrations", "before").once 175 | @task.expects(:notify).with(" 0 before Alpha").once 176 | @task.expects(:notify).with(" 1 before Beta").once 177 | 178 | Rake::Task["db:migrate:list:before"].invoke 179 | end 180 | end 181 | --------------------------------------------------------------------------------