strong_migrations 1.8.0 → 2.3.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.
@@ -8,7 +8,7 @@ module StrongMigrations
8
8
  end
9
9
 
10
10
  def min_version
11
- "5.7"
11
+ "8.0"
12
12
  end
13
13
 
14
14
  def server_version
@@ -65,9 +65,10 @@ module StrongMigrations
65
65
  sql = <<~SQL
66
66
  SELECT cs.MAXLEN
67
67
  FROM INFORMATION_SCHEMA.CHARACTER_SETS cs
68
- INNER JOIN INFORMATION_SCHEMA.COLLATIONS c ON c.CHARACTER_SET_NAME = cs.CHARACTER_SET_NAME
69
- INNER JOIN INFORMATION_SCHEMA.TABLES t ON t.TABLE_COLLATION = c.COLLATION_NAME
70
- WHERE t.TABLE_SCHEMA = database() AND t.TABLE_NAME = #{connection.quote(table)}
68
+ INNER JOIN INFORMATION_SCHEMA.COLUMNS c ON c.CHARACTER_SET_NAME = cs.CHARACTER_SET_NAME
69
+ WHERE c.TABLE_SCHEMA = database() AND
70
+ c.TABLE_NAME = #{connection.quote(table)} AND
71
+ c.COLUMN_NAME = #{connection.quote(column)}
71
72
  SQL
72
73
  row = connection.select_all(sql).first
73
74
  if row
@@ -91,6 +92,10 @@ module StrongMigrations
91
92
  "writes"
92
93
  end
93
94
 
95
+ def max_constraint_name_length
96
+ 64
97
+ end
98
+
94
99
  private
95
100
 
96
101
  # do not memoize
@@ -6,7 +6,7 @@ module StrongMigrations
6
6
  end
7
7
 
8
8
  def min_version
9
- "10"
9
+ "12"
10
10
  end
11
11
 
12
12
  def server_version
@@ -42,7 +42,7 @@ module StrongMigrations
42
42
  end
43
43
 
44
44
  def add_column_default_safe?
45
- server_version >= Gem::Version.new("11")
45
+ true
46
46
  end
47
47
 
48
48
  def change_type_safe?(table, column, type, options, existing_column, existing_type)
@@ -95,7 +95,7 @@ module StrongMigrations
95
95
  "timestamp" => "timestamp without time zone",
96
96
  "timestamptz" => "timestamp with time zone"
97
97
  }
98
- maybe_safe = type_map.values.include?(existing_type) && precision >= existing_precision
98
+ maybe_safe = type_map.value?(existing_type) && precision >= existing_precision
99
99
 
100
100
  if maybe_safe
101
101
  new_type = type.to_s == "datetime" ? datetime_type : type.to_s
@@ -103,7 +103,7 @@ module StrongMigrations
103
103
  # resolve with fallback
104
104
  new_type = type_map[new_type] || new_type
105
105
 
106
- safe = new_type == existing_type || (server_version >= Gem::Version.new("12") && time_zone == "UTC")
106
+ safe = new_type == existing_type || time_zone == "UTC"
107
107
  end
108
108
  when "time"
109
109
  precision = options[:precision] || options[:limit] || 6
@@ -127,19 +127,19 @@ module StrongMigrations
127
127
  safe
128
128
  end
129
129
 
130
- def constraints(table_name)
130
+ # TODO remove when Active Record < 7.1 is no longer supported
131
+ def index_invalid?(table_name, index_name)
131
132
  query = <<~SQL
132
133
  SELECT
133
- conname AS name,
134
- pg_get_constraintdef(oid) AS def
134
+ indisvalid
135
135
  FROM
136
- pg_constraint
136
+ pg_index
137
137
  WHERE
138
- contype = 'c' AND
139
- convalidated AND
140
- conrelid = #{connection.quote(connection.quote_table_name(table_name))}::regclass
138
+ indrelid = to_regclass(#{connection.quote(connection.quote_table_name(table_name))}) AND
139
+ indexrelid = to_regclass(#{connection.quote(connection.quote_table_name(index_name))}) AND
140
+ indisvalid = false
141
141
  SQL
142
- select_all(query.squish).to_a
142
+ select_all(query.squish).any?
143
143
  end
144
144
 
145
145
  def writes_blocked?
@@ -173,6 +173,15 @@ module StrongMigrations
173
173
  ["primary_key", "serial", "bigserial"]
174
174
  end
175
175
 
176
+ def max_constraint_name_length
177
+ 63
178
+ end
179
+
180
+ def constraints(table, column)
181
+ # TODO improve column check
182
+ connection.check_constraints(table).select { |c| /\b#{Regexp.escape(column.to_s)}\b/.match?(c.expression) }
183
+ end
184
+
176
185
  private
177
186
 
178
187
  def set_timeout(setting, timeout)
@@ -210,15 +219,9 @@ module StrongMigrations
210
219
  end
211
220
 
212
221
  def datetime_type
213
- key =
214
- if ActiveRecord::VERSION::MAJOR >= 7
215
- # https://github.com/rails/rails/pull/41084
216
- # no need to support custom datetime_types
217
- connection.class.datetime_type
218
- else
219
- # https://github.com/rails/rails/issues/21126#issuecomment-327895275
220
- :datetime
221
- end
222
+ # https://github.com/rails/rails/pull/41084
223
+ # no need to support custom datetime_types
224
+ key = connection.class.datetime_type
222
225
 
223
226
  # could be timestamp, timestamp without time zone, timestamp with time zone, etc
224
227
  connection.class.const_get(:NATIVE_DATABASE_TYPES).fetch(key).fetch(:name)
@@ -11,10 +11,16 @@ module StrongMigrations
11
11
 
12
12
  def initialize(migration)
13
13
  @migration = migration
14
+ reset
15
+ end
16
+
17
+ def reset
14
18
  @new_tables = []
15
19
  @new_columns = []
16
20
  @timeouts_set = false
17
21
  @committed = false
22
+ @transaction_disabled = false
23
+ @skip_retries = false
18
24
  end
19
25
 
20
26
  def self.safety_assured
@@ -27,7 +33,10 @@ module StrongMigrations
27
33
  end
28
34
  end
29
35
 
30
- def perform(method, *args)
36
+ def perform(method, *args, &block)
37
+ return yield if skip?
38
+
39
+ check_adapter
31
40
  check_version_supported
32
41
  set_timeouts
33
42
  check_lock_timeout
@@ -96,9 +105,9 @@ module StrongMigrations
96
105
  # TODO figure out how to handle methods that generate multiple statements
97
106
  # like add_reference(table, ref, index: {algorithm: :concurrently})
98
107
  # lock timeout after first statement will cause retry to fail
99
- retry_lock_timeouts { yield }
108
+ retry_lock_timeouts { perform_method(method, *args, &block) }
100
109
  else
101
- yield
110
+ perform_method(method, *args, &block)
102
111
  end
103
112
 
104
113
  # outdated statistics + a new index can hurt performance of existing queries
@@ -109,6 +118,13 @@ module StrongMigrations
109
118
  result
110
119
  end
111
120
 
121
+ def perform_method(method, *args)
122
+ if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
123
+ remove_invalid_index_if_needed(*args)
124
+ end
125
+ yield
126
+ end
127
+
112
128
  def retry_lock_timeouts(check_committed: false)
113
129
  retries = 0
114
130
  begin
@@ -129,8 +145,22 @@ module StrongMigrations
129
145
  version && version <= StrongMigrations.start_after
130
146
  end
131
147
 
148
+ def skip?
149
+ StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
150
+ end
151
+
132
152
  private
133
153
 
154
+ def check_adapter
155
+ return if defined?(@adapter_checked)
156
+
157
+ if adapter.instance_of?(Adapters::AbstractAdapter)
158
+ warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
159
+ end
160
+
161
+ @adapter_checked = true
162
+ end
163
+
134
164
  def check_version_supported
135
165
  return if defined?(@version_checked)
136
166
 
@@ -200,12 +230,48 @@ module StrongMigrations
200
230
  @migration.connection
201
231
  end
202
232
 
233
+ def db_config_name
234
+ connection.pool.db_config.name
235
+ end
236
+
203
237
  def retry_lock_timeouts?(method)
204
238
  (
205
239
  StrongMigrations.lock_timeout_retries > 0 &&
206
240
  !in_transaction? &&
207
- method != :transaction
241
+ method != :transaction &&
242
+ !@skip_retries
208
243
  )
209
244
  end
245
+
246
+ def without_retries
247
+ previous_value = @skip_retries
248
+ begin
249
+ @skip_retries = true
250
+ yield
251
+ ensure
252
+ @skip_retries = previous_value
253
+ end
254
+ end
255
+
256
+ # REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
257
+ def remove_invalid_index_if_needed(*args)
258
+ options = args.extract_options!
259
+
260
+ # ensures has same options as existing index
261
+ # check args to avoid errors with index_exists?
262
+ return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
263
+
264
+ table, columns = args
265
+ index_name = options.fetch(:name, connection.index_name(table, columns))
266
+
267
+ # valid option is ignored for Active Record < 7.1, so check name as well
268
+ return if ar_version < 7.1 && !adapter.index_invalid?(table, index_name)
269
+
270
+ @migration.say("Attempting to remove invalid index")
271
+ without_retries do
272
+ # TODO pass index schema for extra safety?
273
+ @migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
274
+ end
275
+ end
210
276
  end
211
277
  end
@@ -10,7 +10,7 @@ module StrongMigrations
10
10
  if !new_table?(table)
11
11
  if postgresql? && options[:validate] != false
12
12
  add_options = options.merge(validate: false)
13
- name = options[:name] || @migration.check_constraint_options(table, expression, options)[:name]
13
+ name = options[:name] || connection.check_constraint_options(table, expression, options)[:name]
14
14
  validate_options = {name: name}
15
15
 
16
16
  if StrongMigrations.safe_by_default
@@ -39,27 +39,20 @@ module StrongMigrations
39
39
  #
40
40
  # Also, Active Record has special case for uuid columns that allows function default values
41
41
  # https://github.com/rails/rails/blob/v7.0.3.1/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L92-L93
42
- if options.key?(:default) && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
42
+ if !default.nil? && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
43
43
  if options[:null] == false
44
44
  options = options.except(:null)
45
45
  append = "\n\nThen add the NOT NULL constraint in separate migrations."
46
46
  end
47
47
 
48
- if default.nil?
49
- raise_error :add_column_default_null,
50
- command: command_str("add_column", [table, column, type, options.except(:default)]),
51
- append: append,
52
- rewrite_blocks: adapter.rewrite_blocks
53
- else
54
- raise_error :add_column_default,
55
- add_command: command_str("add_column", [table, column, type, options.except(:default)]),
56
- change_command: command_str("change_column_default", [table, column, default]),
57
- remove_command: command_str("remove_column", [table, column]),
58
- code: backfill_code(table, column, default, volatile),
59
- append: append,
60
- rewrite_blocks: adapter.rewrite_blocks,
61
- default_type: (volatile ? "volatile" : "non-null")
62
- end
48
+ raise_error :add_column_default,
49
+ add_command: command_str("add_column", [table, column, type, options.except(:default)]),
50
+ change_command: command_str("change_column_default", [table, column, default]),
51
+ remove_command: command_str("remove_column", [table, column]),
52
+ code: backfill_code(table, column, default, volatile),
53
+ append: append,
54
+ rewrite_blocks: adapter.rewrite_blocks,
55
+ default_type: (volatile ? "volatile" : "non-null")
63
56
  elsif default.is_a?(Proc) && postgresql?
64
57
  # adding a column with a VOLATILE default is not safe
65
58
  # https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
@@ -217,17 +210,43 @@ module StrongMigrations
217
210
  end
218
211
 
219
212
  raise_error :change_column, rewrite_blocks: adapter.rewrite_blocks unless safe
213
+
214
+ # constraints must be rechecked
215
+ # Postgres recommends dropping constraints before and adding them back
216
+ # https://www.postgresql.org/docs/current/ddl-alter.html#DDL-ALTER-COLUMN-TYPE
217
+ if postgresql?
218
+ constraints = adapter.constraints(table, column)
219
+ if constraints.any?
220
+ change_commands = []
221
+ constraints.each do |c|
222
+ change_commands << command_str(:remove_check_constraint, [table, c.expression, {name: c.name}])
223
+ end
224
+ change_commands << command_str(:change_column, args + [options])
225
+ constraints.each do |c|
226
+ change_commands << command_str(:add_check_constraint, [table, c.expression, {name: c.name, validate: false}])
227
+ end
228
+
229
+ validate_commands = []
230
+ constraints.each do |c|
231
+ validate_commands << command_str(:validate_check_constraint, [table, {name: c.name}])
232
+ end
233
+
234
+ raise_error :change_column_constraint,
235
+ change_column_code: change_commands.join("\n "),
236
+ validate_constraint_code: validate_commands.join("\n ")
237
+ end
238
+ end
220
239
  end
221
240
 
222
241
  def check_change_column_default(*args)
223
242
  table, column, _default_or_changes = args
224
243
 
225
244
  # just check ActiveRecord::Base, even though can override on model
226
- partial_inserts = ar_version >= 7 ? ActiveRecord::Base.partial_inserts : ActiveRecord::Base.partial_writes
245
+ partial_inserts = ActiveRecord::Base.partial_inserts
227
246
 
228
247
  if partial_inserts && !new_column?(table, column)
229
248
  raise_error :change_column_default,
230
- config: ar_version >= 7 ? "partial_inserts" : "partial_writes"
249
+ config: "partial_inserts"
231
250
  end
232
251
  end
233
252
 
@@ -235,60 +254,40 @@ module StrongMigrations
235
254
  table, column, null, default = args
236
255
  if !null
237
256
  if postgresql?
238
- safe = false
239
- safe_with_check_constraint = adapter.server_version >= Gem::Version.new("12")
240
- if safe_with_check_constraint
241
- safe = adapter.constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
242
- end
257
+ constraints = connection.check_constraints(table)
258
+ safe = constraints.any? { |c| c.options[:validate] && (c.expression == "#{column} IS NOT NULL" || c.expression == "#{connection.quote_column_name(column)} IS NOT NULL") }
243
259
 
244
260
  unless safe
261
+ expression = "#{quote_column_if_needed(column)} IS NOT NULL"
262
+
245
263
  # match https://github.com/nullobject/rein
246
264
  constraint_name = "#{table}_#{column}_null"
265
+ if adapter.max_constraint_name_length && constraint_name.bytesize > adapter.max_constraint_name_length
266
+ constraint_name = connection.check_constraint_options(table, expression, {})[:name]
247
267
 
248
- add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
249
- validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
250
- remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
251
-
252
- constraint_methods = ar_version >= 6.1
253
-
254
- validate_constraint_code =
255
- if constraint_methods
256
- String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
257
- else
258
- String.new(safety_assured_str(validate_code))
259
- end
260
-
261
- if safe_with_check_constraint
262
- change_args = [table, column, null]
263
-
264
- validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
265
-
266
- if constraint_methods
267
- validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
268
- else
269
- validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
268
+ # avoid collision with Active Record naming for safe_by_default
269
+ if StrongMigrations.safe_by_default
270
+ constraint_name = constraint_name.sub("rails", "strong_migrations")
270
271
  end
271
272
  end
272
273
 
274
+ add_args = [table, expression, {name: constraint_name, validate: false}]
275
+ validate_args = [table, {name: constraint_name}]
276
+ change_args = [table, column, null]
277
+ remove_args = [table, {name: constraint_name}]
278
+
273
279
  if StrongMigrations.safe_by_default
274
- safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
280
+ safe_change_column_null(add_args, validate_args, change_args, remove_args, table, column, default, constraints)
275
281
  throw :safe
276
282
  end
277
283
 
278
- add_constraint_code =
279
- if constraint_methods
280
- command_str(:add_check_constraint, [table, "#{quote_column_if_needed(column)} IS NOT NULL", {name: constraint_name, validate: false}])
281
- else
282
- safety_assured_str(add_code)
283
- end
284
+ add_constraint_code = command_str(:add_check_constraint, add_args)
284
285
 
285
- validate_constraint_code =
286
- if safe_with_check_constraint
287
- down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
288
- "def up\n #{validate_constraint_code}\n end\n\n def down\n #{down_code}\n end"
289
- else
290
- "def change\n #{validate_constraint_code}\n end"
291
- end
286
+ up_code = String.new(command_str(:validate_check_constraint, validate_args))
287
+ up_code << "\n #{command_str(:change_column_null, change_args)}"
288
+ up_code << "\n #{command_str(:remove_check_constraint, remove_args)}"
289
+ down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
290
+ validate_constraint_code = "def up\n #{up_code}\n end\n\n def down\n #{down_code}\n end"
292
291
 
293
292
  raise_error :change_column_null_postgresql,
294
293
  add_constraint_code: add_constraint_code,
@@ -337,29 +336,28 @@ module StrongMigrations
337
336
  columns =
338
337
  case method
339
338
  when :remove_timestamps
340
- ["created_at", "updated_at"]
339
+ [:created_at, :updated_at]
341
340
  when :remove_column
342
- [args[1].to_s]
341
+ [args[1]]
343
342
  when :remove_columns
344
- # Active Record 6.1+ supports options
345
343
  if args.last.is_a?(Hash)
346
- args[1..-2].map(&:to_s)
344
+ args[1..-2]
347
345
  else
348
- args[1..-1].map(&:to_s)
346
+ args[1..-1]
349
347
  end
350
348
  else
351
349
  options = args[2] || {}
352
350
  reference = args[1]
353
351
  cols = []
354
- cols << "#{reference}_type" if options[:polymorphic]
355
- cols << "#{reference}_id"
352
+ cols << "#{reference}_type".to_sym if options[:polymorphic]
353
+ cols << "#{reference}_id".to_sym
356
354
  cols
357
355
  end
358
356
 
359
- code = "self.ignored_columns += #{columns.inspect}"
357
+ code = "self.ignored_columns += #{columns.map(&:to_s).inspect}"
360
358
 
361
359
  raise_error :remove_column,
362
- model: args[0].to_s.classify,
360
+ model: model_name(args[0]),
363
361
  code: code,
364
362
  command: command_str(method, args),
365
363
  column_suffix: columns.size > 1 ? "s" : ""
@@ -373,12 +371,6 @@ module StrongMigrations
373
371
  # avoid suggesting extra (invalid) args
374
372
  args = args[0..1] unless StrongMigrations.safe_by_default
375
373
 
376
- # Active Record < 6.1 only supports two arguments (including options)
377
- if args.size == 2 && ar_version < 6.1
378
- # arg takes precedence over option
379
- options[:column] = args.pop
380
- end
381
-
382
374
  if StrongMigrations.safe_by_default
383
375
  safe_remove_index(*args, **options)
384
376
  throw :safe
@@ -433,7 +425,7 @@ module StrongMigrations
433
425
  message = message + append if append
434
426
 
435
427
  vars[:migration_name] = @migration.class.name
436
- vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
428
+ vars[:migration_suffix] = migration_suffix
437
429
  vars[:base_model] = "ApplicationRecord"
438
430
 
439
431
  # escape % not followed by {
@@ -474,20 +466,20 @@ module StrongMigrations
474
466
  end
475
467
 
476
468
  def backfill_code(table, column, default, function = false)
477
- model = table.to_s.classify
469
+ model = model_name(table)
478
470
  if function
479
471
  # update_all(column: Arel.sql(default)) also works in newer versions of Active Record
480
472
  update_expr = "#{quote_column_if_needed(column)} = #{default}"
481
- "#{model}.unscoped.in_batches do |relation| \n relation.where(#{column}: nil).update_all(#{update_expr.inspect})\n sleep(0.01)\n end"
473
+ "#{model}.unscoped.in_batches(of: 10000) do |relation| \n relation.where(#{column}: nil).update_all(#{update_expr.inspect})\n sleep(0.01)\n end"
482
474
  else
483
- "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
475
+ "#{model}.unscoped.in_batches(of: 10000) do |relation| \n relation.where(#{column}: nil).update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
484
476
  end
485
477
  end
486
478
 
487
479
  # only quote when needed
488
480
  # important! only use for display purposes
489
481
  def quote_column_if_needed(column)
490
- column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
482
+ /\A[a-z0-9_]+\z/.match?(column.to_s) ? column : connection.quote_column_name(column)
491
483
  end
492
484
 
493
485
  def new_table?(table)
@@ -497,5 +489,13 @@ module StrongMigrations
497
489
  def new_column?(table, column)
498
490
  new_table?(table) || @new_columns.include?([table.to_s, column.to_s])
499
491
  end
492
+
493
+ def migration_suffix
494
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
495
+ end
496
+
497
+ def model_name(table)
498
+ table.to_s.classify
499
+ end
500
500
  end
501
501
  end
@@ -25,16 +25,6 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
25
25
  end
26
26
  end",
27
27
 
28
- add_column_default_null:
29
- "Adding a column with a null default blocks %{rewrite_blocks} while the entire table is rewritten.
30
- Instead, add the column without a default value.
31
-
32
- class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
33
- def change
34
- %{command}
35
- end
36
- end",
37
-
38
28
  add_column_default_callable:
39
29
  "Strong Migrations does not support inspecting callable default values.
40
30
  Please make really sure you're not calling a VOLATILE function,
@@ -70,6 +60,22 @@ while the entire table is rewritten. A safer approach is to:
70
60
  change_column_with_not_null:
71
61
  "Changing the type is safe, but setting NOT NULL is not.",
72
62
 
63
+ change_column_constraint: "Changing the type of a column that has check constraints blocks reads and writes
64
+ while every row is checked. Drop the check constraints on the column before
65
+ changing the type and add them back afterwards.
66
+
67
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
68
+ def change
69
+ %{change_column_code}
70
+ end
71
+ end
72
+
73
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
74
+ def change
75
+ %{validate_constraint_code}
76
+ end
77
+ end",
78
+
73
79
  remove_column: "Active Record caches attributes, which causes problems
74
80
  when removing columns. Be sure to ignore the column%{column_suffix}:
75
81
 
@@ -157,7 +163,7 @@ you're doing is safe before proceeding, then wrap it in a safety_assured { ... }
157
163
  create_table:
158
164
  "The force option will destroy existing tables.
159
165
  If this is intended, drop the existing table first.
160
- Otherwise, remove the force option.",
166
+ In any case, remove the force option.",
161
167
 
162
168
  execute:
163
169
  "Strong Migrations does not support inspecting what happens inside an
@@ -18,7 +18,8 @@ module StrongMigrations
18
18
  end
19
19
  end
20
20
  end
21
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
21
+ # same as ActiveRecord::Migration
22
+ ruby2_keywords(:method_missing)
22
23
 
23
24
  def revert(*)
24
25
  if strong_migrations_checker.version_safe?
@@ -1,9 +1,26 @@
1
1
  module StrongMigrations
2
- module DatabaseTasks
3
- # Active Record 7 adds version argument
4
- def migrate(*args)
2
+ module MigrationContext
3
+ def up(...)
5
4
  super
6
5
  rescue => e
6
+ strong_migrations_process_exception(e)
7
+ end
8
+
9
+ def down(...)
10
+ super
11
+ rescue => e
12
+ strong_migrations_process_exception(e)
13
+ end
14
+
15
+ def run(...)
16
+ super
17
+ rescue => e
18
+ strong_migrations_process_exception(e)
19
+ end
20
+
21
+ private
22
+
23
+ def strong_migrations_process_exception(e)
7
24
  if e.cause.is_a?(StrongMigrations::Error)
8
25
  # strip cause and clean backtrace
9
26
  def e.cause
@@ -1,18 +1,20 @@
1
1
  module StrongMigrations
2
2
  module Migrator
3
- def ddl_transaction(migration, *args)
3
+ def ddl_transaction(migration, ...)
4
4
  return super unless StrongMigrations.lock_timeout_retries > 0 && use_transaction?(migration)
5
5
 
6
6
  # handle MigrationProxy class
7
7
  migration = migration.send(:migration) if migration.respond_to?(:migration, true)
8
8
 
9
- # retry migration since the entire transaction needs to be rerun
10
9
  checker = migration.send(:strong_migrations_checker)
10
+ return super if checker.skip?
11
+
12
+ # retry migration since the entire transaction needs to be rerun
11
13
  checker.retry_lock_timeouts(check_committed: true) do
12
14
  # failed transaction reverts timeout, so need to re-apply
13
- checker.timeouts_set = false
15
+ checker.reset
14
16
 
15
- super(migration, *args)
17
+ super(migration, ...)
16
18
  end
17
19
  end
18
20
  end