zero_downtime_migrations 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +137 -12
  4. data/lib/zero_downtime_migrations/data.rb +8 -0
  5. data/lib/zero_downtime_migrations/dsl.rb +37 -0
  6. data/lib/zero_downtime_migrations/migration.rb +66 -17
  7. data/lib/zero_downtime_migrations/relation.rb +12 -0
  8. data/lib/zero_downtime_migrations/validation/add_column.rb +93 -0
  9. data/lib/zero_downtime_migrations/validation/add_index.rb +59 -0
  10. data/lib/zero_downtime_migrations/validation/ddl_migration.rb +22 -0
  11. data/lib/zero_downtime_migrations/validation/mixed_migration.rb +23 -0
  12. data/lib/zero_downtime_migrations/validation.rb +7 -1
  13. data/lib/zero_downtime_migrations.rb +13 -5
  14. data/spec/internal/app/models/post.rb +2 -0
  15. data/spec/internal/app/models/user.rb +2 -0
  16. data/spec/internal/db/migrate/20161012223255_safe_add_index_with_env.rb +2 -2
  17. data/spec/internal/db/migrate/20161012223256_safe_add_index_with_dsl.rb +7 -0
  18. data/spec/internal/db/migrate/{20161012223256_add_index_concurrently.rb → 20161012223257_add_index_concurrently.rb} +0 -0
  19. data/spec/zero_downtime_migrations/relation_spec.rb +53 -0
  20. data/spec/zero_downtime_migrations/{add_column_spec.rb → validation/add_column_spec.rb} +1 -1
  21. data/spec/zero_downtime_migrations/{add_index_spec.rb → validation/add_index_spec.rb} +1 -1
  22. data/spec/zero_downtime_migrations/validation/ddl_migration_spec.rb +68 -0
  23. data/spec/zero_downtime_migrations/validation/mixed_migration_spec.rb +78 -0
  24. data/spec/zero_downtime_migrations/validation_spec.rb +2 -2
  25. data/zero_downtime_migrations.gemspec +1 -1
  26. metadata +27 -10
  27. data/lib/zero_downtime_migrations/add_column.rb +0 -91
  28. data/lib/zero_downtime_migrations/add_index.rb +0 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f40fc64c97d9d1b74b7a26d11e3daaf1b2941896
4
- data.tar.gz: 7edb2b3a1b277191c2167a9a135c721e5d81ae78
3
+ metadata.gz: 5c8dfec1a4d2d84e2a9260829418235053eeec0c
4
+ data.tar.gz: 0568a51dd7990eb896354ed67cfe984a3bcc8f13
5
5
  SHA512:
6
- metadata.gz: 0f74bd5a1ef14f6664b0f5199bb1d11513cb64d2374fad6ce636e10cacff37ef8734c60f242e27ab931f8ae5dffd3cda3b2a6b72be70a1fee95dfde3a8da2b73
7
- data.tar.gz: a6caba1f552ad472dbb78525a11cb2495659ca2cb72be191f98861b1de711f0af8a5d54cb2955ee719a636c578c5e2b7ac8b3688fd5cc34da65ab3c17307832d
6
+ metadata.gz: 1b0b3503a2749957e5f310bd5c38bcfa3243a3fd2d077f793ca17fff631c384a65f6dc75d247588f52912bb43ee36d2bbbd6d173f33912f34abc357f9e0aa4f6
7
+ data.tar.gz: 1d2d7bc71333d19ba28b1c6600a99d23cf2094078f9f45dd9df826ce9306b0284b9e9236eae920b3b1224d33cddeb7f8ce020c81567d77448df1d36e62966b3c
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- zero_downtime_migrations (0.0.0)
4
+ zero_downtime_migrations (0.0.1)
5
5
  activerecord
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,15 +1,15 @@
1
- # zero_downtime_migrations
1
+ # ![LendingHome](https://cloud.githubusercontent.com/assets/2419/19467866/7efa93a8-94c8-11e6-93e7-4375dbb8a7bc.png) zero_downtime_migrations
2
2
 
3
3
  Zero downtime migrations with ActiveRecord and PostgreSQL. Catch problematic migrations at development/test time!
4
4
 
5
- Heavily insprired by the similar projects listed below. Our intent was to target PostgreSQL specific issues and provide clearer instructions on how to perform the migrations the "zero downtime way".
5
+ Heavily inspired by these similar projects:
6
6
 
7
7
  * https://github.com/ankane/strong_migrations
8
8
  * https://github.com/foobarfighter/safe-migrations
9
9
 
10
10
  ## Installation
11
11
 
12
- Simply add the gem to the project `Gemfile`. Ensure that it's only added to the `development` and `test` groups.
12
+ Simply add the gem to the project `Gemfile`. Ensure that it's only **added to the `development` and `test`** groups.
13
13
 
14
14
  ```ruby
15
15
  gem "zero_downtime_migrations", only: %i(development test)
@@ -17,13 +17,17 @@ gem "zero_downtime_migrations", only: %i(development test)
17
17
 
18
18
  ## Usage
19
19
 
20
- This gem will automatically raise exceptions when potential database locking migrations are detected. It checks for common things like:
20
+ This gem will automatically **raise exceptions when potential database locking migrations are detected**.
21
+
22
+ It checks for common things like:
21
23
 
22
24
  * Adding a column with a default
23
25
  * Adding a non-concurrent index
24
- * Mixing data changes with schema migrations
26
+ * Mixing data changes with index or schema migrations
27
+ * Performing data or schema migrations with the DDL transaction disabled
28
+ * Using `each` instead of `find_each` to loop thru `ActiveRecord` objects
25
29
 
26
- These exceptions display clear instructions of how to perform the same operation the "zero downtime way".
30
+ These exceptions display very clear instructions of how to perform the same operation the "zero downtime way".
27
31
 
28
32
  ## Disabling exceptions
29
33
 
@@ -39,16 +43,29 @@ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
39
43
  end
40
44
  ```
41
45
 
42
- We can also disable the exceptions by setting `ENV["SAFETY_ASSURED"]` when running migrations.
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.
43
60
 
44
61
  ```bash
45
62
  SAFETY_ASSURED=1 bundle exec rake db:migrate --trace
46
63
  ```
47
64
 
48
- These enforcements are automatically disabled by default for the following scenarios:
65
+ These enforcements are **automatically disabled by default for the following scenarios**:
49
66
 
50
- * We're loading the database schema with `rake db:schema:load` instead of `db:migrate`
51
- * We're migrating down (reverting a migration)
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
52
69
 
53
70
  ## Validations
54
71
 
@@ -117,14 +134,122 @@ class IndexUsersOnEmail < ActiveRecord::Migration[5.0]
117
134
  end
118
135
  ```
119
136
 
137
+ ### Mixing data/index/schema migrations
138
+
139
+ #### Bad
140
+
141
+ ```ruby
142
+ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
143
+ def change
144
+ add_column :posts, :published, :boolean
145
+ Post.update_all(published: true)
146
+ add_index :posts, :published
147
+ end
148
+ end
149
+ ```
150
+
151
+ #### Good
152
+
153
+ ```ruby
154
+ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
155
+ def change
156
+ add_column :posts, :published, :boolean
157
+ end
158
+ end
159
+ ```
160
+
161
+ ```ruby
162
+ class BackportPublishedOnPosts < ActiveRecord::Migration[5.0]
163
+ def change
164
+ Post.update_all(published: true)
165
+ end
166
+ end
167
+ ```
168
+
169
+ ```ruby
170
+ class IndexPublishedOnPosts < ActiveRecord::Migration[5.0]
171
+ disable_ddl_transaction!
172
+
173
+ def change
174
+ add_index :posts, :published, algorithm: :concurrently
175
+ end
176
+ end
177
+ ```
178
+
179
+ ### Disabling the DDL transaction
180
+
181
+ #### Bad
182
+
183
+ ```ruby
184
+ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
185
+ disable_ddl_transaction!
186
+
187
+ def change
188
+ add_column :posts, :published, :boolean
189
+ end
190
+ end
191
+ ```
192
+
193
+ ```ruby
194
+ class UpdatePublishedOnPosts < ActiveRecord::Migration[5.0]
195
+ disable_ddl_transaction!
196
+
197
+ def change
198
+ Post.update_all(published: true)
199
+ end
200
+ end
201
+ ```
202
+
203
+ #### Good
204
+
205
+ ```ruby
206
+ class AddPublishedToPosts < ActiveRecord::Migration[5.0]
207
+ def change
208
+ add_column :posts, :published, :boolean
209
+ end
210
+ end
211
+ ```
212
+
213
+ ```ruby
214
+ class UpdatePublishedOnPosts < ActiveRecord::Migration[5.0]
215
+ def change
216
+ Post.update_all(published: true)
217
+ end
218
+ end
219
+ ```
220
+
221
+ ### Looping thru `ActiveRecord::Base` objects
222
+
223
+ #### Bad
224
+
225
+ ```ruby
226
+ class BackportPublishedDefaultOnPosts < ActiveRecord::Migration[5.0]
227
+ def change
228
+ Post.all.each do |post|
229
+ post.update_attribute(published: true)
230
+ end
231
+ end
232
+ end
233
+ ```
234
+
235
+ #### Good
236
+
237
+ ```ruby
238
+ class BackportPublishedDefaultOnPosts < ActiveRecord::Migration[5.0]
239
+ def change
240
+ Post.all.find_each do |post|
241
+ post.update_attribute(published: true)
242
+ end
243
+ end
244
+ end
245
+ ```
246
+
120
247
  ### TODO
121
248
 
122
249
  * Changing a column type
123
250
  * Removing a column
124
251
  * Renaming a column
125
252
  * Renaming a table
126
- * Mixing data changes with schema or index migrations
127
- * Performing schema changes with the DDL transaction disabled
128
253
 
129
254
  ## Testing
130
255
 
@@ -0,0 +1,8 @@
1
+ module ZeroDowntimeMigrations
2
+ module Data
3
+ def initialize(*)
4
+ Migration.data = true
5
+ super
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+ module ZeroDowntimeMigrations
2
+ module DSL
3
+ attr_accessor :data, :ddl, :index, :migrating, :safe
4
+
5
+ def data?
6
+ !!@data
7
+ end
8
+
9
+ def ddl?
10
+ !!@ddl
11
+ end
12
+
13
+ def index?
14
+ !!@index
15
+ end
16
+
17
+ def migrating?
18
+ !!@migrating
19
+ end
20
+
21
+ def mixed?
22
+ [data?, ddl?, index?].select(&:itself).size > 1
23
+ end
24
+
25
+ def safe?
26
+ !!@safe || ENV["SAFETY_ASSURED"].presence
27
+ end
28
+
29
+ def safety_assured
30
+ Migration.safe = true
31
+ end
32
+
33
+ def unsafe?
34
+ !safe?
35
+ end
36
+ end
37
+ end
@@ -1,31 +1,80 @@
1
1
  module ZeroDowntimeMigrations
2
2
  module Migration
3
+ extend DSL
4
+
5
+ def self.prepended(mod)
6
+ mod.singleton_class.prepend(DSL)
7
+ end
8
+
3
9
  def ddl_disabled?
4
10
  !!disable_ddl_transaction
5
11
  end
6
12
 
13
+ def define(*)
14
+ Migration.migrating = true
15
+ Migration.safe = true
16
+ super
17
+ end
18
+
7
19
  def migrate(direction)
8
20
  @direction = direction
9
- super
21
+
22
+ Migration.data = false
23
+ Migration.ddl = false
24
+ Migration.index = false
25
+ Migration.migrating = true
26
+ Migration.safe ||= reverse_migration? || rollup_migration?
27
+
28
+ super.tap do
29
+ validate(:ddl_migration)
30
+ validate(:mixed_migration)
31
+ Migration.migrating = false
32
+ Migration.safe = false
33
+ end
10
34
  end
11
35
 
12
36
  private
13
37
 
14
- def loading_schema?
15
- is_a?(ActiveRecord::Schema)
38
+ def ddl_method?(method)
39
+ %i(
40
+ add_belongs_to
41
+ add_column
42
+ add_foreign_key
43
+ add_reference
44
+ add_timestamps
45
+ change_column
46
+ change_column_default
47
+ change_column_null
48
+ change_table
49
+ create_join_table
50
+ create_table
51
+ drop_join_table
52
+ drop_table
53
+ remove_belongs_to
54
+ remove_column
55
+ remove_columns
56
+ remove_foreign_key
57
+ remove_index
58
+ remove_index!
59
+ remove_reference
60
+ remove_timestamps
61
+ rename_column
62
+ rename_column_indexes
63
+ rename_index
64
+ rename_table
65
+ rename_table_indexes
66
+ ).include?(method)
16
67
  end
17
68
 
18
- def method_missing(method, *args)
19
- unless loading_schema? || reverse_migration? || rollup_migration? || safe?
20
- validator = "#{namespace}::#{method.to_s.classify}".safe_constantize
21
- validator.new(self, args).validate! if validator
22
- end
23
-
24
- super
69
+ def index_method?(method)
70
+ %i(add_index).include?(method)
25
71
  end
26
72
 
27
- def namespace
28
- Module.nesting.last
73
+ def method_missing(method, *args)
74
+ Migration.ddl = true if ddl_method?(method)
75
+ Migration.index = true if index_method?(method)
76
+ validate(method, *args)
77
+ super
29
78
  end
30
79
 
31
80
  def reverse_migration?
@@ -37,15 +86,15 @@ module ZeroDowntimeMigrations
37
86
  end
38
87
 
39
88
  def safety_assured
40
- safe = @safe
41
- @safe = true
89
+ safe = Migration.safe
90
+ Migration.safe = true
42
91
  yield
43
92
  ensure
44
- @safe = safe
93
+ Migration.safe = safe
45
94
  end
46
95
 
47
- def safe?
48
- !!(@safe || ENV["SAFE_MIGRATION"].presence)
96
+ def validate(type, *args)
97
+ Validation.validate!(type, self, *args) if Migration.unsafe?
49
98
  end
50
99
  end
51
100
  end
@@ -0,0 +1,12 @@
1
+ module ZeroDowntimeMigrations
2
+ module Relation
3
+ prepend Data
4
+
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)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,93 @@
1
+ module ZeroDowntimeMigrations
2
+ class Validation
3
+ class AddColumn < Validation
4
+ def validate!
5
+ return if options[:default].nil? # only nil is safe
6
+ message = "Adding a column with a default is unsafe!"
7
+ error!(message, correction)
8
+ end
9
+
10
+ private
11
+
12
+ def correction
13
+ <<-MESSAGE.strip_heredoc
14
+ This action can potentially lock your database table!
15
+
16
+ Instead, let's first add the column without a default.
17
+
18
+ class Add#{column_title}To#{table_title} < ActiveRecord::Migration
19
+ def change
20
+ add_column :#{table}, :#{column}, :#{column_type}
21
+ end
22
+ end
23
+
24
+ Then set the new column default in a separate migration. Note that
25
+ this does not update any existing data.
26
+
27
+ class AddDefault#{column_title}To#{table_title} < ActiveRecord::Migration
28
+ def change
29
+ change_column_default :#{table}, :#{column}, #{column_default}
30
+ end
31
+ end
32
+
33
+ If necessary then backport the default value for existing data in batches.
34
+ This should be done in its own migration as well.
35
+
36
+ class BackportDefault#{column_title}To#{table_title} < ActiveRecord::Migration
37
+ def change
38
+ #{table_model}.select(:id).find_in_batches.with_index do |records, index|
39
+ puts "Processing batch \#{index + 1}\\r"
40
+ #{table_model}.where(id: records).update_all(#{column}: #{column_default})
41
+ end
42
+ end
43
+ end
44
+
45
+ Note that in some cases it may not even be necessary to backport a default value.
46
+
47
+ class #{table_model} < ActiveRecord::Base
48
+ def #{column}
49
+ self["#{column}"] ||= #{column_default}
50
+ end
51
+ end
52
+
53
+ If you're 100% positive that this migration is already safe, then wrap the
54
+ call to `add_column` in a `safety_assured` block.
55
+
56
+ class Add#{column_title}To#{table_title} < ActiveRecord::Migration
57
+ def change
58
+ safety_assured { add_column :#{table}, :#{column}, :#{column_type}, default: #{column_default} }
59
+ end
60
+ end
61
+ MESSAGE
62
+ end
63
+
64
+ def column
65
+ args[1]
66
+ end
67
+
68
+ def column_default
69
+ options[:default].inspect
70
+ end
71
+
72
+ def column_title
73
+ column.to_s.camelize
74
+ end
75
+
76
+ def column_type
77
+ args[2]
78
+ end
79
+
80
+ def table
81
+ args[0]
82
+ end
83
+
84
+ def table_model
85
+ table_title.singularize
86
+ end
87
+
88
+ def table_title
89
+ table.to_s.camelize
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,59 @@
1
+ module ZeroDowntimeMigrations
2
+ class Validation
3
+ class AddIndex < Validation
4
+ def validate!
5
+ return if concurrent? && migration.ddl_disabled?
6
+ message = "Adding a non-concurrent index is unsafe!"
7
+ error!(message, correction)
8
+ end
9
+
10
+ private
11
+
12
+ def correction
13
+ <<-MESSAGE.strip_heredoc
14
+ This action can potentially lock your database table!
15
+
16
+ Instead, let's add the index concurrently in its own migration with
17
+ the DDL transaction disabled.
18
+
19
+ class Index#{table_title}On#{column_title} < ActiveRecord::Migration
20
+ disable_ddl_transaction!
21
+
22
+ def change
23
+ add_index :#{table}, #{column.inspect}, algorithm: :concurrently
24
+ end
25
+ end
26
+
27
+ If you're 100% positive that this migration is already safe, then wrap the
28
+ call to `add_index` in a `safety_assured` block.
29
+
30
+ class Index#{table_title}On#{column_title} < ActiveRecord::Migration
31
+ def change
32
+ safety_assured { add_index :#{table}, #{column.inspect} }
33
+ end
34
+ end
35
+ MESSAGE
36
+ end
37
+
38
+ def concurrent?
39
+ options[:algorithm] == :concurrently
40
+ end
41
+
42
+ def column
43
+ args[1]
44
+ end
45
+
46
+ def column_title
47
+ Array(column).map(&:to_s).join("_and_").camelize
48
+ end
49
+
50
+ def table
51
+ args[0]
52
+ end
53
+
54
+ def table_title
55
+ table.to_s.camelize
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,22 @@
1
+ module ZeroDowntimeMigrations
2
+ class Validation
3
+ class DdlMigration < Validation
4
+ def validate!
5
+ return unless migration.ddl_disabled? && !Migration.index?
6
+ message = "Disabling the DDL transaction is unsafe!"
7
+ error!(message, correction)
8
+ end
9
+
10
+ private
11
+
12
+ def correction
13
+ <<-MESSAGE.strip_heredoc
14
+ The DDL transaction should only be disabled for migrations that add indexes.
15
+
16
+ Any other data or schema changes must live in their own migration files with
17
+ the DDL transaction enabled just in case they need to be rolled back.
18
+ MESSAGE
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ module ZeroDowntimeMigrations
2
+ class Validation
3
+ class MixedMigration < Validation
4
+ def validate!
5
+ return unless Migration.mixed?
6
+ message = "Mixing data/index/schema changes in the same migration is unsafe!"
7
+ error!(message, correction)
8
+ end
9
+
10
+ private
11
+
12
+ def correction
13
+ <<-MESSAGE.strip_heredoc
14
+ Instead, let's split apart these types of migrations into separate files.
15
+
16
+ * Introduce schema changes with methods like `create_table` or `add_column` in one file.
17
+ * Update data with methods like `update_all` or `save` in another file.
18
+ * Add indexes concurrently within their own file as well.
19
+ MESSAGE
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,8 +1,14 @@
1
1
  module ZeroDowntimeMigrations
2
2
  class Validation
3
+ def self.validate!(type, migration, *args)
4
+ validator = type.to_s.classify
5
+ validator = const_get(validator) if const_defined?(validator)
6
+ validator.new(migration, *args).validate! if validator
7
+ end
8
+
3
9
  attr_reader :migration, :args
4
10
 
5
- def initialize(migration, args)
11
+ def initialize(migration, *args)
6
12
  @migration = migration
7
13
  @args = args
8
14
  end
@@ -1,19 +1,27 @@
1
1
  require "active_record"
2
- require "pathname"
3
2
 
3
+ require_relative "zero_downtime_migrations/data"
4
+ require_relative "zero_downtime_migrations/dsl"
4
5
  require_relative "zero_downtime_migrations/migration"
6
+ require_relative "zero_downtime_migrations/relation"
5
7
  require_relative "zero_downtime_migrations/validation"
8
+ require_relative "zero_downtime_migrations/validation/add_column"
9
+ require_relative "zero_downtime_migrations/validation/add_index"
10
+ require_relative "zero_downtime_migrations/validation/ddl_migration"
11
+ require_relative "zero_downtime_migrations/validation/mixed_migration"
6
12
  require_relative "zero_downtime_migrations/unsafe_migration_error"
7
13
 
8
- require_relative "zero_downtime_migrations/add_column"
9
- require_relative "zero_downtime_migrations/add_index"
10
-
14
+ ActiveRecord::Base.send(:prepend, ZeroDowntimeMigrations::Data)
11
15
  ActiveRecord::Migration.send(:prepend, ZeroDowntimeMigrations::Migration)
16
+ ActiveRecord::Relation.send(:prepend, ZeroDowntimeMigrations::Relation)
17
+ ActiveRecord::Schema.send(:prepend, ZeroDowntimeMigrations::Migration)
12
18
 
13
19
  module ZeroDowntimeMigrations
20
+ GEMSPEC = name.underscore.concat(".gemspec")
21
+
14
22
  class << self
15
23
  def gemspec
16
- @gemspec ||= Gem::Specification.load(root.join("zero_downtime_migrations.gemspec").to_s)
24
+ @gemspec ||= Gem::Specification.load(root.join(GEMSPEC).to_s)
17
25
  end
18
26
 
19
27
  def root
@@ -0,0 +1,2 @@
1
+ class Post < ActiveRecord::Base
2
+ end
@@ -0,0 +1,2 @@
1
+ class User < ActiveRecord::Base
2
+ end
@@ -1,7 +1,7 @@
1
1
  class SafeAddIndexWithEnv < ActiveRecord::Migration[5.0]
2
2
  def change
3
- ENV["SAFE_MIGRATION"] = "1"
3
+ ENV["SAFETY_ASSURED"] = "1"
4
4
  add_index :users, :created_at
5
- ENV.delete("SAFE_MIGRATION")
5
+ ENV.delete("SAFETY_ASSURED")
6
6
  end
7
7
  end
@@ -0,0 +1,7 @@
1
+ class SafeAddIndexWithDsl < ActiveRecord::Migration[5.0]
2
+ safety_assured
3
+
4
+ def change
5
+ add_index :posts, :created_at
6
+ end
7
+ end
@@ -0,0 +1,53 @@
1
+ RSpec.describe ZeroDowntimeMigrations::Relation do
2
+ let(:error) { ZeroDowntimeMigrations::UnsafeMigrationError }
3
+
4
+ context "with data migrations using each" do
5
+ let(:migration) do
6
+ Class.new(ActiveRecord::Migration[5.0]) do
7
+ def change
8
+ User.all.each
9
+ end
10
+ end
11
+ end
12
+
13
+ it "raises an unsafe migration error" do
14
+ expect { migration.migrate(:up) }.to raise_error(error)
15
+ end
16
+ end
17
+
18
+ context "with data migrations using each within safety_assured" do
19
+ let(:migration) do
20
+ Class.new(ActiveRecord::Migration[5.0]) do
21
+ def change
22
+ safety_assured do
23
+ User.all.each
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ it "does not raise an unsafe migration error" do
30
+ expect { migration.migrate(:up) }.not_to raise_error(error)
31
+ end
32
+ end
33
+
34
+ context "with data migrations using find_each" do
35
+ let(:migration) do
36
+ Class.new(ActiveRecord::Migration[5.0]) do
37
+ def change
38
+ User.all.find_each
39
+ end
40
+ end
41
+ end
42
+
43
+ it "does not raise an unsafe migration error" do
44
+ expect { migration.migrate(:up) }.not_to raise_error(error)
45
+ end
46
+ end
47
+
48
+ context "outside of a migration" do
49
+ it "does not raise an unsafe migration error" do
50
+ expect { User.all.each }.not_to raise_error(error)
51
+ end
52
+ end
53
+ end
@@ -1,4 +1,4 @@
1
- RSpec.describe ZeroDowntimeMigrations::AddColumn do
1
+ RSpec.describe ZeroDowntimeMigrations::Validation::AddColumn do
2
2
  let(:error) { ZeroDowntimeMigrations::UnsafeMigrationError }
3
3
 
4
4
  context "with a default" do
@@ -1,4 +1,4 @@
1
- RSpec.describe ZeroDowntimeMigrations::AddIndex do
1
+ RSpec.describe ZeroDowntimeMigrations::Validation::AddIndex do
2
2
  let(:error) { ZeroDowntimeMigrations::UnsafeMigrationError }
3
3
 
4
4
  context "with ddl transaction enabled" do
@@ -0,0 +1,68 @@
1
+ RSpec.describe ZeroDowntimeMigrations::Validation::DdlMigration do
2
+ let(:error) { ZeroDowntimeMigrations::UnsafeMigrationError }
3
+
4
+ context "with a migration that adds a column with ddl disabled" do
5
+ let(:migration) do
6
+ Class.new(ActiveRecord::Migration[5.0]) do
7
+ disable_ddl_transaction!
8
+
9
+ def change
10
+ add_column :users, :active, :boolean
11
+ end
12
+ end
13
+ end
14
+
15
+ it "raises an unsafe migration error" do
16
+ expect { migration.migrate(:up) }.to raise_error(error)
17
+ end
18
+ end
19
+
20
+ context "with a migration that queries data with ddl disabled" do
21
+ let(:migration) do
22
+ Class.new(ActiveRecord::Migration[5.0]) do
23
+ disable_ddl_transaction!
24
+
25
+ def change
26
+ User.find_in_batches
27
+ end
28
+ end
29
+ end
30
+
31
+ it "raises an unsafe migration error" do
32
+ expect { migration.migrate(:up) }.to raise_error(error)
33
+ end
34
+ end
35
+
36
+ context "with a migration that updates data with ddl disabled" do
37
+ let(:migration) do
38
+ Class.new(ActiveRecord::Migration[5.0]) do
39
+ disable_ddl_transaction!
40
+
41
+ def change
42
+ add_column :users, :active, :boolean
43
+ User.update_all(active: true)
44
+ end
45
+ end
46
+ end
47
+
48
+ it "raises an unsafe migration error" do
49
+ expect { migration.migrate(:up) }.to raise_error(error)
50
+ end
51
+ end
52
+
53
+ context "with a migration that creates data with ddl disabled" do
54
+ let(:migration) do
55
+ Class.new(ActiveRecord::Migration[5.0]) do
56
+ disable_ddl_transaction!
57
+
58
+ def change
59
+ User.new(email: "test").save!
60
+ end
61
+ end
62
+ end
63
+
64
+ it "raises an unsafe migration error" do
65
+ expect { migration.migrate(:up) }.to raise_error(error)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,78 @@
1
+ RSpec.describe ZeroDowntimeMigrations::Validation::MixedMigration do
2
+ let(:error) { ZeroDowntimeMigrations::UnsafeMigrationError }
3
+
4
+ context "with a migration that adds a column and index" do
5
+ let(:migration) do
6
+ Class.new(ActiveRecord::Migration[5.0]) do
7
+ def change
8
+ add_column :users, :active, :boolean
9
+ add_index :users, :active
10
+ end
11
+ end
12
+ end
13
+
14
+ it "raises an unsafe migration error" do
15
+ expect { migration.migrate(:up) }.to raise_error(error)
16
+ end
17
+ end
18
+
19
+ context "with a migration that adds a column and queries data" do
20
+ let(:migration) do
21
+ Class.new(ActiveRecord::Migration[5.0]) do
22
+ def change
23
+ add_column :users, :active, :boolean
24
+ User.find_in_batches
25
+ end
26
+ end
27
+ end
28
+
29
+ it "raises an unsafe migration error" do
30
+ expect { migration.migrate(:up) }.to raise_error(error)
31
+ end
32
+ end
33
+
34
+ context "with a migration that adds a column and updates data" do
35
+ let(:migration) do
36
+ Class.new(ActiveRecord::Migration[5.0]) do
37
+ def change
38
+ add_column :users, :active, :boolean
39
+ User.update_all(active: true)
40
+ end
41
+ end
42
+ end
43
+
44
+ it "raises an unsafe migration error" do
45
+ expect { migration.migrate(:up) }.to raise_error(error)
46
+ end
47
+ end
48
+
49
+ context "with a migration that adds an index and updates data" do
50
+ let(:migration) do
51
+ Class.new(ActiveRecord::Migration[5.0]) do
52
+ def change
53
+ User.where(email: nil).delete_all
54
+ add_index :users, :created_at
55
+ end
56
+ end
57
+ end
58
+
59
+ it "raises an unsafe migration error" do
60
+ expect { migration.migrate(:up) }.to raise_error(error)
61
+ end
62
+ end
63
+
64
+ context "with a migration that adds a column and creates data" do
65
+ let(:migration) do
66
+ Class.new(ActiveRecord::Migration[5.0]) do
67
+ def change
68
+ add_column :users, :active, :boolean
69
+ User.new(email: "test").save!
70
+ end
71
+ end
72
+ end
73
+
74
+ it "raises an unsafe migration error" do
75
+ expect { migration.migrate(:up) }.to raise_error(error)
76
+ end
77
+ end
78
+ end
@@ -1,8 +1,8 @@
1
1
  RSpec.describe ZeroDowntimeMigrations::Validation do
2
- subject { described_class.new(migration, args) }
2
+ subject { described_class.new(migration, *args) }
3
3
 
4
4
  let(:migration) { double("migration") }
5
- let(:args) { double("args") }
5
+ let(:args) { [] }
6
6
 
7
7
  describe "#args" do
8
8
  it "returns the initialized args" do
@@ -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.0"
14
+ s.version = "0.0.1"
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.0
4
+ version: 0.0.1
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-17 00:00:00.000000000 Z
11
+ date: 2016-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -44,22 +44,33 @@ files:
44
44
  - bin/test
45
45
  - docker-compose.yml
46
46
  - lib/zero_downtime_migrations.rb
47
- - lib/zero_downtime_migrations/add_column.rb
48
- - lib/zero_downtime_migrations/add_index.rb
47
+ - lib/zero_downtime_migrations/data.rb
48
+ - lib/zero_downtime_migrations/dsl.rb
49
49
  - lib/zero_downtime_migrations/migration.rb
50
+ - lib/zero_downtime_migrations/relation.rb
50
51
  - lib/zero_downtime_migrations/unsafe_migration_error.rb
51
52
  - lib/zero_downtime_migrations/validation.rb
53
+ - lib/zero_downtime_migrations/validation/add_column.rb
54
+ - lib/zero_downtime_migrations/validation/add_index.rb
55
+ - lib/zero_downtime_migrations/validation/ddl_migration.rb
56
+ - lib/zero_downtime_migrations/validation/mixed_migration.rb
57
+ - spec/internal/app/models/post.rb
58
+ - spec/internal/app/models/user.rb
52
59
  - spec/internal/config/database.yml
53
60
  - spec/internal/db/migrate/20161012223252_rollup_migrations.rb
54
61
  - spec/internal/db/migrate/20161012223253_safe_add_column_with_default.rb
55
62
  - spec/internal/db/migrate/20161012223254_safe_add_index.rb
56
63
  - spec/internal/db/migrate/20161012223255_safe_add_index_with_env.rb
57
- - spec/internal/db/migrate/20161012223256_add_index_concurrently.rb
64
+ - spec/internal/db/migrate/20161012223256_safe_add_index_with_dsl.rb
65
+ - spec/internal/db/migrate/20161012223257_add_index_concurrently.rb
58
66
  - spec/internal/db/schema.rb
59
67
  - spec/internal/log/.gitignore
60
68
  - spec/spec_helper.rb
61
- - spec/zero_downtime_migrations/add_column_spec.rb
62
- - spec/zero_downtime_migrations/add_index_spec.rb
69
+ - spec/zero_downtime_migrations/relation_spec.rb
70
+ - spec/zero_downtime_migrations/validation/add_column_spec.rb
71
+ - spec/zero_downtime_migrations/validation/add_index_spec.rb
72
+ - spec/zero_downtime_migrations/validation/ddl_migration_spec.rb
73
+ - spec/zero_downtime_migrations/validation/mixed_migration_spec.rb
63
74
  - spec/zero_downtime_migrations/validation_spec.rb
64
75
  - spec/zero_downtime_migrations_spec.rb
65
76
  - zero_downtime_migrations.gemspec
@@ -93,16 +104,22 @@ signing_key:
93
104
  specification_version: 4
94
105
  summary: Zero downtime migrations with ActiveRecord and PostgreSQL
95
106
  test_files:
107
+ - spec/internal/app/models/post.rb
108
+ - spec/internal/app/models/user.rb
96
109
  - spec/internal/config/database.yml
97
110
  - spec/internal/db/migrate/20161012223252_rollup_migrations.rb
98
111
  - spec/internal/db/migrate/20161012223253_safe_add_column_with_default.rb
99
112
  - spec/internal/db/migrate/20161012223254_safe_add_index.rb
100
113
  - spec/internal/db/migrate/20161012223255_safe_add_index_with_env.rb
101
- - spec/internal/db/migrate/20161012223256_add_index_concurrently.rb
114
+ - spec/internal/db/migrate/20161012223256_safe_add_index_with_dsl.rb
115
+ - spec/internal/db/migrate/20161012223257_add_index_concurrently.rb
102
116
  - spec/internal/db/schema.rb
103
117
  - spec/internal/log/.gitignore
104
118
  - spec/spec_helper.rb
105
- - spec/zero_downtime_migrations/add_column_spec.rb
106
- - spec/zero_downtime_migrations/add_index_spec.rb
119
+ - spec/zero_downtime_migrations/relation_spec.rb
120
+ - spec/zero_downtime_migrations/validation/add_column_spec.rb
121
+ - spec/zero_downtime_migrations/validation/add_index_spec.rb
122
+ - spec/zero_downtime_migrations/validation/ddl_migration_spec.rb
123
+ - spec/zero_downtime_migrations/validation/mixed_migration_spec.rb
107
124
  - spec/zero_downtime_migrations/validation_spec.rb
108
125
  - spec/zero_downtime_migrations_spec.rb
@@ -1,91 +0,0 @@
1
- module ZeroDowntimeMigrations
2
- class AddColumn < Validation
3
- def validate!
4
- return if options[:default].nil? # only nil is safe
5
- message = "Adding a column with a default is unsafe!"
6
- error!(message, correction)
7
- end
8
-
9
- private
10
-
11
- def correction
12
- <<-MESSAGE.strip_heredoc
13
- This action can potentially lock your database table!
14
-
15
- Instead, let's first add the column without a default.
16
-
17
- class Add#{column_title}To#{table_title} < ActiveRecord::Migration
18
- def change
19
- add_column :#{table}, :#{column}, :#{column_type}
20
- end
21
- end
22
-
23
- Then set the new column default in a separate migration. Note that
24
- this does not update any existing data.
25
-
26
- class AddDefault#{column_title}To#{table_title} < ActiveRecord::Migration
27
- def change
28
- change_column_default :#{table}, :#{column}, #{column_default}
29
- end
30
- end
31
-
32
- If necessary then backport the default value for existing data in batches.
33
- This should be done in its own migration as well.
34
-
35
- class BackportDefault#{column_title}To#{table_title} < ActiveRecord::Migration
36
- def change
37
- #{table_model}.select(:id).find_in_batches.with_index do |records, index|
38
- puts "Processing batch \#{index + 1}\\r"
39
- #{table_model}.where(id: records).update_all(#{column}: #{column_default})
40
- end
41
- end
42
- end
43
-
44
- Note that in some cases it may not even be necessary to backport a default value.
45
-
46
- class #{table_model} < ActiveRecord::Base
47
- def #{column}
48
- self["#{column}"] ||= #{column_default}
49
- end
50
- end
51
-
52
- If you're 100% positive that this migration is already safe, then wrap the
53
- call to `add_column` in a `safety_assured` block.
54
-
55
- class Add#{column_title}To#{table_title} < ActiveRecord::Migration
56
- def change
57
- safety_assured { add_column :#{table}, :#{column}, :#{column_type}, default: #{column_default} }
58
- end
59
- end
60
- MESSAGE
61
- end
62
-
63
- def column
64
- args[1]
65
- end
66
-
67
- def column_default
68
- options[:default].inspect
69
- end
70
-
71
- def column_title
72
- column.to_s.camelize
73
- end
74
-
75
- def column_type
76
- args[2]
77
- end
78
-
79
- def table
80
- args[0]
81
- end
82
-
83
- def table_model
84
- table_title.singularize
85
- end
86
-
87
- def table_title
88
- table.to_s.camelize
89
- end
90
- end
91
- end
@@ -1,57 +0,0 @@
1
- module ZeroDowntimeMigrations
2
- class AddIndex < Validation
3
- def validate!
4
- return if concurrent? && migration.ddl_disabled?
5
- message = "Adding a non-concurrent index is unsafe!"
6
- error!(message, correction)
7
- end
8
-
9
- private
10
-
11
- def correction
12
- <<-MESSAGE.strip_heredoc
13
- This action can potentially lock your database table!
14
-
15
- Instead, let's add the index concurrently in its own migration with
16
- the DDL transaction disabled.
17
-
18
- class Index#{table_title}On#{column_title} < ActiveRecord::Migration
19
- disable_ddl_transaction!
20
-
21
- def change
22
- add_index :#{table}, #{column.inspect}, algorithm: :concurrently
23
- end
24
- end
25
-
26
- If you're 100% positive that this migration is already safe, then wrap the
27
- call to `add_index` in a `safety_assured` block.
28
-
29
- class Index#{table_title}On#{column_title} < ActiveRecord::Migration
30
- def change
31
- safety_assured { add_index :#{table}, #{column.inspect} }
32
- end
33
- end
34
- MESSAGE
35
- end
36
-
37
- def concurrent?
38
- options[:algorithm] == :concurrently
39
- end
40
-
41
- def column
42
- args[1]
43
- end
44
-
45
- def column_title
46
- Array(column).map(&:to_s).join("_and_").camelize
47
- end
48
-
49
- def table
50
- args[0]
51
- end
52
-
53
- def table_title
54
- table.to_s.camelize
55
- end
56
- end
57
- end