strong_migrations 1.4.4 → 2.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -1
- data/LICENSE.txt +1 -1
- data/README.md +285 -155
- data/lib/generators/strong_migrations/install_generator.rb +3 -7
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +3 -0
- data/lib/strong_migrations/adapters/abstract_adapter.rb +13 -10
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +2 -2
- data/lib/strong_migrations/adapters/mysql_adapter.rb +10 -5
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +25 -28
- data/lib/strong_migrations/checker.rb +109 -19
- data/lib/strong_migrations/checks.rb +130 -85
- data/lib/strong_migrations/error_messages.rb +61 -14
- data/lib/strong_migrations/migration.rb +12 -6
- data/lib/strong_migrations/{database_tasks.rb → migration_context.rb} +20 -3
- data/lib/strong_migrations/migrator.rb +7 -5
- data/lib/strong_migrations/safe_methods.rb +60 -40
- data/lib/strong_migrations/schema_dumper.rb +15 -4
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +9 -6
- metadata +7 -11
@@ -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] ||
|
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
|
@@ -32,33 +32,27 @@ module StrongMigrations
|
|
32
32
|
table, column, type = args
|
33
33
|
default = options[:default]
|
34
34
|
|
35
|
+
# keep track of new columns of change_column_default check
|
36
|
+
@new_columns << [table.to_s, column.to_s]
|
37
|
+
|
35
38
|
# Check key since DEFAULT NULL behaves differently from no default
|
36
39
|
#
|
37
40
|
# Also, Active Record has special case for uuid columns that allows function default values
|
38
41
|
# https://github.com/rails/rails/blob/v7.0.3.1/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L92-L93
|
39
|
-
if
|
42
|
+
if !default.nil? && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
|
40
43
|
if options[:null] == false
|
41
44
|
options = options.except(:null)
|
42
|
-
append = "
|
43
|
-
|
44
|
-
Then add the NOT NULL constraint in separate migrations."
|
45
|
+
append = "\n\nThen add the NOT NULL constraint in separate migrations."
|
45
46
|
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
change_command: command_str("change_column_default", [table, column, default]),
|
56
|
-
remove_command: command_str("remove_column", [table, column]),
|
57
|
-
code: backfill_code(table, column, default, volatile),
|
58
|
-
append: append,
|
59
|
-
rewrite_blocks: adapter.rewrite_blocks,
|
60
|
-
default_type: (volatile ? "volatile" : "non-null")
|
61
|
-
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")
|
62
56
|
elsif default.is_a?(Proc) && postgresql?
|
63
57
|
# adding a column with a VOLATILE default is not safe
|
64
58
|
# https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
|
@@ -71,6 +65,17 @@ Then add the NOT NULL constraint in separate migrations."
|
|
71
65
|
raise_error :add_column_json,
|
72
66
|
command: command_str("add_column", [table, column, :jsonb, options])
|
73
67
|
end
|
68
|
+
|
69
|
+
if type.to_s == "virtual" && options[:stored]
|
70
|
+
raise_error :add_column_generated_stored, rewrite_blocks: adapter.rewrite_blocks
|
71
|
+
end
|
72
|
+
|
73
|
+
if adapter.auto_incrementing_types.include?(type.to_s)
|
74
|
+
append = (mysql? || mariadb?) ? "\n\nIf using statement-based replication, this can also generate different values on replicas." : ""
|
75
|
+
raise_error :add_column_auto_incrementing,
|
76
|
+
rewrite_blocks: adapter.rewrite_blocks,
|
77
|
+
append: append
|
78
|
+
end
|
74
79
|
end
|
75
80
|
|
76
81
|
def check_add_exclusion_constraint(*args)
|
@@ -147,7 +152,7 @@ Then add the NOT NULL constraint in separate migrations."
|
|
147
152
|
if bad_index || options[:foreign_key]
|
148
153
|
if index_value.is_a?(Hash)
|
149
154
|
options[:index] = options[:index].merge(algorithm: :concurrently)
|
150
|
-
|
155
|
+
elsif index_value
|
151
156
|
options = options.merge(index: {algorithm: :concurrently})
|
152
157
|
end
|
153
158
|
|
@@ -158,9 +163,7 @@ Then add the NOT NULL constraint in separate migrations."
|
|
158
163
|
|
159
164
|
if options.delete(:foreign_key)
|
160
165
|
headline = "Adding a foreign key blocks writes on both tables."
|
161
|
-
append = "
|
162
|
-
|
163
|
-
Then add the foreign key in separate migrations."
|
166
|
+
append = "\n\nThen add the foreign key in separate migrations."
|
164
167
|
else
|
165
168
|
headline = "Adding an index non-concurrently locks the table."
|
166
169
|
end
|
@@ -173,6 +176,21 @@ Then add the foreign key in separate migrations."
|
|
173
176
|
end
|
174
177
|
end
|
175
178
|
|
179
|
+
def check_add_unique_constraint(*args)
|
180
|
+
args.extract_options!
|
181
|
+
table, column = args
|
182
|
+
|
183
|
+
# column and using_index cannot be used together
|
184
|
+
# check for column to ensure error message can be generated
|
185
|
+
if column && !new_table?(table)
|
186
|
+
index_name = connection.index_name(table, {column: column})
|
187
|
+
raise_error :add_unique_constraint,
|
188
|
+
index_command: command_str(:add_index, [table, column, {unique: true, algorithm: :concurrently}]),
|
189
|
+
constraint_command: command_str(:add_unique_constraint, [table, {using_index: index_name}]),
|
190
|
+
remove_command: command_str(:remove_unique_constraint, [table, column])
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
176
194
|
def check_change_column(*args)
|
177
195
|
options = args.extract_options!
|
178
196
|
table, column, type = args
|
@@ -192,66 +210,84 @@ Then add the foreign key in separate migrations."
|
|
192
210
|
end
|
193
211
|
|
194
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
|
239
|
+
end
|
240
|
+
|
241
|
+
def check_change_column_default(*args)
|
242
|
+
table, column, _default_or_changes = args
|
243
|
+
|
244
|
+
# just check ActiveRecord::Base, even though can override on model
|
245
|
+
partial_inserts = ActiveRecord::Base.partial_inserts
|
246
|
+
|
247
|
+
if partial_inserts && !new_column?(table, column)
|
248
|
+
raise_error :change_column_default,
|
249
|
+
config: "partial_inserts"
|
250
|
+
end
|
195
251
|
end
|
196
252
|
|
197
253
|
def check_change_column_null(*args)
|
198
254
|
table, column, null, default = args
|
199
255
|
if !null
|
200
256
|
if postgresql?
|
201
|
-
|
202
|
-
|
203
|
-
if safe_with_check_constraint
|
204
|
-
safe = adapter.constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
|
205
|
-
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") }
|
206
259
|
|
207
260
|
unless safe
|
261
|
+
expression = "#{quote_column_if_needed(column)} IS NOT NULL"
|
262
|
+
|
208
263
|
# match https://github.com/nullobject/rein
|
209
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]
|
210
267
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
constraint_methods = ar_version >= 6.1
|
216
|
-
|
217
|
-
validate_constraint_code =
|
218
|
-
if constraint_methods
|
219
|
-
String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
|
220
|
-
else
|
221
|
-
String.new(safety_assured_str(validate_code))
|
222
|
-
end
|
223
|
-
|
224
|
-
if safe_with_check_constraint
|
225
|
-
change_args = [table, column, null]
|
226
|
-
|
227
|
-
validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
|
228
|
-
|
229
|
-
if constraint_methods
|
230
|
-
validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
|
231
|
-
else
|
232
|
-
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")
|
233
271
|
end
|
234
272
|
end
|
235
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
|
+
|
236
279
|
if StrongMigrations.safe_by_default
|
237
|
-
safe_change_column_null(
|
280
|
+
safe_change_column_null(add_args, validate_args, change_args, remove_args, table, column, default, constraints)
|
238
281
|
throw :safe
|
239
282
|
end
|
240
283
|
|
241
|
-
add_constraint_code =
|
242
|
-
if constraint_methods
|
243
|
-
command_str(:add_check_constraint, [table, "#{quote_column_if_needed(column)} IS NOT NULL", {name: constraint_name, validate: false}])
|
244
|
-
else
|
245
|
-
safety_assured_str(add_code)
|
246
|
-
end
|
284
|
+
add_constraint_code = command_str(:add_check_constraint, add_args)
|
247
285
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
"def change\n #{validate_constraint_code}\n end"
|
254
|
-
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"
|
255
291
|
|
256
292
|
raise_error :change_column_null_postgresql,
|
257
293
|
add_constraint_code: add_constraint_code,
|
@@ -300,29 +336,28 @@ Then add the foreign key in separate migrations."
|
|
300
336
|
columns =
|
301
337
|
case method
|
302
338
|
when :remove_timestamps
|
303
|
-
[
|
339
|
+
[:created_at, :updated_at]
|
304
340
|
when :remove_column
|
305
|
-
[args[1]
|
341
|
+
[args[1]]
|
306
342
|
when :remove_columns
|
307
|
-
# Active Record 6.1+ supports options
|
308
343
|
if args.last.is_a?(Hash)
|
309
|
-
args[1..-2]
|
344
|
+
args[1..-2]
|
310
345
|
else
|
311
|
-
args[1..-1]
|
346
|
+
args[1..-1]
|
312
347
|
end
|
313
348
|
else
|
314
349
|
options = args[2] || {}
|
315
350
|
reference = args[1]
|
316
351
|
cols = []
|
317
|
-
cols << "#{reference}_type" if options[:polymorphic]
|
318
|
-
cols << "#{reference}_id"
|
352
|
+
cols << "#{reference}_type".to_sym if options[:polymorphic]
|
353
|
+
cols << "#{reference}_id".to_sym
|
319
354
|
cols
|
320
355
|
end
|
321
356
|
|
322
|
-
code = "self.ignored_columns
|
357
|
+
code = "self.ignored_columns += #{columns.map(&:to_s).inspect}"
|
323
358
|
|
324
359
|
raise_error :remove_column,
|
325
|
-
model: args[0]
|
360
|
+
model: model_name(args[0]),
|
326
361
|
code: code,
|
327
362
|
command: command_str(method, args),
|
328
363
|
column_suffix: columns.size > 1 ? "s" : ""
|
@@ -336,12 +371,6 @@ Then add the foreign key in separate migrations."
|
|
336
371
|
# avoid suggesting extra (invalid) args
|
337
372
|
args = args[0..1] unless StrongMigrations.safe_by_default
|
338
373
|
|
339
|
-
# Active Record < 6.1 only supports two arguments (including options)
|
340
|
-
if args.size == 2 && ar_version < 6.1
|
341
|
-
# arg takes precedence over option
|
342
|
-
options[:column] = args.pop
|
343
|
-
end
|
344
|
-
|
345
374
|
if StrongMigrations.safe_by_default
|
346
375
|
safe_remove_index(*args, **options)
|
347
376
|
throw :safe
|
@@ -355,6 +384,10 @@ Then add the foreign key in separate migrations."
|
|
355
384
|
raise_error :rename_column
|
356
385
|
end
|
357
386
|
|
387
|
+
def check_rename_schema
|
388
|
+
raise_error :rename_schema
|
389
|
+
end
|
390
|
+
|
358
391
|
def check_rename_table
|
359
392
|
raise_error :rename_table
|
360
393
|
end
|
@@ -396,7 +429,7 @@ Then add the foreign key in separate migrations."
|
|
396
429
|
message = message + append if append
|
397
430
|
|
398
431
|
vars[:migration_name] = @migration.class.name
|
399
|
-
vars[:migration_suffix] =
|
432
|
+
vars[:migration_suffix] = migration_suffix
|
400
433
|
vars[:base_model] = "ApplicationRecord"
|
401
434
|
|
402
435
|
# escape % not followed by {
|
@@ -437,24 +470,36 @@ Then add the foreign key in separate migrations."
|
|
437
470
|
end
|
438
471
|
|
439
472
|
def backfill_code(table, column, default, function = false)
|
440
|
-
model = table
|
473
|
+
model = model_name(table)
|
441
474
|
if function
|
442
475
|
# update_all(column: Arel.sql(default)) also works in newer versions of Active Record
|
443
476
|
update_expr = "#{quote_column_if_needed(column)} = #{default}"
|
444
|
-
"#{model}.unscoped.in_batches do |relation| \n relation.where(#{column}: nil).update_all(#{update_expr.inspect})\n sleep(0.01)\n end"
|
477
|
+
"#{model}.unscoped.in_batches(of: 10000) do |relation| \n relation.where(#{column}: nil).update_all(#{update_expr.inspect})\n sleep(0.01)\n end"
|
445
478
|
else
|
446
|
-
"#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|
479
|
+
"#{model}.unscoped.in_batches(of: 10000) do |relation| \n relation.where(#{column}: nil).update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
|
447
480
|
end
|
448
481
|
end
|
449
482
|
|
450
483
|
# only quote when needed
|
451
484
|
# important! only use for display purposes
|
452
485
|
def quote_column_if_needed(column)
|
453
|
-
|
486
|
+
/\A[a-z0-9_]+\z/.match?(column.to_s) ? column : connection.quote_column_name(column)
|
454
487
|
end
|
455
488
|
|
456
489
|
def new_table?(table)
|
457
490
|
@new_tables.include?(table.to_s)
|
458
491
|
end
|
492
|
+
|
493
|
+
def new_column?(table, column)
|
494
|
+
new_table?(table) || @new_columns.include?([table.to_s, column.to_s])
|
495
|
+
end
|
496
|
+
|
497
|
+
def migration_suffix
|
498
|
+
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
|
499
|
+
end
|
500
|
+
|
501
|
+
def model_name(table)
|
502
|
+
table.to_s.classify
|
503
|
+
end
|
459
504
|
end
|
460
505
|
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,
|
@@ -50,6 +40,12 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
|
50
40
|
end
|
51
41
|
end",
|
52
42
|
|
43
|
+
add_column_generated_stored:
|
44
|
+
"Adding a stored generated column blocks %{rewrite_blocks} while the entire table is rewritten.",
|
45
|
+
|
46
|
+
add_column_auto_incrementing:
|
47
|
+
"Adding an auto-incrementing column blocks %{rewrite_blocks} while the entire table is rewritten.",
|
48
|
+
|
53
49
|
change_column:
|
54
50
|
"Changing the type of an existing column blocks %{rewrite_blocks}
|
55
51
|
while the entire table is rewritten. A safer approach is to:
|
@@ -64,6 +60,22 @@ while the entire table is rewritten. A safer approach is to:
|
|
64
60
|
change_column_with_not_null:
|
65
61
|
"Changing the type is safe, but setting NOT NULL is not.",
|
66
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
|
+
|
67
79
|
remove_column: "Active Record caches attributes, which causes problems
|
68
80
|
when removing columns. Be sure to ignore the column%{column_suffix}:
|
69
81
|
|
@@ -85,18 +97,29 @@ in your application. A safer approach is to:
|
|
85
97
|
|
86
98
|
1. Create a new column
|
87
99
|
2. Write to both columns
|
88
|
-
3. Backfill data from the old column to new column
|
100
|
+
3. Backfill data from the old column to the new column
|
89
101
|
4. Move reads from the old column to the new column
|
90
102
|
5. Stop writing to the old column
|
91
103
|
6. Drop the old column",
|
92
104
|
|
105
|
+
rename_schema:
|
106
|
+
"Renaming a schema that's in use will cause errors
|
107
|
+
in your application. A safer approach is to:
|
108
|
+
|
109
|
+
1. Create a new schema
|
110
|
+
2. Write to both schemas
|
111
|
+
3. Backfill data from the old schema to the new schema
|
112
|
+
4. Move reads from the old schema to the new schema
|
113
|
+
5. Stop writing to the old schema
|
114
|
+
6. Drop the old schema",
|
115
|
+
|
93
116
|
rename_table:
|
94
117
|
"Renaming a table that's in use will cause errors
|
95
118
|
in your application. A safer approach is to:
|
96
119
|
|
97
120
|
1. Create a new table. Don't forget to recreate indexes from the old table
|
98
121
|
2. Write to both tables
|
99
|
-
3. Backfill data from the old table to new table
|
122
|
+
3. Backfill data from the old table to the new table
|
100
123
|
4. Move reads from the old table to the new table
|
101
124
|
5. Stop writing to the old table
|
102
125
|
6. Drop the old table",
|
@@ -151,13 +174,20 @@ you're doing is safe before proceeding, then wrap it in a safety_assured { ... }
|
|
151
174
|
create_table:
|
152
175
|
"The force option will destroy existing tables.
|
153
176
|
If this is intended, drop the existing table first.
|
154
|
-
|
177
|
+
In any case, remove the force option.",
|
155
178
|
|
156
179
|
execute:
|
157
180
|
"Strong Migrations does not support inspecting what happens inside an
|
158
181
|
execute call, so cannot help you here. Please make really sure that what
|
159
182
|
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
|
160
183
|
|
184
|
+
change_column_default:
|
185
|
+
"Partial writes are enabled, which can cause incorrect values
|
186
|
+
to be inserted when changing the default value of a column.
|
187
|
+
Disable partial writes in config/application.rb:
|
188
|
+
|
189
|
+
config.active_record.%{config} = false",
|
190
|
+
|
161
191
|
change_column_null:
|
162
192
|
"Passing a default value to change_column_null runs a single UPDATE query,
|
163
193
|
which can cause downtime. Instead, backfill the existing rows in the
|
@@ -234,7 +264,24 @@ end",
|
|
234
264
|
Use disable_ddl_transaction! or a separate migration.",
|
235
265
|
|
236
266
|
add_exclusion_constraint:
|
237
|
-
"Adding an exclusion constraint blocks reads and writes while every row is checked."
|
267
|
+
"Adding an exclusion constraint blocks reads and writes while every row is checked.",
|
268
|
+
|
269
|
+
add_unique_constraint:
|
270
|
+
"Adding a unique constraint creates a unique index, which blocks reads and writes.
|
271
|
+
Instead, create a unique index concurrently, then use it for the constraint.
|
272
|
+
|
273
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
274
|
+
disable_ddl_transaction!
|
275
|
+
|
276
|
+
def up
|
277
|
+
%{index_command}
|
278
|
+
%{constraint_command}
|
279
|
+
end
|
280
|
+
|
281
|
+
def down
|
282
|
+
%{remove_command}
|
283
|
+
end
|
284
|
+
end"
|
238
285
|
}
|
239
286
|
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
240
287
|
end
|
@@ -7,10 +7,7 @@ module StrongMigrations
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def method_missing(method, *args)
|
10
|
-
return super if is_a?(ActiveRecord::Schema)
|
11
|
-
|
12
|
-
# Active Record 7.0.2+ versioned schema
|
13
|
-
return super if defined?(ActiveRecord::Schema::Definition) && is_a?(ActiveRecord::Schema::Definition)
|
10
|
+
return super if is_a?(ActiveRecord::Schema) || is_a?(ActiveRecord::Schema::Definition)
|
14
11
|
|
15
12
|
catch(:safe) do
|
16
13
|
strong_migrations_checker.perform(method, *args) do
|
@@ -18,10 +15,19 @@ module StrongMigrations
|
|
18
15
|
end
|
19
16
|
end
|
20
17
|
end
|
21
|
-
|
18
|
+
# same as ActiveRecord::Migration
|
19
|
+
ruby2_keywords(:method_missing)
|
20
|
+
|
21
|
+
def revert(*)
|
22
|
+
if strong_migrations_checker.version_safe?
|
23
|
+
safety_assured { super }
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
22
28
|
|
23
29
|
def safety_assured
|
24
|
-
strong_migrations_checker.safety_assured do
|
30
|
+
strong_migrations_checker.class.safety_assured do
|
25
31
|
yield
|
26
32
|
end
|
27
33
|
end
|
@@ -1,9 +1,26 @@
|
|
1
1
|
module StrongMigrations
|
2
|
-
module
|
3
|
-
|
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,
|
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
|
-
migration = migration.send(:migration) if migration.respond_to?(:migration, true)
|
7
|
+
migration = migration.send(:migration) if !migration.is_a?(ActiveRecord::Migration) && 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.
|
15
|
+
checker.reset
|
14
16
|
|
15
|
-
super(migration,
|
17
|
+
super(migration, ...)
|
16
18
|
end
|
17
19
|
end
|
18
20
|
end
|