zero_downtime_migrations 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5c8dfec1a4d2d84e2a9260829418235053eeec0c
4
- data.tar.gz: 0568a51dd7990eb896354ed67cfe984a3bcc8f13
3
+ metadata.gz: c98c1cbcf271026706fc6d40043d1fcba981ef04
4
+ data.tar.gz: aa219a884d42a7024256b9763f08decbfb6950d4
5
5
  SHA512:
6
- metadata.gz: 1b0b3503a2749957e5f310bd5c38bcfa3243a3fd2d077f793ca17fff631c384a65f6dc75d247588f52912bb43ee36d2bbbd6d173f33912f34abc357f9e0aa4f6
7
- data.tar.gz: 1d2d7bc71333d19ba28b1c6600a99d23cf2094078f9f45dd9df826ce9306b0284b9e9236eae920b3b1224d33cddeb7f8ce020c81567d77448df1d36e62966b3c
6
+ metadata.gz: 862eaac1afae554f4727fad033c8f1ac023973c3dc2d849535eb715fb2a18ec87c770aca6a591d604417d18fd23c9116f8c02684cf1914ee5d28d3df30950bd0
7
+ data.tar.gz: 30bbf4dd92155ae61b48721d8088f9cee339ba502634b775f6169cc5e4b7405d3109261da0d93bd127b1cb5fb72d77628e85e51b7db5a3be6b5e7debb389705b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- zero_downtime_migrations (0.0.1)
4
+ zero_downtime_migrations (0.0.2)
5
5
  activerecord
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  # ![LendingHome](https://cloud.githubusercontent.com/assets/2419/19467866/7efa93a8-94c8-11e6-93e7-4375dbb8a7bc.png) zero_downtime_migrations
2
+ [![Code Climate](https://codeclimate.com/github/LendingHome/zero_downtime_migrations/badges/gpa.svg)](https://codeclimate.com/github/LendingHome/zero_downtime_migrations) [![Coverage](https://codeclimate.com/github/LendingHome/zero_downtime_migrations/badges/coverage.svg)](https://codeclimate.com/github/LendingHome/zero_downtime_migrations) [![Gem Version](https://badge.fury.io/rb/zero_downtime_migrations.svg)](http://badge.fury.io/rb/zero_downtime_migrations)
2
3
 
3
- Zero downtime migrations with ActiveRecord and PostgreSQL. Catch problematic migrations at development/test time!
4
+ > Zero downtime migrations with ActiveRecord and PostgreSQL.
4
5
 
5
- Heavily inspired by these similar projects:
6
+ Catch problematic migrations at development/test time! Heavily inspired by these similar projects:
6
7
 
7
8
  * https://github.com/ankane/strong_migrations
8
9
  * https://github.com/foobarfighter/safe-migrations
9
10
 
10
11
  ## Installation
11
12
 
12
- Simply add the gem to the project `Gemfile`. Ensure that it's only **added to the `development` and `test`** groups.
13
+ Simply add this gem to the project `Gemfile` under the **`development` and `test`** groups.
13
14
 
14
15
  ```ruby
15
16
  gem "zero_downtime_migrations", only: %i(development test)
@@ -27,45 +28,7 @@ It checks for common things like:
27
28
  * Performing data or schema migrations with the DDL transaction disabled
28
29
  * Using `each` instead of `find_each` to loop thru `ActiveRecord` objects
29
30
 
30
- These exceptions display very clear instructions of how to perform the same operation the "zero downtime way".
31
-
32
- ## Disabling exceptions
33
-
34
- We can disable any of these "zero downtime migration" enforcements by wrapping them in a `safety_assured` block.
35
-
36
- ```ruby
37
- class AddPublishedToPosts < ActiveRecord::Migration[5.0]
38
- def change
39
- safety_assured do
40
- add_column :posts, :published, :boolean, default: true
41
- end
42
- end
43
- end
44
- ```
45
-
46
- We can also mark an entire migration as safe by using the `safety_assured` helper method.
47
-
48
- ```ruby
49
- class AddPublishedToPosts < ActiveRecord::Migration[5.0]
50
- safety_assured
51
-
52
- def change
53
- add_column :posts, :published, :boolean
54
- Post.where("created_at >= ?", 1.day.ago).update_all(published: true)
55
- end
56
- end
57
- ```
58
-
59
- Enforcements can be globally disabled by setting `ENV["SAFETY_ASSURED"]` when running migrations.
60
-
61
- ```bash
62
- SAFETY_ASSURED=1 bundle exec rake db:migrate --trace
63
- ```
64
-
65
- These enforcements are **automatically disabled by default for the following scenarios**:
66
-
67
- * The database schema is being loaded with `rake db:schema:load` instead of `db:migrate`
68
- * The current migration is a reverse (down) migration
31
+ These exceptions display clear instructions of how to perform the same operation the "zero downtime way".
69
32
 
70
33
  ## Validations
71
34
 
@@ -73,6 +36,8 @@ These enforcements are **automatically disabled by default for the following sce
73
36
 
74
37
  #### Bad
75
38
 
39
+ This migration can potentially lock your database table!
40
+
76
41
  ```ruby
77
42
  class AddPublishedToPosts < ActiveRecord::Migration[5.0]
78
43
  def change
@@ -83,6 +48,8 @@ end
83
48
 
84
49
  #### Good
85
50
 
51
+ Instead, let's first add the column without a default.
52
+
86
53
  ```ruby
87
54
  class AddPublishedToPosts < ActiveRecord::Migration[5.0]
88
55
  def change
@@ -91,6 +58,8 @@ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
91
58
  end
92
59
  ```
93
60
 
61
+ Then set the new column default in a separate migration. Note that this does not update any existing data.
62
+
94
63
  ```ruby
95
64
  class SetPublishedDefaultOnPosts < ActiveRecord::Migration[5.0]
96
65
  def change
@@ -99,6 +68,8 @@ class SetPublishedDefaultOnPosts < ActiveRecord::Migration[5.0]
99
68
  end
100
69
  ```
101
70
 
71
+ If necessary then backport the default value for existing data in batches. This should be done in its own migration as well.
72
+
102
73
  ```ruby
103
74
  class BackportPublishedDefaultOnPosts < ActiveRecord::Migration[5.0]
104
75
  def change
@@ -114,6 +85,8 @@ end
114
85
 
115
86
  #### Bad
116
87
 
88
+ This action can potentially lock your database table while indexing all existing data!
89
+
117
90
  ```ruby
118
91
  class IndexUsersOnEmail < ActiveRecord::Migration[5.0]
119
92
  def change
@@ -124,6 +97,8 @@ end
124
97
 
125
98
  #### Good
126
99
 
100
+ Instead, let's add the index concurrently in its own migration with the DDL transaction disabled.
101
+
127
102
  ```ruby
128
103
  class IndexUsersOnEmail < ActiveRecord::Migration[5.0]
129
104
  disable_ddl_transaction!
@@ -138,6 +113,8 @@ end
138
113
 
139
114
  #### Bad
140
115
 
116
+ Performing migrations that change the schema, update data, or add indexes within one big transaction is unsafe!
117
+
141
118
  ```ruby
142
119
  class AddPublishedToPosts < ActiveRecord::Migration[5.0]
143
120
  def change
@@ -150,6 +127,12 @@ end
150
127
 
151
128
  #### Good
152
129
 
130
+ Instead, let's split apart these types of migrations into separate files.
131
+
132
+ * Introduce schema changes with methods like `create_table` or `add_column` in one file.
133
+ * Update data with methods like `update_all` or `save` in another file.
134
+ * Add indexes concurrently within their own file as well.
135
+
153
136
  ```ruby
154
137
  class AddPublishedToPosts < ActiveRecord::Migration[5.0]
155
138
  def change
@@ -180,6 +163,8 @@ end
180
163
 
181
164
  #### Bad
182
165
 
166
+ The DDL transaction should only be disabled for migrations that add indexes.
167
+
183
168
  ```ruby
184
169
  class AddPublishedToPosts < ActiveRecord::Migration[5.0]
185
170
  disable_ddl_transaction!
@@ -202,6 +187,8 @@ end
202
187
 
203
188
  #### Good
204
189
 
190
+ Any other data or schema changes must live in their own migration files with the DDL transaction enabled just in case they make changes that need to be rolled back.
191
+
205
192
  ```ruby
206
193
  class AddPublishedToPosts < ActiveRecord::Migration[5.0]
207
194
  def change
@@ -222,6 +209,8 @@ end
222
209
 
223
210
  #### Bad
224
211
 
212
+ This might accidentally load tens or hundreds of thousands of records into memory all at the same time!
213
+
225
214
  ```ruby
226
215
  class BackportPublishedDefaultOnPosts < ActiveRecord::Migration[5.0]
227
216
  def change
@@ -234,6 +223,8 @@ end
234
223
 
235
224
  #### Good
236
225
 
226
+ Let's use the `find_each` method to fetch records in batches instead.
227
+
237
228
  ```ruby
238
229
  class BackportPublishedDefaultOnPosts < ActiveRecord::Migration[5.0]
239
230
  def change
@@ -251,6 +242,45 @@ end
251
242
  * Renaming a column
252
243
  * Renaming a table
253
244
 
245
+ ## Disabling "zero downtime migration" enforcements
246
+
247
+ We can disable any of these "zero downtime migration" enforcements by wrapping them in a `safety_assured` block.
248
+
249
+ ```ruby
250
+ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
251
+ def change
252
+ safety_assured do
253
+ add_column :posts, :published, :boolean, default: true
254
+ end
255
+ end
256
+ end
257
+ ```
258
+
259
+ We can also mark an entire migration as safe by using the `safety_assured` helper method.
260
+
261
+ ```ruby
262
+ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
263
+ safety_assured
264
+
265
+ def change
266
+ add_column :posts, :published, :boolean
267
+ Post.where("created_at >= ?", 1.day.ago).update_all(published: true)
268
+ end
269
+ end
270
+ ```
271
+
272
+ Enforcements can be globally disabled by setting `ENV["SAFETY_ASSURED"]` when running migrations.
273
+
274
+ ```bash
275
+ SAFETY_ASSURED=1 bundle exec rake db:migrate --trace
276
+ ```
277
+
278
+ These enforcements are **automatically disabled by default for the following scenarios**:
279
+
280
+ * The database schema is being loaded with `rake db:schema:load` instead of `db:migrate`
281
+ * The current migration is a reverse (down) migration
282
+ * The current migration is named `RollupMigrations`
283
+
254
284
  ## Testing
255
285
 
256
286
  ```bash
@@ -265,6 +295,10 @@ bundle exec rspec
265
295
  * Commit, do not mess with the version or history.
266
296
  * Open a pull request. Bonus points for topic branches.
267
297
 
298
+ ## Authors
299
+
300
+ * [Sean Huber](https://github.com/shuber)
301
+
268
302
  ## License
269
303
 
270
304
  [MIT](https://github.com/lendinghome/zero_downtime_migrations/blob/master/LICENSE) - Copyright © 2016 LendingHome
data/docker-compose.yml CHANGED
@@ -9,6 +9,7 @@ services:
9
9
  - postgres
10
10
  environment:
11
11
  - CI
12
+ - CODECLIMATE_REPO_TOKEN=efff74f42f6e9dff9daa21e21e8d8d2d3cb33f37b84a9d06bb907edb48a2cf6f
12
13
  - DATABASE_URL=postgres://postgres@postgres/zero_downtime_migrations
13
14
  - DOCKER=true
14
15
  - RACK_ENV=test
@@ -1,5 +1,11 @@
1
1
  module ZeroDowntimeMigrations
2
- class UnsafeMigrationError < StandardError
2
+ class Error < StandardError
3
+ end
4
+
5
+ class UndefinedValidationError < Error
6
+ end
7
+
8
+ class UnsafeMigrationError < Error
3
9
  def initialize(error, correction)
4
10
  error_with_type = "#{self.class.name}: #{error}"
5
11
  super("#{error_with_type}\n\n#{correction}")
@@ -94,7 +94,9 @@ module ZeroDowntimeMigrations
94
94
  end
95
95
 
96
96
  def validate(type, *args)
97
- Validation.validate!(type, self, *args) if Migration.unsafe?
97
+ Validation.validate!(type, self, *args)
98
+ rescue UndefinedValidationError
99
+ nil
98
100
  end
99
101
  end
100
102
  end
@@ -3,10 +3,8 @@ module ZeroDowntimeMigrations
3
3
  prepend Data
4
4
 
5
5
  def each(*)
6
- return super unless Migration.migrating? && Migration.unsafe?
7
- error = "Using ActiveRecord::Relation#each is unsafe!"
8
- correction = "Instead, let's use the find_each method to query in batches."
9
- raise UnsafeMigrationError.new(error, correction)
6
+ Validation.validate!(:find_each)
7
+ super
10
8
  end
11
9
  end
12
10
  end
@@ -15,6 +15,18 @@ module ZeroDowntimeMigrations
15
15
 
16
16
  Any other data or schema changes must live in their own migration files with
17
17
  the DDL transaction enabled just in case they need to be rolled back.
18
+
19
+ If you're 100% positive that this migration is already safe, then simply
20
+ add a call to `safety_assured` to your migration.
21
+
22
+ class #{migration_name} < ActiveRecord::Migration
23
+ disable_ddl_transaction!
24
+ safety_assured
25
+
26
+ def change
27
+ # ...
28
+ end
29
+ end
18
30
  MESSAGE
19
31
  end
20
32
  end
@@ -0,0 +1,32 @@
1
+ module ZeroDowntimeMigrations
2
+ class Validation
3
+ class FindEach < Validation
4
+ def validate!
5
+ message = "Using `ActiveRecord::Relation#each` is unsafe!"
6
+ error!(message, correction)
7
+ end
8
+
9
+ private
10
+
11
+ def correction
12
+ <<-MESSAGE.strip_heredoc
13
+ Let's use the `find_each` method to fetch records in batches instead.
14
+
15
+ Otherwise we may accidentally load tens or hundreds of thousands of
16
+ records into memory all at the same time!
17
+
18
+ If you're 100% positive that this migration is already safe, then wrap the
19
+ call to `each` in a `safety_assured` block.
20
+
21
+ class YourMigration < ActiveRecord::Migration
22
+ def change
23
+ safety_assured do
24
+ # use .each in this block
25
+ end
26
+ end
27
+ end
28
+ MESSAGE
29
+ end
30
+ end
31
+ end
32
+ end
@@ -16,6 +16,17 @@ module ZeroDowntimeMigrations
16
16
  * Introduce schema changes with methods like `create_table` or `add_column` in one file.
17
17
  * Update data with methods like `update_all` or `save` in another file.
18
18
  * Add indexes concurrently within their own file as well.
19
+
20
+ If you're 100% positive that this migration is already safe, then simply
21
+ add a call to `safety_assured` to your migration.
22
+
23
+ class #{migration_name} < ActiveRecord::Migration
24
+ safety_assured
25
+
26
+ def change
27
+ # ...
28
+ end
29
+ end
19
30
  MESSAGE
20
31
  end
21
32
  end
@@ -1,9 +1,14 @@
1
1
  module ZeroDowntimeMigrations
2
2
  class Validation
3
- def self.validate!(type, migration, *args)
3
+ def self.validate!(type, migration = nil, *args)
4
+ return unless Migration.migrating? && Migration.unsafe?
4
5
  validator = type.to_s.classify
5
- validator = const_get(validator) if const_defined?(validator)
6
- validator.new(migration, *args).validate! if validator
6
+
7
+ if const_defined?(validator)
8
+ const_get(validator).new(migration, *args).validate!
9
+ else
10
+ raise UndefinedValidationError.new(validator)
11
+ end
7
12
  end
8
13
 
9
14
  attr_reader :migration, :args
@@ -17,6 +22,10 @@ module ZeroDowntimeMigrations
17
22
  raise UnsafeMigrationError.new(*args)
18
23
  end
19
24
 
25
+ def migration_name
26
+ migration.class.name
27
+ end
28
+
20
29
  def options
21
30
  args.last.is_a?(Hash) ? args.last : {}
22
31
  end
@@ -2,14 +2,15 @@ require "active_record"
2
2
 
3
3
  require_relative "zero_downtime_migrations/data"
4
4
  require_relative "zero_downtime_migrations/dsl"
5
+ require_relative "zero_downtime_migrations/error"
5
6
  require_relative "zero_downtime_migrations/migration"
6
7
  require_relative "zero_downtime_migrations/relation"
7
8
  require_relative "zero_downtime_migrations/validation"
8
9
  require_relative "zero_downtime_migrations/validation/add_column"
9
10
  require_relative "zero_downtime_migrations/validation/add_index"
10
11
  require_relative "zero_downtime_migrations/validation/ddl_migration"
12
+ require_relative "zero_downtime_migrations/validation/find_each"
11
13
  require_relative "zero_downtime_migrations/validation/mixed_migration"
12
- require_relative "zero_downtime_migrations/unsafe_migration_error"
13
14
 
14
15
  ActiveRecord::Base.send(:prepend, ZeroDowntimeMigrations::Data)
15
16
  ActiveRecord::Migration.send(:prepend, ZeroDowntimeMigrations::Migration)
@@ -0,0 +1,10 @@
1
+ class CreateTableComments < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :comments do |t|
4
+ t.references :user, null: false
5
+ t.references :post, null: false
6
+ t.text :body, null: false
7
+ t.timestamps null: false
8
+ end
9
+ end
10
+ end
@@ -30,20 +30,4 @@ RSpec.describe ZeroDowntimeMigrations::Validation::AddIndex do
30
30
  expect { migration.migrate(:up) }.to raise_error(error)
31
31
  end
32
32
  end
33
-
34
- context "with the correct options" do
35
- let(:migration) do
36
- Class.new(ActiveRecord::Migration[5.0]) do
37
- disable_ddl_transaction!
38
-
39
- def change
40
- add_index :users, :updated_at, algorithm: :concurrently
41
- end
42
- end
43
- end
44
-
45
- it "does not raise an unsafe migration error" do
46
- expect { migration.migrate(:up) }.not_to raise_error(error)
47
- end
48
- end
49
33
  end
@@ -1,4 +1,4 @@
1
- RSpec.describe ZeroDowntimeMigrations::Relation do
1
+ RSpec.describe ZeroDowntimeMigrations::Validation::FindEach do
2
2
  let(:error) { ZeroDowntimeMigrations::UnsafeMigrationError }
3
3
 
4
4
  context "with data migrations using each" do
@@ -4,6 +4,14 @@ RSpec.describe ZeroDowntimeMigrations::Validation do
4
4
  let(:migration) { double("migration") }
5
5
  let(:args) { [] }
6
6
 
7
+ describe ".validate!" do
8
+ let(:error) { ZeroDowntimeMigrations::UndefinedValidationError }
9
+
10
+ it "raises UndefinedValidationError if one does not exist" do
11
+ expect { described_class.validate!(:invalid) }.to raise_error(error)
12
+ end
13
+ end
14
+
7
15
  describe "#args" do
8
16
  it "returns the initialized args" do
9
17
  expect(subject.args).to eq(args)
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.required_ruby_version = ">= 2.0.0"
12
12
  s.summary = "Zero downtime migrations with ActiveRecord and PostgreSQL"
13
13
  s.test_files = `git ls-files -- spec/*`.split("\n")
14
- s.version = "0.0.1"
14
+ s.version = "0.0.2"
15
15
 
16
16
  s.add_dependency "activerecord"
17
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zero_downtime_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - LendingHome
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-10-18 00:00:00.000000000 Z
11
+ date: 2016-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -46,13 +46,14 @@ files:
46
46
  - lib/zero_downtime_migrations.rb
47
47
  - lib/zero_downtime_migrations/data.rb
48
48
  - lib/zero_downtime_migrations/dsl.rb
49
+ - lib/zero_downtime_migrations/error.rb
49
50
  - lib/zero_downtime_migrations/migration.rb
50
51
  - lib/zero_downtime_migrations/relation.rb
51
- - lib/zero_downtime_migrations/unsafe_migration_error.rb
52
52
  - lib/zero_downtime_migrations/validation.rb
53
53
  - lib/zero_downtime_migrations/validation/add_column.rb
54
54
  - lib/zero_downtime_migrations/validation/add_index.rb
55
55
  - lib/zero_downtime_migrations/validation/ddl_migration.rb
56
+ - lib/zero_downtime_migrations/validation/find_each.rb
56
57
  - lib/zero_downtime_migrations/validation/mixed_migration.rb
57
58
  - spec/internal/app/models/post.rb
58
59
  - spec/internal/app/models/user.rb
@@ -63,13 +64,14 @@ files:
63
64
  - spec/internal/db/migrate/20161012223255_safe_add_index_with_env.rb
64
65
  - spec/internal/db/migrate/20161012223256_safe_add_index_with_dsl.rb
65
66
  - spec/internal/db/migrate/20161012223257_add_index_concurrently.rb
67
+ - spec/internal/db/migrate/20161012223258_create_table_comments.rb
66
68
  - spec/internal/db/schema.rb
67
69
  - spec/internal/log/.gitignore
68
70
  - spec/spec_helper.rb
69
- - spec/zero_downtime_migrations/relation_spec.rb
70
71
  - spec/zero_downtime_migrations/validation/add_column_spec.rb
71
72
  - spec/zero_downtime_migrations/validation/add_index_spec.rb
72
73
  - spec/zero_downtime_migrations/validation/ddl_migration_spec.rb
74
+ - spec/zero_downtime_migrations/validation/find_each_spec.rb
73
75
  - spec/zero_downtime_migrations/validation/mixed_migration_spec.rb
74
76
  - spec/zero_downtime_migrations/validation_spec.rb
75
77
  - spec/zero_downtime_migrations_spec.rb
@@ -113,13 +115,14 @@ test_files:
113
115
  - spec/internal/db/migrate/20161012223255_safe_add_index_with_env.rb
114
116
  - spec/internal/db/migrate/20161012223256_safe_add_index_with_dsl.rb
115
117
  - spec/internal/db/migrate/20161012223257_add_index_concurrently.rb
118
+ - spec/internal/db/migrate/20161012223258_create_table_comments.rb
116
119
  - spec/internal/db/schema.rb
117
120
  - spec/internal/log/.gitignore
118
121
  - spec/spec_helper.rb
119
- - spec/zero_downtime_migrations/relation_spec.rb
120
122
  - spec/zero_downtime_migrations/validation/add_column_spec.rb
121
123
  - spec/zero_downtime_migrations/validation/add_index_spec.rb
122
124
  - spec/zero_downtime_migrations/validation/ddl_migration_spec.rb
125
+ - spec/zero_downtime_migrations/validation/find_each_spec.rb
123
126
  - spec/zero_downtime_migrations/validation/mixed_migration_spec.rb
124
127
  - spec/zero_downtime_migrations/validation_spec.rb
125
128
  - spec/zero_downtime_migrations_spec.rb