strong_migrations 0.7.9 → 1.4.4

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