strong_migrations 0.4.0 → 0.4.1

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
  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: []