strong_migrations 0.8.0 → 1.2.0

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.
@@ -0,0 +1,421 @@
1
+ # TODO better pattern
2
+ module StrongMigrations
3
+ module Checks
4
+ private
5
+
6
+ def check_add_check_constraint(*args)
7
+ options = args.extract_options!
8
+ table, expression = args
9
+
10
+ if !new_table?(table)
11
+ if postgresql? && options[:validate] != false
12
+ add_options = options.merge(validate: false)
13
+ name = options[:name] || @migration.check_constraint_options(table, expression, options)[:name]
14
+ validate_options = {name: name}
15
+
16
+ if StrongMigrations.safe_by_default
17
+ safe_add_check_constraint(*args, add_options, validate_options)
18
+ throw :safe
19
+ end
20
+
21
+ raise_error :add_check_constraint,
22
+ add_check_constraint_code: command_str("add_check_constraint", [table, expression, add_options]),
23
+ validate_check_constraint_code: command_str("validate_check_constraint", [table, validate_options])
24
+ elsif mysql? || mariadb?
25
+ raise_error :add_check_constraint_mysql
26
+ end
27
+ end
28
+ end
29
+
30
+ def check_add_column(*args)
31
+ options = args.extract_options!
32
+ table, column, type = args
33
+ default = options[:default]
34
+
35
+ if !default.nil? && !adapter.add_column_default_safe?
36
+ if options[:null] == false
37
+ options = options.except(:null)
38
+ append = "
39
+
40
+ Then add the NOT NULL constraint in separate migrations."
41
+ end
42
+
43
+ raise_error :add_column_default,
44
+ add_command: command_str("add_column", [table, column, type, options.except(:default)]),
45
+ change_command: command_str("change_column_default", [table, column, default]),
46
+ remove_command: command_str("remove_column", [table, column]),
47
+ code: backfill_code(table, column, default),
48
+ append: append,
49
+ rewrite_blocks: adapter.rewrite_blocks
50
+ elsif default.is_a?(Proc) && postgresql?
51
+ # adding a column with a VOLATILE default is not safe
52
+ # https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
53
+ # functions like random() and clock_timestamp() are VOLATILE
54
+ # check for Proc to match Active Record
55
+ raise_error :add_column_default_callable
56
+ end
57
+
58
+ if type.to_s == "json" && postgresql?
59
+ raise_error :add_column_json,
60
+ command: command_str("add_column", [table, column, :jsonb, options])
61
+ end
62
+ end
63
+
64
+ # unlike add_index, we don't make an exception here for new tables
65
+ #
66
+ # with add_index, it's fine to lock a new table even after inserting data
67
+ # since the table won't be in use by the application
68
+ #
69
+ # with add_foreign_key, this would cause issues since it locks the referenced table
70
+ #
71
+ # it's okay to allow if the table is empty, but not a fan of data-dependent checks,
72
+ # since the data in production could be different from development
73
+ #
74
+ # note: adding foreign_keys with create_table is fine
75
+ # since the table is always guaranteed to be empty
76
+ def check_add_foreign_key(*args)
77
+ options = args.extract_options!
78
+ from_table, to_table = args
79
+
80
+ validate = options.fetch(:validate, true)
81
+ if postgresql? && validate
82
+ if StrongMigrations.safe_by_default
83
+ safe_add_foreign_key(*args, **options)
84
+ throw :safe
85
+ end
86
+
87
+ raise_error :add_foreign_key,
88
+ add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
89
+ validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
90
+ end
91
+ end
92
+
93
+ def check_add_index(*args)
94
+ options = args.extract_options!
95
+ table, columns = args
96
+
97
+ if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
98
+ raise_error :add_index_columns, header: "Best practice"
99
+ end
100
+
101
+ # safe_by_default goes through this path as well
102
+ if postgresql? && options[:algorithm] == :concurrently && adapter.index_corruption?
103
+ raise_error :add_index_corruption
104
+ end
105
+
106
+ # safe to add non-concurrently to new tables (even after inserting data)
107
+ # since the table won't be in use by the application
108
+ if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
109
+ if StrongMigrations.safe_by_default
110
+ safe_add_index(*args, **options)
111
+ throw :safe
112
+ end
113
+
114
+ raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
115
+ end
116
+ end
117
+
118
+ def check_add_reference(method, *args)
119
+ options = args.extract_options!
120
+ table, reference = args
121
+
122
+ if postgresql?
123
+ index_value = options.fetch(:index, true)
124
+ concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
125
+ bad_index = index_value && !concurrently_set
126
+
127
+ if bad_index || options[:foreign_key]
128
+ if index_value.is_a?(Hash)
129
+ options[:index] = options[:index].merge(algorithm: :concurrently)
130
+ else
131
+ options = options.merge(index: {algorithm: :concurrently})
132
+ end
133
+
134
+ if StrongMigrations.safe_by_default
135
+ safe_add_reference(*args, **options)
136
+ throw :safe
137
+ end
138
+
139
+ if options.delete(:foreign_key)
140
+ headline = "Adding a foreign key blocks writes on both tables."
141
+ append = "
142
+
143
+ Then add the foreign key in separate migrations."
144
+ else
145
+ headline = "Adding an index non-concurrently locks the table."
146
+ end
147
+
148
+ raise_error :add_reference,
149
+ headline: headline,
150
+ command: command_str(method, [table, reference, options]),
151
+ append: append
152
+ end
153
+ end
154
+ end
155
+
156
+ def check_change_column(*args)
157
+ options = args.extract_options!
158
+ table, column, type = args
159
+
160
+ safe = false
161
+ existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
162
+ if existing_column
163
+ existing_type = existing_column.sql_type.sub(/\(\d+(,\d+)?\)/, "")
164
+ safe = adapter.change_type_safe?(table, column, type, options, existing_column, existing_type)
165
+ end
166
+
167
+ # unsafe to set NOT NULL for safe types with Postgres
168
+ # TODO check if safe for MySQL and MariaDB
169
+ if safe && existing_column.null && options[:null] == false
170
+ raise_error :change_column_with_not_null
171
+ end
172
+
173
+ raise_error :change_column, rewrite_blocks: adapter.rewrite_blocks unless safe
174
+ end
175
+
176
+ def check_change_column_null(*args)
177
+ table, column, null, default = args
178
+ if !null
179
+ if postgresql?
180
+ safe = false
181
+ safe_with_check_constraint = adapter.server_version >= Gem::Version.new("12")
182
+ if safe_with_check_constraint
183
+ safe = adapter.constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
184
+ end
185
+
186
+ unless safe
187
+ # match https://github.com/nullobject/rein
188
+ constraint_name = "#{table}_#{column}_null"
189
+
190
+ add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
191
+ validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
192
+ remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
193
+
194
+ constraint_methods = ar_version >= 6.1
195
+
196
+ validate_constraint_code =
197
+ if constraint_methods
198
+ String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
199
+ else
200
+ String.new(safety_assured_str(validate_code))
201
+ end
202
+
203
+ if safe_with_check_constraint
204
+ change_args = [table, column, null]
205
+
206
+ validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
207
+
208
+ if constraint_methods
209
+ validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
210
+ else
211
+ validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
212
+ end
213
+ end
214
+
215
+ if StrongMigrations.safe_by_default
216
+ safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
217
+ throw :safe
218
+ end
219
+
220
+ add_constraint_code =
221
+ if constraint_methods
222
+ # only quote when needed
223
+ expr_column = column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
224
+ command_str(:add_check_constraint, [table, "#{expr_column} IS NOT NULL", {name: constraint_name, validate: false}])
225
+ else
226
+ safety_assured_str(add_code)
227
+ end
228
+
229
+ raise_error :change_column_null_postgresql,
230
+ add_constraint_code: add_constraint_code,
231
+ validate_constraint_code: "def change\n #{validate_constraint_code}\n end"
232
+ end
233
+ elsif mysql? || mariadb?
234
+ unless adapter.strict_mode?
235
+ raise_error :change_column_null_mysql
236
+ end
237
+ end
238
+
239
+ if !default.nil?
240
+ raise_error :change_column_null,
241
+ code: backfill_code(table, column, default)
242
+ end
243
+ end
244
+ end
245
+
246
+ def check_change_table
247
+ raise_error :change_table, header: "Possibly dangerous operation"
248
+ end
249
+
250
+ def check_create_join_table(*args)
251
+ options = args.extract_options!
252
+
253
+ raise_error :create_table if options[:force]
254
+
255
+ # TODO keep track of new table of add_index check
256
+ end
257
+
258
+ def check_create_table(*args)
259
+ options = args.extract_options!
260
+ table, _ = args
261
+
262
+ raise_error :create_table if options[:force]
263
+
264
+ # keep track of new table of add_index check
265
+ @new_tables << table.to_s
266
+ end
267
+
268
+ def check_execute
269
+ raise_error :execute, header: "Possibly dangerous operation"
270
+ end
271
+
272
+ def check_remove_column(method, *args)
273
+ columns =
274
+ case method
275
+ when :remove_timestamps
276
+ ["created_at", "updated_at"]
277
+ when :remove_column
278
+ [args[1].to_s]
279
+ when :remove_columns
280
+ # Active Record 6.1+ supports options
281
+ if args.last.is_a?(Hash)
282
+ args[1..-2].map(&:to_s)
283
+ else
284
+ args[1..-1].map(&:to_s)
285
+ end
286
+ else
287
+ options = args[2] || {}
288
+ reference = args[1]
289
+ cols = []
290
+ cols << "#{reference}_type" if options[:polymorphic]
291
+ cols << "#{reference}_id"
292
+ cols
293
+ end
294
+
295
+ code = "self.ignored_columns = #{columns.inspect}"
296
+
297
+ raise_error :remove_column,
298
+ model: args[0].to_s.classify,
299
+ code: code,
300
+ command: command_str(method, args),
301
+ column_suffix: columns.size > 1 ? "s" : ""
302
+ end
303
+
304
+ def check_remove_index(*args)
305
+ options = args.extract_options!
306
+ table, _ = args
307
+
308
+ if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
309
+ # avoid suggesting extra (invalid) args
310
+ args = args[0..1] unless StrongMigrations.safe_by_default
311
+
312
+ # Active Record < 6.1 only supports two arguments (including options)
313
+ if args.size == 2 && ar_version < 6.1
314
+ # arg takes precedence over option
315
+ options[:column] = args.pop
316
+ end
317
+
318
+ if StrongMigrations.safe_by_default
319
+ safe_remove_index(*args, **options)
320
+ throw :safe
321
+ end
322
+
323
+ raise_error :remove_index, command: command_str("remove_index", args + [options.merge(algorithm: :concurrently)])
324
+ end
325
+ end
326
+
327
+ def check_rename_column
328
+ raise_error :rename_column
329
+ end
330
+
331
+ def check_rename_table
332
+ raise_error :rename_table
333
+ end
334
+
335
+ def check_validate_check_constraint
336
+ if postgresql? && adapter.writes_blocked?
337
+ raise_error :validate_check_constraint
338
+ end
339
+ end
340
+
341
+ def check_validate_foreign_key
342
+ if postgresql? && adapter.writes_blocked?
343
+ raise_error :validate_foreign_key
344
+ end
345
+ end
346
+
347
+ # helpers
348
+
349
+ def postgresql?
350
+ adapter.instance_of?(Adapters::PostgreSQLAdapter)
351
+ end
352
+
353
+ def mysql?
354
+ adapter.instance_of?(Adapters::MySQLAdapter)
355
+ end
356
+
357
+ def mariadb?
358
+ adapter.instance_of?(Adapters::MariaDBAdapter)
359
+ end
360
+
361
+ def ar_version
362
+ ActiveRecord::VERSION::STRING.to_f
363
+ end
364
+
365
+ def raise_error(message_key, header: nil, append: nil, **vars)
366
+ return unless StrongMigrations.check_enabled?(message_key, version: version)
367
+
368
+ message = StrongMigrations.error_messages[message_key] || "Missing message"
369
+ message = message + append if append
370
+
371
+ vars[:migration_name] = @migration.class.name
372
+ vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
373
+ vars[:base_model] = "ApplicationRecord"
374
+
375
+ # escape % not followed by {
376
+ message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
377
+ @migration.stop!(message, header: header || "Dangerous operation detected")
378
+ end
379
+
380
+ def constraint_str(statement, identifiers)
381
+ # not all identifiers are tables, but this method of quoting should be fine
382
+ statement % identifiers.map { |v| connection.quote_table_name(v) }
383
+ end
384
+
385
+ def safety_assured_str(code)
386
+ "safety_assured do\n execute '#{code}' \n end"
387
+ end
388
+
389
+ def command_str(command, args)
390
+ str_args = args[0..-2].map { |a| a.inspect }
391
+
392
+ # prettier last arg
393
+ last_arg = args[-1]
394
+ if last_arg.is_a?(Hash)
395
+ if last_arg.any?
396
+ str_args << last_arg.map do |k, v|
397
+ if v.is_a?(Hash)
398
+ # pretty index: {algorithm: :concurrently}
399
+ "#{k}: {#{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(", ")}}"
400
+ else
401
+ "#{k}: #{v.inspect}"
402
+ end
403
+ end.join(", ")
404
+ end
405
+ else
406
+ str_args << last_arg.inspect
407
+ end
408
+
409
+ "#{command} #{str_args.join(", ")}"
410
+ end
411
+
412
+ def backfill_code(table, column, default)
413
+ model = table.to_s.classify
414
+ "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
415
+ end
416
+
417
+ def new_table?(table)
418
+ @new_tables.include?(table.to_s)
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,227 @@
1
+ module StrongMigrations
2
+ self.error_messages = {
3
+ add_column_default:
4
+ "Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
5
+ Instead, add the column without a default value, then change the default.
6
+
7
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
8
+ def up
9
+ %{add_command}
10
+ %{change_command}
11
+ end
12
+
13
+ def down
14
+ %{remove_command}
15
+ end
16
+ end
17
+
18
+ Then backfill the existing rows in the Rails console or a separate migration with disable_ddl_transaction!.
19
+
20
+ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
21
+ disable_ddl_transaction!
22
+
23
+ def up
24
+ %{code}
25
+ end
26
+ end",
27
+
28
+ add_column_default_callable:
29
+ "Strong Migrations does not support inspecting callable default values.
30
+ Please make really sure you're not calling a VOLATILE function,
31
+ then wrap it in a safety_assured { ... } block.",
32
+
33
+ add_column_json:
34
+ "There's no equality operator for the json column type, which can cause errors for
35
+ existing SELECT DISTINCT queries in your application. Use jsonb instead.
36
+
37
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
38
+ def change
39
+ %{command}
40
+ end
41
+ end",
42
+
43
+ change_column:
44
+ "Changing the type of an existing column blocks %{rewrite_blocks}
45
+ while the entire table is rewritten. A safer approach is to:
46
+
47
+ 1. Create a new column
48
+ 2. Write to both columns
49
+ 3. Backfill data from the old column to the new column
50
+ 4. Move reads from the old column to the new column
51
+ 5. Stop writing to the old column
52
+ 6. Drop the old column",
53
+
54
+ change_column_with_not_null:
55
+ "Changing the type is safe, but setting NOT NULL is not.",
56
+
57
+ remove_column: "Active Record caches attributes, which causes problems
58
+ when removing columns. Be sure to ignore the column%{column_suffix}:
59
+
60
+ class %{model} < %{base_model}
61
+ %{code}
62
+ end
63
+
64
+ Deploy the code, then wrap this step in a safety_assured { ... } block.
65
+
66
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
67
+ def change
68
+ safety_assured { %{command} }
69
+ end
70
+ end",
71
+
72
+ rename_column:
73
+ "Renaming a column that's in use will cause errors
74
+ in your application. A safer approach is to:
75
+
76
+ 1. Create a new column
77
+ 2. Write to both columns
78
+ 3. Backfill data from the old column to new column
79
+ 4. Move reads from the old column to the new column
80
+ 5. Stop writing to the old column
81
+ 6. Drop the old column",
82
+
83
+ rename_table:
84
+ "Renaming a table that's in use will cause errors
85
+ in your application. A safer approach is to:
86
+
87
+ 1. Create a new table. Don't forget to recreate indexes from the old table
88
+ 2. Write to both tables
89
+ 3. Backfill data from the old table to new table
90
+ 4. Move reads from the old table to the new table
91
+ 5. Stop writing to the old table
92
+ 6. Drop the old table",
93
+
94
+ add_reference:
95
+ "%{headline} Instead, use:
96
+
97
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
98
+ disable_ddl_transaction!
99
+
100
+ def change
101
+ %{command}
102
+ end
103
+ end",
104
+
105
+ add_index:
106
+ "Adding an index non-concurrently blocks writes. Instead, use:
107
+
108
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
109
+ disable_ddl_transaction!
110
+
111
+ def change
112
+ %{command}
113
+ end
114
+ end",
115
+
116
+ remove_index:
117
+ "Removing an index non-concurrently blocks writes. Instead, use:
118
+
119
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
120
+ disable_ddl_transaction!
121
+
122
+ def change
123
+ %{command}
124
+ end
125
+ end",
126
+
127
+ add_index_columns:
128
+ "Adding a non-unique index with more than three columns rarely improves performance.
129
+ Instead, start an index with columns that narrow down the results the most.",
130
+
131
+ add_index_corruption:
132
+ "Adding an index concurrently can cause silent data corruption in Postgres 14.0 to 14.3.
133
+ Upgrade Postgres before adding new indexes, or wrap this step in a safety_assured { ... } block
134
+ to accept the risk.",
135
+
136
+ change_table:
137
+ "Strong Migrations does not support inspecting what happens inside a
138
+ change_table block, so cannot help you here. Please make really sure that what
139
+ you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
140
+
141
+ create_table:
142
+ "The force option will destroy existing tables.
143
+ If this is intended, drop the existing table first.
144
+ Otherwise, remove the force option.",
145
+
146
+ execute:
147
+ "Strong Migrations does not support inspecting what happens inside an
148
+ execute call, so cannot help you here. Please make really sure that what
149
+ you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
150
+
151
+ change_column_null:
152
+ "Passing a default value to change_column_null runs a single UPDATE query,
153
+ which can cause downtime. Instead, backfill the existing rows in the
154
+ Rails console or a separate migration with disable_ddl_transaction!.
155
+
156
+ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
157
+ disable_ddl_transaction!
158
+
159
+ def up
160
+ %{code}
161
+ end
162
+ end",
163
+
164
+ change_column_null_postgresql:
165
+ "Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
166
+ Instead, add a check constraint and validate it in a separate migration.
167
+
168
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
169
+ def change
170
+ %{add_constraint_code}
171
+ end
172
+ end
173
+
174
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
175
+ %{validate_constraint_code}
176
+ end",
177
+
178
+ change_column_null_mysql:
179
+ "Setting NOT NULL on an existing column is not safe without strict mode enabled.",
180
+
181
+ add_foreign_key:
182
+ "Adding a foreign key blocks writes on both tables. Instead,
183
+ add the foreign key without validating existing rows,
184
+ then validate them in a separate migration.
185
+
186
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
187
+ def change
188
+ %{add_foreign_key_code}
189
+ end
190
+ end
191
+
192
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
193
+ def change
194
+ %{validate_foreign_key_code}
195
+ end
196
+ end",
197
+
198
+ validate_foreign_key:
199
+ "Validating a foreign key while writes are blocked is dangerous.
200
+ Use disable_ddl_transaction! or a separate migration.",
201
+
202
+ add_check_constraint:
203
+ "Adding a check constraint key blocks reads and writes while every row is checked.
204
+ Instead, add the check constraint without validating existing rows,
205
+ then validate them in a separate migration.
206
+
207
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
208
+ def change
209
+ %{add_check_constraint_code}
210
+ end
211
+ end
212
+
213
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
214
+ def change
215
+ %{validate_check_constraint_code}
216
+ end
217
+ end",
218
+
219
+ add_check_constraint_mysql:
220
+ "Adding a check constraint to an existing table is not safe with your database engine.",
221
+
222
+ validate_check_constraint:
223
+ "Validating a check constraint while writes are blocked is dangerous.
224
+ Use disable_ddl_transaction! or a separate migration."
225
+ }
226
+ self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
227
+ end
@@ -12,8 +12,10 @@ module StrongMigrations
12
12
  # Active Record 7.0.2+ versioned schema
13
13
  return super if defined?(ActiveRecord::Schema::Definition) && is_a?(ActiveRecord::Schema::Definition)
14
14
 
15
- strong_migrations_checker.perform(method, *args) do
16
- super
15
+ catch(:safe) do
16
+ strong_migrations_checker.perform(method, *args) do
17
+ super
18
+ end
17
19
  end
18
20
  end
19
21
  ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
@@ -0,0 +1,19 @@
1
+ module StrongMigrations
2
+ module Migrator
3
+ def ddl_transaction(migration, *args)
4
+ return super unless StrongMigrations.lock_timeout_retries > 0 && use_transaction?(migration)
5
+
6
+ # handle MigrationProxy class
7
+ migration = migration.send(:migration) if migration.respond_to?(:migration, true)
8
+
9
+ # retry migration since the entire transaction needs to be rerun
10
+ checker = migration.send(:strong_migrations_checker)
11
+ checker.retry_lock_timeouts(check_committed: true) do
12
+ # failed transaction reverts timeout, so need to re-apply
13
+ checker.timeouts_set = false
14
+
15
+ super(migration, *args)
16
+ end
17
+ end
18
+ end
19
+ end