strong_migrations 0.4.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4a27bb71eb7436386540b0f86fb17d3bf82f2e100eff8e836acb842d22ffab6
4
- data.tar.gz: 81b1f63c48a92599e7d421adbbd22411e26a467c052520dd1ee0982d2e150f6a
3
+ metadata.gz: bce49483e36caa343a3496771d052e4cc7d4db1e72c0ca23d80be1339bcb4dae
4
+ data.tar.gz: e15a13f2c41a610c8a6825a209581b2b66d28584ecc41fcc6c46f2f0d69ff293
5
5
  SHA512:
6
- metadata.gz: 27f6188cf4fdb6391a3f5e7926e292029c9ae47260010349ee93fa908d7abd74a40b2ac6d9bc7970f1e9c0b35c85c60b0c8719b125ee32dac3903190823483cf
7
- data.tar.gz: 5682113d7f4a56a11cf9c9dfe3bcefb373c101737bf8073970f1a56eb87efeb79e0a648c6908d7f8f77adf3ed68690a7b4523a43abef149ef09b8cff945ea8e2
6
+ metadata.gz: 68f4b5e294502c361c23cf17bf921310a285b27b4f5112c115ac26683af0f3841bf2815229bf793feab85ae71cac93e3e4e76f6928aa47cfe913669d2c3219ce
7
+ data.tar.gz: 2dc364a36b8aa88408554a5671d8380cc6d27ab17321542181cebac3568cc5bcc4ea61d14508e6c59a9641e41e074ee0120d7f4098648c1f6508be1d479289c5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 0.4.1
2
+
3
+ - Added `target_postgresql_version`
4
+ - Added `unscoped` to backfill instructions
5
+
1
6
  ## 0.4.0
2
7
 
3
8
  - Added check for `add_foreign_key`
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Strong Migrations
2
2
 
3
- Catch unsafe migrations at dev time
3
+ Catch unsafe migrations in development
4
4
 
5
5
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
6
6
 
@@ -25,7 +25,7 @@ Strong Migrations detects potentially dangerous operations in migrations, preven
25
25
  The following operations can cause downtime or errors:
26
26
 
27
27
  - [[+]](#removing-a-column) removing a column
28
- - [[+]](#adding-a-column-with-a-default-value) adding a column with a non-null default value to an existing table
28
+ - [[+]](#adding-a-column-with-a-default-value) adding a column with a default value
29
29
  - [[+]](#backfilling-data) backfilling data
30
30
  - [[+]](#adding-an-index) adding an index non-concurrently
31
31
  - [[+]](#adding-a-reference) adding a reference
@@ -39,7 +39,7 @@ The following operations can cause downtime or errors:
39
39
 
40
40
  Also checks for best practices:
41
41
 
42
- - [[+]](#) keeping non-unique indexes to three columns or less
42
+ - [[+]](#keeping-non-unique-indexes-to-three-columns-or-less) keeping non-unique indexes to three columns or less
43
43
 
44
44
  ## The Zero Downtime Way
45
45
 
@@ -84,7 +84,7 @@ end
84
84
 
85
85
  #### Bad
86
86
 
87
- Adding a column with a non-null default causes the entire table to be rewritten.
87
+ Adding a column with a default value to an existing table causes the entire table to be rewritten.
88
88
 
89
89
  ```ruby
90
90
  class AddSomeColumnToUsers < ActiveRecord::Migration[5.2]
@@ -141,7 +141,7 @@ class BackfillSomeColumn < ActiveRecord::Migration[5.2]
141
141
  disable_ddl_transaction!
142
142
 
143
143
  def change
144
- User.in_batches do |relation|
144
+ User.unscoped.in_batches do |relation|
145
145
  relation.update_all some_column: "default_value"
146
146
  sleep(0.1) # throttle
147
147
  end
@@ -153,7 +153,7 @@ end
153
153
 
154
154
  #### Bad
155
155
 
156
- In Postgres, adding a non-concurrent indexes lock the table.
156
+ In Postgres, adding a non-concurrent index locks the table.
157
157
 
158
158
  ```ruby
159
159
  class AddSomeIndexToUsers < ActiveRecord::Migration[5.2]
@@ -330,7 +330,7 @@ A safer approach is to:
330
330
  5. Stop writing to the old table
331
331
  6. Drop the old table
332
332
 
333
- ### Creating a table with the `force` option
333
+ ### Creating a table with the force option
334
334
 
335
335
  #### Bad
336
336
 
@@ -360,7 +360,7 @@ class CreateUsers < ActiveRecord::Migration[5.2]
360
360
  end
361
361
  ```
362
362
 
363
- ### Using `change_column_null` with a default value
363
+ ### Using change_column_null with a default value
364
364
 
365
365
  #### Bad
366
366
 
@@ -418,7 +418,7 @@ end
418
418
 
419
419
  #### Bad
420
420
 
421
- Adding an index with more than three columns only helps on extremely large tables.
421
+ Adding a non-unique index with more than three columns rarely improves performance.
422
422
 
423
423
  ```ruby
424
424
  class AddSomeIndexToUsers < ActiveRecord::Migration[5.2]
@@ -430,10 +430,12 @@ end
430
430
 
431
431
  #### Good
432
432
 
433
+ Instead, start an index with columns that narrow down the results the most.
434
+
433
435
  ```ruby
434
436
  class AddSomeIndexToUsers < ActiveRecord::Migration[5.2]
435
437
  def change
436
- add_index :users, [:a, :b, :c]
438
+ add_index :users, [:b, :d]
437
439
  end
438
440
  end
439
441
  ```
@@ -482,10 +484,10 @@ Use the version from your latest migration.
482
484
 
483
485
  ## Dangerous Tasks
484
486
 
485
- For safety, dangerous rake tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use:
487
+ For safety, dangerous database tasks are disabled in production - `db:drop`, `db:reset`, `db:schema:load`, and `db:structure:load`. To get around this, use:
486
488
 
487
489
  ```sh
488
- SAFETY_ASSURED=1 rake db:drop
490
+ SAFETY_ASSURED=1 rails db:drop
489
491
  ```
490
492
 
491
493
  ## Faster Migrations
@@ -515,7 +517,9 @@ StrongMigrations.error_messages[:add_column_default] = "Your custom instructions
515
517
 
516
518
  Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
517
519
 
518
- ## Analyze Tables (Postgres)
520
+ ## Postgres-Specific Features
521
+
522
+ ### Analyze Tables
519
523
 
520
524
  Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with:
521
525
 
@@ -523,7 +527,7 @@ Analyze tables automatically (to update planner statistics) after an index is ad
523
527
  StrongMigrations.auto_analyze = true
524
528
  ```
525
529
 
526
- ## Lock Timeout (Postgres)
530
+ ### Lock Timeout
527
531
 
528
532
  It’s a good idea to set a lock timeout for the database user that runs migrations. This way, if migrations can’t acquire a lock in a timely manner, other statements won’t be stuck behind it. Here’s a great explanation of [how lock queues work](https://www.citusdata.com/blog/2018/02/15/when-postgresql-blocks/).
529
533
 
@@ -533,14 +537,20 @@ ALTER ROLE myuser SET lock_timeout = '10s';
533
537
 
534
538
  There’s also [a gem](https://github.com/gocardless/activerecord-safer_migrations) you can use for this.
535
539
 
536
- ## Bigint Primary Keys (Postgres & MySQL)
540
+ ### Target Version
541
+
542
+ If your development database version is different from production, you can specify the production version so the right checks are run in development.
543
+
544
+ ```ruby
545
+ StrongMigrations.target_postgresql_version = 10 # or 9.6, etc
546
+ ```
537
547
 
538
- Rails 5.1+ uses `bigint` for primary keys to keep you from running out of ids. To get this in earlier versions of Rails, check out [rails-bigint-primarykey](https://github.com/Shopify/rails-bigint-primarykey).
548
+ For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
539
549
 
540
550
  ## Additional Reading
541
551
 
542
552
  - [Rails Migrations with No Downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
543
- - [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)
553
+ - [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)
544
554
 
545
555
  ## Credits
546
556
 
@@ -1,5 +1,6 @@
1
1
  require "active_support"
2
2
 
3
+ require "strong_migrations/checker"
3
4
  require "strong_migrations/database_tasks"
4
5
  require "strong_migrations/migration"
5
6
  require "strong_migrations/railtie" if defined?(Rails)
@@ -8,7 +9,7 @@ require "strong_migrations/version"
8
9
 
9
10
  module StrongMigrations
10
11
  class << self
11
- attr_accessor :auto_analyze, :start_after, :checks, :error_messages
12
+ attr_accessor :auto_analyze, :start_after, :checks, :error_messages, :target_postgresql_version
12
13
  end
13
14
  self.auto_analyze = false
14
15
  self.start_after = 0
@@ -113,9 +114,8 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
113
114
  end",
114
115
 
115
116
  add_index_columns:
116
- "Adding an index with more than three columns only helps on extremely large tables.
117
-
118
- If you're sure this is what you want, wrap it in a safety_assured { ... } block.",
117
+ "Adding a non-unique index with more than three columns rarely improves performance.
118
+ Instead, start an index with columns that narrow down the results the most.",
119
119
 
120
120
  change_table:
121
121
  "Strong Migrations does not support inspecting what happens inside a
@@ -0,0 +1,253 @@
1
+ module StrongMigrations
2
+ class Checker
3
+ attr_accessor :direction
4
+
5
+ def initialize(migration)
6
+ @migration = migration
7
+ @new_tables = []
8
+ @safe = false
9
+ end
10
+
11
+ def safety_assured
12
+ previous_value = @safe
13
+ begin
14
+ @safe = true
15
+ yield
16
+ ensure
17
+ @safe = previous_value
18
+ end
19
+ end
20
+
21
+ def perform(method, *args)
22
+ unless safe?
23
+ case method
24
+ when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
25
+ columns =
26
+ case method
27
+ when :remove_timestamps
28
+ ["created_at", "updated_at"]
29
+ when :remove_column
30
+ [args[1].to_s]
31
+ when :remove_columns
32
+ args[1..-1].map(&:to_s)
33
+ else
34
+ options = args[2] || {}
35
+ reference = args[1]
36
+ cols = []
37
+ cols << "#{reference}_type" if options[:polymorphic]
38
+ cols << "#{reference}_id"
39
+ cols
40
+ end
41
+
42
+ code = "self.ignored_columns = #{columns.inspect}"
43
+
44
+ raise_error :remove_column,
45
+ model: args[0].to_s.classify,
46
+ code: code,
47
+ command: command_str(method, args),
48
+ column_suffix: columns.size > 1 ? "s" : ""
49
+ when :change_table
50
+ raise_error :change_table, header: "Possibly dangerous operation"
51
+ when :rename_table
52
+ raise_error :rename_table
53
+ when :rename_column
54
+ raise_error :rename_column
55
+ when :add_index
56
+ table, columns, options = args
57
+ options ||= {}
58
+
59
+ if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
60
+ raise_error :add_index_columns, header: "Best practice"
61
+ end
62
+ if postgresql? && options[:algorithm] != :concurrently && !@new_tables.include?(table.to_s)
63
+ raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
64
+ end
65
+ when :add_column
66
+ table, column, type, options = args
67
+ options ||= {}
68
+ default = options[:default]
69
+
70
+ if !default.nil? && !(postgresql? && postgresql_version >= 110000)
71
+
72
+ if options[:null] == false
73
+ options = options.except(:null)
74
+ append = "
75
+
76
+ Then add the NOT NULL constraint.
77
+
78
+ class %{migration_name}NotNull < ActiveRecord::Migration%{migration_suffix}
79
+ def change
80
+ #{command_str("change_column_null", [table, column, false])}
81
+ end
82
+ end"
83
+ end
84
+
85
+ raise_error :add_column_default,
86
+ add_command: command_str("add_column", [table, column, type, options.except(:default)]),
87
+ change_command: command_str("change_column_default", [table, column, default]),
88
+ remove_command: command_str("remove_column", [table, column]),
89
+ code: backfill_code(table, column, default),
90
+ append: append
91
+ end
92
+
93
+ if type.to_s == "json" && postgresql?
94
+ raise_error :add_column_json
95
+ end
96
+ when :change_column
97
+ table, column, type = args
98
+
99
+ safe = false
100
+ # assume Postgres 9.1+ since previous versions are EOL
101
+ if postgresql? && type.to_s == "text"
102
+ found_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
103
+ safe = found_column && found_column.type == :string
104
+ end
105
+ raise_error :change_column unless safe
106
+ when :create_table
107
+ table, options = args
108
+ options ||= {}
109
+
110
+ raise_error :create_table if options[:force]
111
+
112
+ # keep track of new tables of add_index check
113
+ @new_tables << table.to_s
114
+ when :add_reference, :add_belongs_to
115
+ table, reference, options = args
116
+ options ||= {}
117
+
118
+ index_value = options.fetch(:index, true)
119
+ if postgresql? && index_value
120
+ columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
121
+
122
+ raise_error :add_reference,
123
+ reference_command: command_str(method, [table, reference, options.merge(index: false)]),
124
+ index_command: command_str("add_index", [table, columns, {algorithm: :concurrently}])
125
+ end
126
+ when :execute
127
+ raise_error :execute, header: "Possibly dangerous operation"
128
+ when :change_column_null
129
+ table, column, null, default = args
130
+ if !null && !default.nil?
131
+ raise_error :change_column_null,
132
+ code: backfill_code(table, column, default)
133
+ end
134
+ when :add_foreign_key
135
+ from_table, to_table, options = args
136
+ options ||= {}
137
+ validate = options.fetch(:validate, true)
138
+
139
+ if postgresql?
140
+ if ActiveRecord::VERSION::STRING >= "5.2"
141
+ if validate
142
+ raise_error :add_foreign_key,
143
+ add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
144
+ validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
145
+ end
146
+ else
147
+ # always validated before 5.2
148
+
149
+ # fk name logic from rails
150
+ primary_key = options[:primary_key] || "id"
151
+ column = options[:column] || "#{to_table.to_s.singularize}_id"
152
+ hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10)
153
+ fk_name = options[:name] || "fk_rails_#{hashed_identifier}"
154
+
155
+ raise_error :add_foreign_key,
156
+ add_foreign_key_code: foreign_key_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key]),
157
+ validate_foreign_key_code: foreign_key_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
158
+ end
159
+ end
160
+ end
161
+
162
+ StrongMigrations.checks.each do |check|
163
+ @migration.instance_exec(method, args, &check)
164
+ end
165
+ end
166
+
167
+ result = yield
168
+
169
+ if StrongMigrations.auto_analyze && direction == :up && postgresql? && method == :add_index
170
+ connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
171
+ end
172
+
173
+ result
174
+ end
175
+
176
+ private
177
+
178
+ def connection
179
+ @migration.connection
180
+ end
181
+
182
+ def version
183
+ @migration.version
184
+ end
185
+
186
+ def safe?
187
+ @safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) || direction == :down || version_safe?
188
+ end
189
+
190
+ def version_safe?
191
+ version && version <= StrongMigrations.start_after
192
+ end
193
+
194
+ def postgresql?
195
+ %w(PostgreSQL PostGIS).include?(connection.adapter_name)
196
+ end
197
+
198
+ def postgresql_version
199
+ @postgresql_version ||= begin
200
+ target_version = StrongMigrations.target_postgresql_version
201
+ if target_version && defined?(Rails) && (Rails.env.development? || Rails.env.test?)
202
+ # we only need major version right now
203
+ target_version.to_i * 10000
204
+ else
205
+ connection.execute("SHOW server_version_num").first["server_version_num"].to_i
206
+ end
207
+ end
208
+ end
209
+
210
+ def raise_error(message_key, header: nil, **vars)
211
+ message = StrongMigrations.error_messages[message_key] || "Missing message"
212
+
213
+ vars[:migration_name] = self.class.name
214
+ vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
215
+ vars[:base_model] = "ApplicationRecord"
216
+
217
+ # interpolate variables in appended code
218
+ if vars[:append]
219
+ vars[:append] = vars[:append].gsub(/%(?!{)/, "%%") % vars
220
+ end
221
+
222
+ # escape % not followed by {
223
+ @migration.stop!(message.gsub(/%(?!{)/, "%%") % vars, header: header || "Dangerous operation detected")
224
+ end
225
+
226
+ def foreign_key_str(statement, identifiers)
227
+ # not all identifiers are tables, but this method of quoting should be fine
228
+ code = statement % identifiers.map { |v| connection.quote_table_name(v) }
229
+ "safety_assured do\n execute '#{code}' \n end"
230
+ end
231
+
232
+ def command_str(command, args)
233
+ str_args = args[0..-2].map { |a| a.inspect }
234
+
235
+ # prettier last arg
236
+ last_arg = args[-1]
237
+ if last_arg.is_a?(Hash)
238
+ if last_arg.any?
239
+ str_args << last_arg.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
240
+ end
241
+ else
242
+ str_args << last_arg.inspect
243
+ end
244
+
245
+ "#{command} #{str_args.join(", ")}"
246
+ end
247
+
248
+ def backfill_code(table, column, default)
249
+ model = table.to_s.classify
250
+ "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.1)\n end"
251
+ end
252
+ end
253
+ end
@@ -1,228 +1,25 @@
1
1
  module StrongMigrations
2
2
  module Migration
3
- def safety_assured
4
- previous_value = @safe
5
- @safe = true
6
- yield
7
- ensure
8
- @safe = previous_value
3
+ def initialize(*args)
4
+ super
5
+ @checker = StrongMigrations::Checker.new(self)
9
6
  end
10
7
 
11
8
  def migrate(direction)
12
- @direction = direction
9
+ @checker.direction = direction
13
10
  super
14
11
  end
15
12
 
16
- def method_missing(method, *args, &block)
17
- unless @safe || ENV["SAFETY_ASSURED"] || is_a?(ActiveRecord::Schema) || @direction == :down || version_safe?
18
- case method
19
- when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
20
- columns =
21
- case method
22
- when :remove_timestamps
23
- ["created_at", "updated_at"]
24
- when :remove_column
25
- [args[1].to_s]
26
- when :remove_columns
27
- args[1..-1].map(&:to_s)
28
- else
29
- options = args[2] || {}
30
- reference = args[1]
31
- cols = []
32
- cols << "#{reference}_type" if options[:polymorphic]
33
- cols << "#{reference}_id"
34
- cols
35
- end
36
-
37
- code = "self.ignored_columns = #{columns.inspect}"
38
-
39
- raise_error :remove_column,
40
- model: args[0].to_s.classify,
41
- code: code,
42
- command: command_str(method, args),
43
- column_suffix: columns.size > 1 ? "s" : ""
44
- when :change_table
45
- raise_error :change_table, header: "Possibly dangerous operation"
46
- when :rename_table
47
- raise_error :rename_table
48
- when :rename_column
49
- raise_error :rename_column
50
- when :add_index
51
- table, columns, options = args
52
- options ||= {}
53
-
54
- if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
55
- raise_error :add_index_columns, header: "Best practice"
56
- end
57
- if postgresql? && options[:algorithm] != :concurrently && !@new_tables.to_a.include?(table.to_s)
58
- raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
59
- end
60
- when :add_column
61
- table, column, type, options = args
62
- options ||= {}
63
- default = options[:default]
64
-
65
- if !default.nil? && !(postgresql? && postgresql_version >= 110000)
66
-
67
- if options[:null] == false
68
- options = options.except(:null)
69
- append = "
70
-
71
- Then add the NOT NULL constraint.
72
-
73
- class %{migration_name}NotNull < ActiveRecord::Migration%{migration_suffix}
74
- def change
75
- #{command_str("change_column_null", [table, column, false])}
76
- end
77
- end"
78
- end
79
-
80
- raise_error :add_column_default,
81
- add_command: command_str("add_column", [table, column, type, options.except(:default)]),
82
- change_command: command_str("change_column_default", [table, column, default]),
83
- remove_command: command_str("remove_column", [table, column]),
84
- code: backfill_code(table, column, default),
85
- append: append
86
- end
87
-
88
- if type.to_s == "json" && postgresql?
89
- raise_error :add_column_json
90
- end
91
- when :change_column
92
- table, column, type = args
93
-
94
- safe = false
95
- # assume Postgres 9.1+ since previous versions are EOL
96
- if postgresql? && type.to_s == "text"
97
- found_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
98
- safe = found_column && found_column.type == :string
99
- end
100
- raise_error :change_column unless safe
101
- when :create_table
102
- table, options = args
103
- options ||= {}
104
-
105
- raise_error :create_table if options[:force]
106
-
107
- # keep track of new tables of add_index check
108
- (@new_tables ||= []) << table.to_s
109
- when :add_reference, :add_belongs_to
110
- table, reference, options = args
111
- options ||= {}
112
-
113
- index_value = options.fetch(:index, true)
114
- if postgresql? && index_value
115
- columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
116
-
117
- raise_error :add_reference,
118
- reference_command: command_str(method, [table, reference, options.merge(index: false)]),
119
- index_command: command_str("add_index", [table, columns, {algorithm: :concurrently}])
120
- end
121
- when :execute
122
- raise_error :execute, header: "Possibly dangerous operation"
123
- when :change_column_null
124
- table, column, null, default = args
125
- if !null && !default.nil?
126
- raise_error :change_column_null,
127
- code: backfill_code(table, column, default)
128
- end
129
- when :add_foreign_key
130
- from_table, to_table, options = args
131
- options ||= {}
132
- validate = options.fetch(:validate, true)
133
-
134
- if postgresql?
135
- if ActiveRecord::VERSION::STRING >= "5.2"
136
- if validate
137
- raise_error :add_foreign_key,
138
- add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
139
- validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
140
- end
141
- else
142
- # always validated before 5.2
143
-
144
- # fk name logic from rails
145
- primary_key = options[:primary_key] || "id"
146
- column = options[:column] || "#{to_table.to_s.singularize}_id"
147
- hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10)
148
- fk_name = options[:name] || "fk_rails_#{hashed_identifier}"
149
-
150
- raise_error :add_foreign_key,
151
- add_foreign_key_code: foreign_key_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key]),
152
- validate_foreign_key_code: foreign_key_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
153
- end
154
- end
155
- end
156
-
157
- StrongMigrations.checks.each do |check|
158
- instance_exec(method, args, &check)
159
- end
13
+ def method_missing(method, *args)
14
+ @checker.perform(method, *args) do
15
+ super
160
16
  end
161
-
162
- result = super
163
-
164
- if StrongMigrations.auto_analyze && @direction == :up && postgresql? && method == :add_index
165
- connection.execute "ANALYZE VERBOSE #{connection.quote_table_name(args[0].to_s)}"
166
- end
167
-
168
- result
169
- end
170
-
171
- private
172
-
173
- def postgresql?
174
- %w(PostgreSQL PostGIS).include?(connection.adapter_name)
175
- end
176
-
177
- def postgresql_version
178
- @postgresql_version ||= connection.execute("SHOW server_version_num").first["server_version_num"].to_i
179
- end
180
-
181
- def version_safe?
182
- version && version <= StrongMigrations.start_after
183
- end
184
-
185
- def raise_error(message_key, header: nil, **vars)
186
- message = StrongMigrations.error_messages[message_key] || "Missing message"
187
-
188
- vars[:migration_name] = self.class.name
189
- vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
190
- vars[:base_model] = "ApplicationRecord"
191
-
192
- # interpolate variables in appended code
193
- if vars[:append]
194
- vars[:append] = vars[:append].gsub(/%(?!{)/, "%%") % vars
195
- end
196
-
197
- # escape % not followed by {
198
- stop!(message.gsub(/%(?!{)/, "%%") % vars, header: header || "Dangerous operation detected")
199
- end
200
-
201
- def foreign_key_str(statement, identifiers)
202
- # not all identifiers are tables, but this method of quoting should be fine
203
- code = statement % identifiers.map { |v| connection.quote_table_name(v) }
204
- "safety_assured do\n execute '#{code}' \n end"
205
17
  end
206
18
 
207
- def command_str(command, args)
208
- str_args = args[0..-2].map { |a| a.inspect }
209
-
210
- # prettier last arg
211
- last_arg = args[-1]
212
- if last_arg.is_a?(Hash)
213
- if last_arg.any?
214
- str_args << last_arg.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
215
- end
216
- else
217
- str_args << last_arg.inspect
19
+ def safety_assured
20
+ @checker.safety_assured do
21
+ yield
218
22
  end
219
-
220
- "#{command} #{str_args.join(", ")}"
221
- end
222
-
223
- def backfill_code(table, column, default)
224
- model = table.to_s.classify
225
- "#{model}.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.1)\n end"
226
23
  end
227
24
 
228
25
  def stop!(message, header: "Custom check")
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -1,4 +1,4 @@
1
- # http://nithinbekal.com/posts/safe-rake-tasks
1
+ # https://nithinbekal.com/posts/safe-rake-tasks
2
2
 
3
3
  namespace :strong_migrations do
4
4
  task safety_assured: :environment do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strong_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2019-05-27 00:00:00.000000000 Z
13
+ date: 2019-07-12 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -96,6 +96,7 @@ files:
96
96
  - README.md
97
97
  - lib/strong_migrations.rb
98
98
  - lib/strong_migrations/alphabetize_columns.rb
99
+ - lib/strong_migrations/checker.rb
99
100
  - lib/strong_migrations/database_tasks.rb
100
101
  - lib/strong_migrations/migration.rb
101
102
  - lib/strong_migrations/railtie.rb
@@ -121,8 +122,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
122
  - !ruby/object:Gem::Version
122
123
  version: '0'
123
124
  requirements: []
124
- rubygems_version: 3.0.3
125
+ rubygems_version: 3.0.4
125
126
  signing_key:
126
127
  specification_version: 4
127
- summary: Catch unsafe migrations at dev time
128
+ summary: Catch unsafe migrations in development
128
129
  test_files: []