strong_migrations 2.0.0 → 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 +42 -0
- data/LICENSE.txt +1 -1
- data/README.md +105 -45
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +3 -0
- data/lib/strong_migrations/adapters/abstract_adapter.rb +9 -2
- data/lib/strong_migrations/adapters/mysql_adapter.rb +4 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +17 -24
- data/lib/strong_migrations/checker.rb +85 -5
- data/lib/strong_migrations/checks.rb +73 -28
- data/lib/strong_migrations/error_messages.rb +28 -1
- data/lib/strong_migrations/migration.rb +3 -5
- 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 +57 -30
- 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
@@ -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
|
@@ -70,6 +79,8 @@ module StrongMigrations
|
|
70
79
|
check_remove_index(*args)
|
71
80
|
when :rename_column
|
72
81
|
check_rename_column
|
82
|
+
when :rename_schema
|
83
|
+
check_rename_schema
|
73
84
|
when :rename_table
|
74
85
|
check_rename_table
|
75
86
|
when :validate_check_constraint
|
@@ -96,19 +107,26 @@ module StrongMigrations
|
|
96
107
|
# TODO figure out how to handle methods that generate multiple statements
|
97
108
|
# like add_reference(table, ref, index: {algorithm: :concurrently})
|
98
109
|
# lock timeout after first statement will cause retry to fail
|
99
|
-
retry_lock_timeouts {
|
110
|
+
retry_lock_timeouts { perform_method(method, *args, &block) }
|
100
111
|
else
|
101
|
-
|
112
|
+
perform_method(method, *args, &block)
|
102
113
|
end
|
103
114
|
|
104
115
|
# outdated statistics + a new index can hurt performance of existing queries
|
105
|
-
if StrongMigrations.auto_analyze && direction == :up && method
|
116
|
+
if StrongMigrations.auto_analyze && direction == :up && adds_index?(method, *args)
|
106
117
|
adapter.analyze_table(args[0])
|
107
118
|
end
|
108
119
|
|
109
120
|
result
|
110
121
|
end
|
111
122
|
|
123
|
+
def perform_method(method, *args)
|
124
|
+
if StrongMigrations.remove_invalid_indexes && direction == :up && method == :add_index && postgresql?
|
125
|
+
remove_invalid_index_if_needed(*args)
|
126
|
+
end
|
127
|
+
yield
|
128
|
+
end
|
129
|
+
|
112
130
|
def retry_lock_timeouts(check_committed: false)
|
113
131
|
retries = 0
|
114
132
|
begin
|
@@ -129,8 +147,22 @@ module StrongMigrations
|
|
129
147
|
version && version <= StrongMigrations.start_after
|
130
148
|
end
|
131
149
|
|
150
|
+
def skip?
|
151
|
+
StrongMigrations.skipped_databases.map(&:to_s).include?(db_config_name)
|
152
|
+
end
|
153
|
+
|
132
154
|
private
|
133
155
|
|
156
|
+
def check_adapter
|
157
|
+
return if defined?(@adapter_checked)
|
158
|
+
|
159
|
+
if adapter.instance_of?(Adapters::AbstractAdapter)
|
160
|
+
warn "[strong_migrations] Unsupported adapter: #{connection.adapter_name}. Use StrongMigrations.skip_database(#{db_config_name.to_sym.inspect}) to silence this warning."
|
161
|
+
end
|
162
|
+
|
163
|
+
@adapter_checked = true
|
164
|
+
end
|
165
|
+
|
134
166
|
def check_version_supported
|
135
167
|
return if defined?(@version_checked)
|
136
168
|
|
@@ -151,6 +183,9 @@ module StrongMigrations
|
|
151
183
|
if StrongMigrations.statement_timeout
|
152
184
|
adapter.set_statement_timeout(StrongMigrations.statement_timeout)
|
153
185
|
end
|
186
|
+
if StrongMigrations.transaction_timeout
|
187
|
+
adapter.set_transaction_timeout(StrongMigrations.transaction_timeout)
|
188
|
+
end
|
154
189
|
if StrongMigrations.lock_timeout
|
155
190
|
adapter.set_lock_timeout(StrongMigrations.lock_timeout)
|
156
191
|
end
|
@@ -200,12 +235,57 @@ module StrongMigrations
|
|
200
235
|
@migration.connection
|
201
236
|
end
|
202
237
|
|
238
|
+
def db_config_name
|
239
|
+
connection.pool.db_config.name
|
240
|
+
end
|
241
|
+
|
203
242
|
def retry_lock_timeouts?(method)
|
204
243
|
(
|
205
244
|
StrongMigrations.lock_timeout_retries > 0 &&
|
206
245
|
!in_transaction? &&
|
207
|
-
method != :transaction
|
246
|
+
method != :transaction &&
|
247
|
+
!@skip_retries
|
208
248
|
)
|
209
249
|
end
|
250
|
+
|
251
|
+
def without_retries
|
252
|
+
previous_value = @skip_retries
|
253
|
+
begin
|
254
|
+
@skip_retries = true
|
255
|
+
yield
|
256
|
+
ensure
|
257
|
+
@skip_retries = previous_value
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def adds_index?(method, *args)
|
262
|
+
case method
|
263
|
+
when :add_index
|
264
|
+
true
|
265
|
+
when :add_reference, :add_belongs_to
|
266
|
+
options = args.extract_options!
|
267
|
+
!!options.fetch(:index, true)
|
268
|
+
else
|
269
|
+
false
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# REINDEX INDEX CONCURRENTLY leaves a new invalid index if it fails, so use remove_index instead
|
274
|
+
def remove_invalid_index_if_needed(*args)
|
275
|
+
options = args.extract_options!
|
276
|
+
|
277
|
+
# ensures has same options as existing index
|
278
|
+
# check args to avoid errors with index_exists?
|
279
|
+
return unless args.size == 2 && connection.index_exists?(*args, **options.merge(valid: false))
|
280
|
+
|
281
|
+
table, columns = args
|
282
|
+
index_name = options.fetch(:name, connection.index_name(table, columns))
|
283
|
+
|
284
|
+
@migration.say("Attempting to remove invalid index")
|
285
|
+
without_retries do
|
286
|
+
# TODO pass index schema for extra safety?
|
287
|
+
@migration.remove_index(table, **{name: index_name}.merge(options.slice(:algorithm)))
|
288
|
+
end
|
289
|
+
end
|
210
290
|
end
|
211
291
|
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] ||
|
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
|
@@ -210,17 +210,43 @@ module StrongMigrations
|
|
210
210
|
end
|
211
211
|
|
212
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
|
213
239
|
end
|
214
240
|
|
215
241
|
def check_change_column_default(*args)
|
216
242
|
table, column, _default_or_changes = args
|
217
243
|
|
218
244
|
# just check ActiveRecord::Base, even though can override on model
|
219
|
-
partial_inserts =
|
245
|
+
partial_inserts = ActiveRecord::Base.partial_inserts
|
220
246
|
|
221
247
|
if partial_inserts && !new_column?(table, column)
|
222
248
|
raise_error :change_column_default,
|
223
|
-
config:
|
249
|
+
config: "partial_inserts"
|
224
250
|
end
|
225
251
|
end
|
226
252
|
|
@@ -228,32 +254,40 @@ module StrongMigrations
|
|
228
254
|
table, column, null, default = args
|
229
255
|
if !null
|
230
256
|
if postgresql?
|
231
|
-
|
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") }
|
232
259
|
|
233
260
|
unless safe
|
261
|
+
expression = "#{quote_column_if_needed(column)} IS NOT NULL"
|
262
|
+
|
234
263
|
# match https://github.com/nullobject/rein
|
235
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]
|
236
267
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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")
|
271
|
+
end
|
272
|
+
end
|
242
273
|
|
274
|
+
add_args = [table, expression, {name: constraint_name, validate: false}]
|
275
|
+
validate_args = [table, {name: constraint_name}]
|
243
276
|
change_args = [table, column, null]
|
244
|
-
|
245
|
-
validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
|
246
|
-
validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
|
277
|
+
remove_args = [table, {name: constraint_name}]
|
247
278
|
|
248
279
|
if StrongMigrations.safe_by_default
|
249
|
-
safe_change_column_null(
|
280
|
+
safe_change_column_null(add_args, validate_args, change_args, remove_args, table, column, default, constraints)
|
250
281
|
throw :safe
|
251
282
|
end
|
252
283
|
|
253
|
-
add_constraint_code = command_str(:add_check_constraint,
|
284
|
+
add_constraint_code = command_str(:add_check_constraint, add_args)
|
254
285
|
|
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)}"
|
255
289
|
down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
|
256
|
-
validate_constraint_code = "def up\n #{
|
290
|
+
validate_constraint_code = "def up\n #{up_code}\n end\n\n def down\n #{down_code}\n end"
|
257
291
|
|
258
292
|
raise_error :change_column_null_postgresql,
|
259
293
|
add_constraint_code: add_constraint_code,
|
@@ -302,29 +336,28 @@ module StrongMigrations
|
|
302
336
|
columns =
|
303
337
|
case method
|
304
338
|
when :remove_timestamps
|
305
|
-
[
|
339
|
+
[:created_at, :updated_at]
|
306
340
|
when :remove_column
|
307
|
-
[args[1]
|
341
|
+
[args[1]]
|
308
342
|
when :remove_columns
|
309
|
-
# Active Record 6.1+ supports options
|
310
343
|
if args.last.is_a?(Hash)
|
311
|
-
args[1..-2]
|
344
|
+
args[1..-2]
|
312
345
|
else
|
313
|
-
args[1..-1]
|
346
|
+
args[1..-1]
|
314
347
|
end
|
315
348
|
else
|
316
349
|
options = args[2] || {}
|
317
350
|
reference = args[1]
|
318
351
|
cols = []
|
319
|
-
cols << "#{reference}_type" if options[:polymorphic]
|
320
|
-
cols << "#{reference}_id"
|
352
|
+
cols << "#{reference}_type".to_sym if options[:polymorphic]
|
353
|
+
cols << "#{reference}_id".to_sym
|
321
354
|
cols
|
322
355
|
end
|
323
356
|
|
324
|
-
code = "self.ignored_columns += #{columns.inspect}"
|
357
|
+
code = "self.ignored_columns += #{columns.map(&:to_s).inspect}"
|
325
358
|
|
326
359
|
raise_error :remove_column,
|
327
|
-
model: args[0]
|
360
|
+
model: model_name(args[0]),
|
328
361
|
code: code,
|
329
362
|
command: command_str(method, args),
|
330
363
|
column_suffix: columns.size > 1 ? "s" : ""
|
@@ -351,6 +384,10 @@ module StrongMigrations
|
|
351
384
|
raise_error :rename_column
|
352
385
|
end
|
353
386
|
|
387
|
+
def check_rename_schema
|
388
|
+
raise_error :rename_schema
|
389
|
+
end
|
390
|
+
|
354
391
|
def check_rename_table
|
355
392
|
raise_error :rename_table
|
356
393
|
end
|
@@ -392,7 +429,7 @@ module StrongMigrations
|
|
392
429
|
message = message + append if append
|
393
430
|
|
394
431
|
vars[:migration_name] = @migration.class.name
|
395
|
-
vars[:migration_suffix] =
|
432
|
+
vars[:migration_suffix] = migration_suffix
|
396
433
|
vars[:base_model] = "ApplicationRecord"
|
397
434
|
|
398
435
|
# escape % not followed by {
|
@@ -433,13 +470,13 @@ module StrongMigrations
|
|
433
470
|
end
|
434
471
|
|
435
472
|
def backfill_code(table, column, default, function = false)
|
436
|
-
model = table
|
473
|
+
model = model_name(table)
|
437
474
|
if function
|
438
475
|
# update_all(column: Arel.sql(default)) also works in newer versions of Active Record
|
439
476
|
update_expr = "#{quote_column_if_needed(column)} = #{default}"
|
440
|
-
"#{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"
|
441
478
|
else
|
442
|
-
"#{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"
|
443
480
|
end
|
444
481
|
end
|
445
482
|
|
@@ -456,5 +493,13 @@ module StrongMigrations
|
|
456
493
|
def new_column?(table, column)
|
457
494
|
new_table?(table) || @new_columns.include?([table.to_s, column.to_s])
|
458
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
|
@@ -60,6 +60,22 @@ while the entire table is rewritten. A safer approach is to:
|
|
60
60
|
change_column_with_not_null:
|
61
61
|
"Changing the type is safe, but setting NOT NULL is not.",
|
62
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
|
+
|
63
79
|
remove_column: "Active Record caches attributes, which causes problems
|
64
80
|
when removing columns. Be sure to ignore the column%{column_suffix}:
|
65
81
|
|
@@ -86,6 +102,17 @@ in your application. A safer approach is to:
|
|
86
102
|
5. Stop writing to the old column
|
87
103
|
6. Drop the old column",
|
88
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
|
+
|
89
116
|
rename_table:
|
90
117
|
"Renaming a table that's in use will cause errors
|
91
118
|
in your application. A safer approach is to:
|
@@ -147,7 +174,7 @@ you're doing is safe before proceeding, then wrap it in a safety_assured { ... }
|
|
147
174
|
create_table:
|
148
175
|
"The force option will destroy existing tables.
|
149
176
|
If this is intended, drop the existing table first.
|
150
|
-
|
177
|
+
In any case, remove the force option.",
|
151
178
|
|
152
179
|
execute:
|
153
180
|
"Strong Migrations does not support inspecting what happens inside an
|
@@ -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,7 +15,8 @@ module StrongMigrations
|
|
18
15
|
end
|
19
16
|
end
|
20
17
|
end
|
21
|
-
|
18
|
+
# same as ActiveRecord::Migration
|
19
|
+
ruby2_keywords(:method_missing)
|
22
20
|
|
23
21
|
def revert(*)
|
24
22
|
if strong_migrations_checker.version_safe?
|
@@ -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
|
@@ -1,10 +1,9 @@
|
|
1
1
|
module StrongMigrations
|
2
2
|
module SafeMethods
|
3
3
|
def safe_by_default_method?(method)
|
4
|
-
StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :add_check_constraint, :change_column_null].include?(method)
|
4
|
+
StrongMigrations.safe_by_default && !version_safe? && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :add_check_constraint, :change_column_null].include?(method)
|
5
5
|
end
|
6
6
|
|
7
|
-
# TODO check if invalid index with expected name exists and remove if needed
|
8
7
|
def safe_add_index(*args, **options)
|
9
8
|
disable_transaction
|
10
9
|
@migration.add_index(*args, **options.merge(algorithm: :concurrently))
|
@@ -47,14 +46,14 @@ module StrongMigrations
|
|
47
46
|
def safe_add_foreign_key(from_table, to_table, *args, **options)
|
48
47
|
@migration.reversible do |dir|
|
49
48
|
dir.up do
|
50
|
-
|
49
|
+
if !connection.foreign_key_exists?(from_table, to_table, **options.merge(validate: false))
|
50
|
+
@migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
|
51
|
+
end
|
51
52
|
disable_transaction
|
52
|
-
|
53
|
-
@migration.validate_foreign_key(from_table, to_table, **validate_options)
|
53
|
+
@migration.validate_foreign_key(from_table, to_table, **options.slice(:column, :name))
|
54
54
|
end
|
55
55
|
dir.down do
|
56
|
-
|
57
|
-
@migration.remove_foreign_key(from_table, to_table, **remove_options)
|
56
|
+
@migration.remove_foreign_key(from_table, to_table, **options.slice(:column, :name))
|
58
57
|
end
|
59
58
|
end
|
60
59
|
end
|
@@ -62,7 +61,10 @@ module StrongMigrations
|
|
62
61
|
def safe_add_check_constraint(table, expression, *args, add_options, validate_options)
|
63
62
|
@migration.reversible do |dir|
|
64
63
|
dir.up do
|
65
|
-
|
64
|
+
# only skip invalid constraints
|
65
|
+
unless connection.check_constraints(table).any? { |c| c.options[:name] == validate_options[:name] && !c.options[:validate] }
|
66
|
+
@migration.add_check_constraint(table, expression, *args, **add_options)
|
67
|
+
end
|
66
68
|
disable_transaction
|
67
69
|
@migration.validate_check_constraint(table, **validate_options)
|
68
70
|
end
|
@@ -72,35 +74,60 @@ module StrongMigrations
|
|
72
74
|
end
|
73
75
|
end
|
74
76
|
|
75
|
-
def safe_change_column_null(
|
77
|
+
def safe_change_column_null(add_args, validate_args, change_args, remove_args, table, column, default, constraints)
|
76
78
|
@migration.reversible do |dir|
|
77
79
|
dir.up do
|
78
80
|
unless default.nil?
|
79
|
-
|
80
|
-
|
81
|
+
# TODO search for parent model if needed
|
82
|
+
if connection.pool != ActiveRecord::Base.connection_pool
|
83
|
+
raise_error :change_column_null,
|
84
|
+
code: backfill_code(table, column, default)
|
85
|
+
end
|
86
|
+
|
87
|
+
model =
|
88
|
+
Class.new(ActiveRecord::Base) do
|
89
|
+
self.table_name = table
|
90
|
+
|
91
|
+
def self.to_s
|
92
|
+
"Backfill"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
update_sql =
|
97
|
+
model.connection_pool.with_connection do |c|
|
98
|
+
quoted_column = c.quote_column_name(column)
|
99
|
+
quoted_default = c.quote_default_expression(default, c.send(:column_for, table, column))
|
100
|
+
"#{quoted_column} = #{quoted_default}"
|
101
|
+
end
|
81
102
|
|
82
|
-
|
83
|
-
@migration.execute(add_code)
|
103
|
+
@migration.say("Backfilling default")
|
84
104
|
disable_transaction
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
@migration.change_column_null(*change_args)
|
89
|
-
@migration.safety_assured do
|
90
|
-
@migration.execute(remove_code)
|
105
|
+
model.unscoped.in_batches(of: 10000) do |relation|
|
106
|
+
relation.where(column => nil).update_all(update_sql)
|
107
|
+
sleep(0.01)
|
91
108
|
end
|
92
109
|
end
|
110
|
+
|
111
|
+
add_options = add_args.extract_options!
|
112
|
+
validate_options = validate_args.extract_options!
|
113
|
+
remove_options = remove_args.extract_options!
|
114
|
+
|
115
|
+
# only skip invalid constraints
|
116
|
+
unless constraints.any? { |c| c.options[:name] == validate_options[:name] && !c.options[:validate] }
|
117
|
+
@migration.add_check_constraint(*add_args, **add_options)
|
118
|
+
end
|
119
|
+
disable_transaction
|
120
|
+
|
121
|
+
connection.begin_db_transaction
|
122
|
+
@migration.validate_check_constraint(*validate_args, **validate_options)
|
123
|
+
@migration.change_column_null(*change_args)
|
124
|
+
@migration.remove_check_constraint(*remove_args, **remove_options)
|
125
|
+
connection.commit_db_transaction
|
93
126
|
end
|
94
127
|
dir.down do
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
@migration.change_column_null(*down_args)
|
99
|
-
else
|
100
|
-
@migration.safety_assured do
|
101
|
-
@migration.execute(remove_code)
|
102
|
-
end
|
103
|
-
end
|
128
|
+
down_args = change_args.dup
|
129
|
+
down_args[2] = true
|
130
|
+
@migration.change_column_null(*down_args)
|
104
131
|
end
|
105
132
|
end
|
106
133
|
end
|
@@ -109,13 +136,13 @@ module StrongMigrations
|
|
109
136
|
# so just commit at start
|
110
137
|
def disable_transaction
|
111
138
|
if in_transaction? && !transaction_disabled
|
112
|
-
|
139
|
+
connection.commit_db_transaction
|
113
140
|
self.transaction_disabled = true
|
114
141
|
end
|
115
142
|
end
|
116
143
|
|
117
144
|
def in_transaction?
|
118
|
-
|
145
|
+
connection.open_transactions > 0
|
119
146
|
end
|
120
147
|
end
|
121
148
|
end
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module StrongMigrations
|
2
2
|
module SchemaDumper
|
3
|
-
def initialize(connection,
|
3
|
+
def initialize(connection, ...)
|
4
4
|
return super unless StrongMigrations.alphabetize_schema
|
5
5
|
|
6
|
-
super(WrappedConnection.new(connection),
|
6
|
+
super(WrappedConnection.new(connection), ...)
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
@@ -14,8 +14,19 @@ module StrongMigrations
|
|
14
14
|
@connection = connection
|
15
15
|
end
|
16
16
|
|
17
|
-
def columns(
|
18
|
-
@connection.columns(
|
17
|
+
def columns(...)
|
18
|
+
@connection.columns(...).sort_by(&:name)
|
19
|
+
end
|
20
|
+
|
21
|
+
# forward private methods with send
|
22
|
+
# method_missing cannot tell how method was called
|
23
|
+
# this is not ideal, but other solutions have drawbacks
|
24
|
+
def send(name, ...)
|
25
|
+
if respond_to?(name, true)
|
26
|
+
super
|
27
|
+
else
|
28
|
+
@connection.send(name, ...)
|
29
|
+
end
|
19
30
|
end
|
20
31
|
end
|
21
32
|
end
|