online_migrations 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +34 -31
- data/docs/background_migrations.md +36 -4
- data/docs/configuring.md +3 -2
- data/lib/generators/online_migrations/install_generator.rb +3 -7
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +18 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +5 -3
- data/lib/generators/online_migrations/templates/migration.rb.tt +8 -3
- data/lib/generators/online_migrations/upgrade_generator.rb +33 -0
- data/lib/online_migrations/background_migrations/application_record.rb +13 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +1 -1
- data/lib/online_migrations/background_migrations/copy_column.rb +6 -20
- data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +2 -20
- data/lib/online_migrations/background_migrations/migration.rb +123 -34
- data/lib/online_migrations/background_migrations/migration_helpers.rb +0 -4
- data/lib/online_migrations/background_migrations/migration_job.rb +15 -12
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -2
- data/lib/online_migrations/background_migrations/migration_runner.rb +56 -11
- data/lib/online_migrations/background_migrations/reset_counters.rb +3 -9
- data/lib/online_migrations/background_migrations/scheduler.rb +5 -15
- data/lib/online_migrations/change_column_type_helpers.rb +68 -83
- data/lib/online_migrations/command_checker.rb +11 -29
- data/lib/online_migrations/config.rb +7 -15
- data/lib/online_migrations/copy_trigger.rb +15 -10
- data/lib/online_migrations/error_messages.rb +13 -25
- data/lib/online_migrations/foreign_keys_collector.rb +2 -2
- data/lib/online_migrations/indexes_collector.rb +3 -3
- data/lib/online_migrations/lock_retrier.rb +4 -9
- data/lib/online_migrations/schema_cache.rb +0 -6
- data/lib/online_migrations/schema_dumper.rb +1 -1
- data/lib/online_migrations/schema_statements.rb +64 -256
- data/lib/online_migrations/utils.rb +18 -56
- data/lib/online_migrations/verbose_sql_logs.rb +3 -2
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +7 -6
- metadata +8 -7
- data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
- data/lib/online_migrations/foreign_key_definition.rb +0 -17
@@ -72,6 +72,9 @@ module OnlineMigrations
|
|
72
72
|
#
|
73
73
|
# @example With additional column options
|
74
74
|
# initialize_column_type_change(:users, :name, :string, limit: 64)
|
75
|
+
# @example With type casting
|
76
|
+
# initialize_column_type_change(:users, :settings, :jsonb, type_cast_function: "jsonb")
|
77
|
+
# initialize_column_type_change(:users, :company_id, :integer, type_cast_function: Arel.sql("company_id::integer"))
|
75
78
|
#
|
76
79
|
def initialize_column_type_change(table_name, column_name, new_type, **options)
|
77
80
|
initialize_columns_type_change(table_name, [[column_name, new_type]], column_name => options)
|
@@ -91,13 +94,13 @@ module OnlineMigrations
|
|
91
94
|
# @see #initialize_column_type_change
|
92
95
|
#
|
93
96
|
def initialize_columns_type_change(table_name, columns_and_types, **options)
|
94
|
-
if !columns_and_types.is_a?(Array) || !columns_and_types.all?
|
97
|
+
if !columns_and_types.is_a?(Array) || !columns_and_types.all?(Array)
|
95
98
|
raise ArgumentError, "columns_and_types must be an array of arrays"
|
96
99
|
end
|
97
100
|
|
98
|
-
conversions = columns_and_types.
|
101
|
+
conversions = columns_and_types.to_h do |(column_name, _new_type)|
|
99
102
|
[column_name, __change_type_column(column_name)]
|
100
|
-
end
|
103
|
+
end
|
101
104
|
|
102
105
|
if (extra_keys = (options.keys - conversions.keys)).any?
|
103
106
|
raise ArgumentError, "Options has unknown keys: #{extra_keys.map(&:inspect).join(', ')}. " \
|
@@ -105,10 +108,14 @@ module OnlineMigrations
|
|
105
108
|
end
|
106
109
|
|
107
110
|
transaction do
|
111
|
+
type_cast_functions = {}.with_indifferent_access
|
112
|
+
|
108
113
|
columns_and_types.each do |(column_name, new_type)|
|
109
|
-
old_col =
|
114
|
+
old_col = column_for(table_name, column_name)
|
110
115
|
old_col_options = __options_from_column(old_col, [:collation, :comment])
|
111
116
|
column_options = options[column_name] || {}
|
117
|
+
type_cast_function = column_options.delete(:type_cast_function)
|
118
|
+
type_cast_functions[column_name] = type_cast_function if type_cast_function
|
112
119
|
tmp_column_name = conversions[column_name]
|
113
120
|
|
114
121
|
if raw_connection.server_version >= 11_00_00
|
@@ -132,7 +139,7 @@ module OnlineMigrations
|
|
132
139
|
end
|
133
140
|
end
|
134
141
|
|
135
|
-
__create_copy_triggers(table_name, conversions.keys, conversions.values)
|
142
|
+
__create_copy_triggers(table_name, conversions.keys, conversions.values, type_cast_functions: type_cast_functions)
|
136
143
|
end
|
137
144
|
end
|
138
145
|
|
@@ -239,13 +246,15 @@ module OnlineMigrations
|
|
239
246
|
def finalize_columns_type_change(table_name, *column_names)
|
240
247
|
__ensure_not_in_transaction!
|
241
248
|
|
242
|
-
conversions = column_names.
|
249
|
+
conversions = column_names.to_h do |column_name|
|
243
250
|
[column_name.to_s, __change_type_column(column_name)]
|
244
|
-
end
|
251
|
+
end
|
252
|
+
|
253
|
+
primary_key = primary_key(table_name)
|
245
254
|
|
246
255
|
conversions.each do |column_name, tmp_column_name|
|
247
|
-
old_column =
|
248
|
-
column =
|
256
|
+
old_column = column_for(table_name, column_name)
|
257
|
+
column = column_for(table_name, tmp_column_name)
|
249
258
|
|
250
259
|
# We already set default and NOT NULL for to-be-PK columns
|
251
260
|
# for PG >= 11, so can skip this case
|
@@ -264,7 +273,12 @@ module OnlineMigrations
|
|
264
273
|
__copy_foreign_keys(table_name, column_name, tmp_column_name)
|
265
274
|
__copy_check_constraints(table_name, column_name, tmp_column_name)
|
266
275
|
|
267
|
-
|
276
|
+
# Exclusion constraints were added in https://github.com/rails/rails/pull/40224.
|
277
|
+
if Utils.ar_version >= 7.1
|
278
|
+
__copy_exclusion_constraints(table_name, column_name, tmp_column_name)
|
279
|
+
end
|
280
|
+
|
281
|
+
if column_name == primary_key
|
268
282
|
__finalize_primary_key_type_change(table_name, column_name, column_names)
|
269
283
|
end
|
270
284
|
end
|
@@ -274,7 +288,7 @@ module OnlineMigrations
|
|
274
288
|
# already were swapped and which were not.
|
275
289
|
transaction do
|
276
290
|
conversions
|
277
|
-
.reject { |column_name, _tmp_column_name| column_name == primary_key
|
291
|
+
.reject { |column_name, _tmp_column_name| column_name == primary_key }
|
278
292
|
.each do |column_name, tmp_column_name|
|
279
293
|
swap_column_names(table_name, column_name, tmp_column_name)
|
280
294
|
end
|
@@ -302,40 +316,27 @@ module OnlineMigrations
|
|
302
316
|
def revert_finalize_columns_type_change(table_name, *column_names)
|
303
317
|
__ensure_not_in_transaction!
|
304
318
|
|
305
|
-
conversions = column_names.
|
319
|
+
conversions = column_names.to_h do |column_name|
|
306
320
|
[column_name.to_s, __change_type_column(column_name)]
|
307
|
-
end.to_h
|
308
|
-
|
309
|
-
transaction do
|
310
|
-
conversions
|
311
|
-
.reject { |column_name, _tmp_column_name| column_name == primary_key(table_name) }
|
312
|
-
.each do |column_name, tmp_column_name|
|
313
|
-
swap_column_names(table_name, column_name, tmp_column_name)
|
314
|
-
end
|
315
|
-
|
316
|
-
__reset_trigger_function(table_name, column_names)
|
317
321
|
end
|
318
322
|
|
319
|
-
|
320
|
-
|
321
|
-
if index.columns.include?(tmp_column_name)
|
322
|
-
remove_index(table_name, tmp_column_name, algorithm: :concurrently)
|
323
|
-
end
|
324
|
-
end
|
323
|
+
primary_key = primary_key(table_name)
|
324
|
+
primary_key_conversion = conversions.delete(primary_key)
|
325
325
|
|
326
|
-
|
327
|
-
|
328
|
-
|
326
|
+
# No need to remove indexes, foreign keys etc, because it can take a significant amount
|
327
|
+
# of time and will be automatically removed if decided to remove the column itself.
|
328
|
+
if conversions.any?
|
329
|
+
transaction do
|
330
|
+
conversions.each do |column_name, tmp_column_name|
|
331
|
+
swap_column_names(table_name, column_name, tmp_column_name)
|
329
332
|
end
|
330
|
-
end
|
331
333
|
|
332
|
-
|
333
|
-
remove_check_constraint(table_name, name: constraint.constraint_name)
|
334
|
+
__reset_trigger_function(table_name, column_names)
|
334
335
|
end
|
336
|
+
end
|
335
337
|
|
336
|
-
|
337
|
-
|
338
|
-
end
|
338
|
+
if primary_key_conversion
|
339
|
+
__finalize_primary_key_type_change(table_name, primary_key, column_names)
|
339
340
|
end
|
340
341
|
end
|
341
342
|
|
@@ -362,9 +363,9 @@ module OnlineMigrations
|
|
362
363
|
# @see #cleanup_column_type_change
|
363
364
|
#
|
364
365
|
def cleanup_columns_type_change(table_name, *column_names)
|
365
|
-
conversions = column_names.
|
366
|
-
|
367
|
-
end
|
366
|
+
conversions = column_names.index_with do |column_name|
|
367
|
+
__change_type_column(column_name)
|
368
|
+
end
|
368
369
|
|
369
370
|
transaction do
|
370
371
|
__remove_copy_triggers(table_name, conversions.keys, conversions.values)
|
@@ -392,8 +393,8 @@ module OnlineMigrations
|
|
392
393
|
CopyTrigger.on_table(table_name, connection: self).name(from_column, to_column)
|
393
394
|
end
|
394
395
|
|
395
|
-
def __create_copy_triggers(table_name,
|
396
|
-
CopyTrigger.on_table(table_name, connection: self).create(
|
396
|
+
def __create_copy_triggers(table_name, from_columns, to_columns, type_cast_functions: nil)
|
397
|
+
CopyTrigger.on_table(table_name, connection: self).create(from_columns, to_columns, type_cast_functions: type_cast_functions)
|
397
398
|
end
|
398
399
|
|
399
400
|
def __remove_copy_triggers(table_name, from_column, to_column)
|
@@ -419,8 +420,6 @@ module OnlineMigrations
|
|
419
420
|
name = index.name.gsub(from_column, to_column)
|
420
421
|
end
|
421
422
|
|
422
|
-
# Generate a shorter name if needed.
|
423
|
-
max_identifier_length = 63 # could use just `max_identifier_length` method for ActiveRecord >= 5.0.
|
424
423
|
name = index_name(table_name, new_columns) if !name || name.length > max_identifier_length
|
425
424
|
|
426
425
|
options = {
|
@@ -433,8 +432,7 @@ module OnlineMigrations
|
|
433
432
|
options[:using] = index.using if index.using
|
434
433
|
options[:where] = index.where if index.where
|
435
434
|
|
436
|
-
|
437
|
-
if Utils.ar_version >= 5.2 && !index.opclasses.blank?
|
435
|
+
if index.opclasses.present?
|
438
436
|
opclasses = index.opclasses.dup
|
439
437
|
|
440
438
|
# Copy the operator classes for the old column (if any) to the new column.
|
@@ -475,23 +473,39 @@ module OnlineMigrations
|
|
475
473
|
end
|
476
474
|
|
477
475
|
def __copy_check_constraints(table_name, from_column, to_column)
|
478
|
-
|
479
|
-
|
480
|
-
|
476
|
+
check_constraints = check_constraints(table_name).select { |c| c.expression.include?(from_column) }
|
477
|
+
|
478
|
+
check_constraints.each do |check|
|
479
|
+
new_expression = check.expression.gsub(from_column, to_column)
|
481
480
|
|
482
481
|
add_check_constraint(table_name, new_expression, validate: false)
|
483
482
|
|
484
|
-
if check
|
483
|
+
if check.validated?
|
485
484
|
validate_check_constraint(table_name, expression: new_expression)
|
486
485
|
end
|
487
486
|
end
|
488
487
|
end
|
489
488
|
|
489
|
+
def __copy_exclusion_constraints(table_name, from_column, to_column)
|
490
|
+
exclusion_constraints = exclusion_constraints(table_name).select { |c| c.expression.include?(from_column) }
|
491
|
+
|
492
|
+
exclusion_constraints.each do |constraint|
|
493
|
+
new_expression = constraint.expression.gsub(from_column, to_column)
|
494
|
+
add_exclusion_constraint(
|
495
|
+
table_name,
|
496
|
+
new_expression,
|
497
|
+
using: constraint.using,
|
498
|
+
where: constraint.where,
|
499
|
+
deferrable: constraint.deferrable
|
500
|
+
)
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
490
504
|
def __set_not_null(table_name, column_name)
|
491
505
|
# For PG >= 12 we can "promote" CHECK constraint to NOT NULL constraint:
|
492
506
|
# https://github.com/postgres/postgres/commit/bbb96c3704c041d139181c6601e5bc770e045d26
|
493
507
|
if raw_connection.server_version >= 12_00_00
|
494
|
-
execute(
|
508
|
+
execute(<<~SQL)
|
495
509
|
ALTER TABLE #{quote_table_name(table_name)}
|
496
510
|
ALTER #{quote_column_name(column_name)}
|
497
511
|
SET NOT NULL
|
@@ -501,7 +515,7 @@ module OnlineMigrations
|
|
501
515
|
# through PG internal tables.
|
502
516
|
# In-depth analysis of implications of this was made, so this approach
|
503
517
|
# is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
|
504
|
-
execute(
|
518
|
+
execute(<<~SQL)
|
505
519
|
UPDATE pg_catalog.pg_attribute
|
506
520
|
SET attnotnull = true
|
507
521
|
WHERE attrelid = #{quote(table_name)}::regclass
|
@@ -510,37 +524,8 @@ module OnlineMigrations
|
|
510
524
|
end
|
511
525
|
end
|
512
526
|
|
513
|
-
def __check_constraints_for(table_name, column_name)
|
514
|
-
__check_constraints(table_name).select { |c| c["column_name"] == column_name }
|
515
|
-
end
|
516
|
-
|
517
|
-
def __check_constraints(table_name)
|
518
|
-
schema = __schema_for_table(table_name)
|
519
|
-
|
520
|
-
check_sql = <<-SQL.strip_heredoc
|
521
|
-
SELECT
|
522
|
-
ccu.column_name as column_name,
|
523
|
-
con.conname as constraint_name,
|
524
|
-
pg_get_constraintdef(con.oid) as constraint_def,
|
525
|
-
con.convalidated AS valid
|
526
|
-
FROM pg_catalog.pg_constraint con
|
527
|
-
INNER JOIN pg_catalog.pg_class rel
|
528
|
-
ON rel.oid = con.conrelid
|
529
|
-
INNER JOIN pg_catalog.pg_namespace nsp
|
530
|
-
ON nsp.oid = con.connamespace
|
531
|
-
INNER JOIN information_schema.constraint_column_usage ccu
|
532
|
-
ON con.conname = ccu.constraint_name
|
533
|
-
AND rel.relname = ccu.table_name
|
534
|
-
WHERE rel.relname = #{quote(table_name)}
|
535
|
-
AND con.contype = 'c'
|
536
|
-
AND nsp.nspname = #{schema}
|
537
|
-
SQL
|
538
|
-
|
539
|
-
select_all(check_sql)
|
540
|
-
end
|
541
|
-
|
542
527
|
def __rename_constraint(table_name, old_name, new_name)
|
543
|
-
execute(
|
528
|
+
execute(<<~SQL)
|
544
529
|
ALTER TABLE #{quote_table_name(table_name)}
|
545
530
|
RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}
|
546
531
|
SQL
|
@@ -612,7 +597,7 @@ module OnlineMigrations
|
|
612
597
|
def __referencing_table_names(table_name)
|
613
598
|
schema = __schema_for_table(table_name)
|
614
599
|
|
615
|
-
select_values(
|
600
|
+
select_values(<<~SQL)
|
616
601
|
SELECT DISTINCT con.conrelid::regclass::text AS conrelname
|
617
602
|
FROM pg_catalog.pg_constraint con
|
618
603
|
INNER JOIN pg_catalog.pg_namespace nsp
|
@@ -154,13 +154,7 @@ module OnlineMigrations
|
|
154
154
|
check_columns_removal(command, *args, **options)
|
155
155
|
else
|
156
156
|
if respond_to?(command, true)
|
157
|
-
|
158
|
-
# Workaround for Active Record < 5 change_column_default
|
159
|
-
# not accepting options.
|
160
|
-
send(command, *args, **options, &block)
|
161
|
-
else
|
162
|
-
send(command, *args, &block)
|
163
|
-
end
|
157
|
+
send(command, *args, **options, &block)
|
164
158
|
else
|
165
159
|
# assume it is safe
|
166
160
|
true
|
@@ -362,9 +356,7 @@ module OnlineMigrations
|
|
362
356
|
precision = options[:precision] || options[:limit] || 6
|
363
357
|
existing_precision = existing_column.precision || existing_column.limit || 6
|
364
358
|
|
365
|
-
|
366
|
-
(existing_type == :interval || (Utils.ar_version < 6.1 && existing_column.sql_type.start_with?("interval"))) &&
|
367
|
-
precision >= existing_precision
|
359
|
+
existing_type == :interval && precision >= existing_precision
|
368
360
|
when :inet
|
369
361
|
existing_type == :cidr
|
370
362
|
else
|
@@ -487,7 +479,7 @@ module OnlineMigrations
|
|
487
479
|
|
488
480
|
def add_reference(table_name, ref_name, **options)
|
489
481
|
# Always added by default in 5.0+
|
490
|
-
index = options.fetch(:index)
|
482
|
+
index = options.fetch(:index, true)
|
491
483
|
|
492
484
|
if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
|
493
485
|
raise_error :add_hash_index
|
@@ -515,7 +507,7 @@ module OnlineMigrations
|
|
515
507
|
end
|
516
508
|
|
517
509
|
if !options[:polymorphic]
|
518
|
-
type = (options[:type] ||
|
510
|
+
type = (options[:type] || :bigint).to_sym
|
519
511
|
column_name = "#{ref_name}_id"
|
520
512
|
|
521
513
|
foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
|
@@ -573,9 +565,9 @@ module OnlineMigrations
|
|
573
565
|
end
|
574
566
|
|
575
567
|
if index_def
|
576
|
-
existing_options = [:name, :columns, :unique, :where, :type, :using, :opclasses].
|
568
|
+
existing_options = [:name, :columns, :unique, :where, :type, :using, :opclasses].filter_map do |option|
|
577
569
|
[option, index_def.public_send(option)] if index_def.respond_to?(option)
|
578
|
-
end.
|
570
|
+
end.to_h
|
579
571
|
|
580
572
|
@removed_indexes << IndexDefinition.new(table: table_name, **existing_options)
|
581
573
|
end
|
@@ -738,20 +730,10 @@ module OnlineMigrations
|
|
738
730
|
template = OnlineMigrations.config.error_messages.fetch(message_key)
|
739
731
|
|
740
732
|
vars[:migration_name] = @migration.name
|
741
|
-
vars[:migration_parent] = Utils.
|
742
|
-
vars[:model_parent] = Utils.model_parent_string
|
733
|
+
vars[:migration_parent] = "ActiveRecord::Migration[#{Utils.ar_version}]"
|
743
734
|
vars[:ar_version] = Utils.ar_version
|
744
735
|
|
745
|
-
|
746
|
-
message = ERB.new(template, trim_mode: "<>").result_with_hash(vars)
|
747
|
-
else
|
748
|
-
# `result_with_hash` was added in ruby 2.5
|
749
|
-
b = TOPLEVEL_BINDING.dup
|
750
|
-
vars.each_pair do |key, value|
|
751
|
-
b.local_variable_set(key, value)
|
752
|
-
end
|
753
|
-
message = ERB.new(template, nil, "<>").result(b)
|
754
|
-
end
|
736
|
+
message = ERB.new(template, trim_mode: "<>").result_with_hash(vars)
|
755
737
|
|
756
738
|
if (link = ERROR_MESSAGE_TO_LINK[message_key])
|
757
739
|
message += "\nFor more details, see https://github.com/fatkodima/online_migrations##{link}"
|
@@ -790,7 +772,7 @@ module OnlineMigrations
|
|
790
772
|
end
|
791
773
|
|
792
774
|
def crud_blocked?
|
793
|
-
locks_query =
|
775
|
+
locks_query = <<~SQL
|
794
776
|
SELECT relation::regclass::text
|
795
777
|
FROM pg_locks
|
796
778
|
WHERE mode IN ('ShareLock', 'ShareRowExclusiveLock', 'ExclusiveLock', 'AccessExclusiveLock')
|
@@ -808,7 +790,7 @@ module OnlineMigrations
|
|
808
790
|
end
|
809
791
|
|
810
792
|
def check_constraints(table_name)
|
811
|
-
constraints_query =
|
793
|
+
constraints_query = <<~SQL
|
812
794
|
SELECT pg_get_constraintdef(oid) AS def
|
813
795
|
FROM pg_constraint
|
814
796
|
WHERE contype = 'c'
|
@@ -829,7 +811,7 @@ module OnlineMigrations
|
|
829
811
|
|
830
812
|
def check_mismatched_foreign_key_type(table_name, column_name, type, **options)
|
831
813
|
column_name = column_name.to_s
|
832
|
-
ref_name = column_name.
|
814
|
+
ref_name = column_name.delete_suffix("_id")
|
833
815
|
|
834
816
|
if like_foreign_key?(column_name, type)
|
835
817
|
foreign_table_name = Utils.foreign_table_name(ref_name, options)
|
@@ -16,7 +16,6 @@ module OnlineMigrations
|
|
16
16
|
#
|
17
17
|
def start_after=(value)
|
18
18
|
if value.is_a?(Hash)
|
19
|
-
ensure_supports_multiple_dbs
|
20
19
|
@start_after = value.stringify_keys
|
21
20
|
else
|
22
21
|
@start_after = value
|
@@ -49,7 +48,6 @@ module OnlineMigrations
|
|
49
48
|
#
|
50
49
|
def target_version=(value)
|
51
50
|
if value.is_a?(Hash)
|
52
|
-
ensure_supports_multiple_dbs
|
53
51
|
@target_version = value.stringify_keys
|
54
52
|
else
|
55
53
|
@target_version = value
|
@@ -148,7 +146,7 @@ module OnlineMigrations
|
|
148
146
|
# Returns a list of enabled checks
|
149
147
|
#
|
150
148
|
# All checks are enabled by default. To disable/enable a check use `disable_check`/`enable_check`.
|
151
|
-
# For the list of available checks look at `
|
149
|
+
# For the list of available checks look at the `error_messages.rb` file.
|
152
150
|
#
|
153
151
|
# @return [Array]
|
154
152
|
#
|
@@ -184,7 +182,7 @@ module OnlineMigrations
|
|
184
182
|
attempts: 30,
|
185
183
|
base_delay: 0.01.seconds,
|
186
184
|
max_delay: 1.minute,
|
187
|
-
lock_timeout: 0.
|
185
|
+
lock_timeout: 0.2.seconds
|
188
186
|
)
|
189
187
|
|
190
188
|
@background_migrations = BackgroundMigrations::Config.new
|
@@ -196,8 +194,8 @@ module OnlineMigrations
|
|
196
194
|
@check_down = false
|
197
195
|
@auto_analyze = false
|
198
196
|
@alphabetize_schema = false
|
199
|
-
@enabled_checks = @error_messages.keys.
|
200
|
-
@verbose_sql_logs = defined?(Rails.env) && Rails.env.production?
|
197
|
+
@enabled_checks = @error_messages.keys.index_with({})
|
198
|
+
@verbose_sql_logs = defined?(Rails.env) && (Rails.env.production? || Rails.env.staging?)
|
201
199
|
end
|
202
200
|
|
203
201
|
def lock_retrier=(value)
|
@@ -210,7 +208,7 @@ module OnlineMigrations
|
|
210
208
|
|
211
209
|
# Enables specific check
|
212
210
|
#
|
213
|
-
# For the list of available checks look at `
|
211
|
+
# For the list of available checks look at the `error_messages.rb` file.
|
214
212
|
#
|
215
213
|
# @param name [Symbol] check name
|
216
214
|
# @param start_after [Integer] migration version from which this check will be performed
|
@@ -222,7 +220,7 @@ module OnlineMigrations
|
|
222
220
|
|
223
221
|
# Disables specific check
|
224
222
|
#
|
225
|
-
# For the list of available checks look at `
|
223
|
+
# For the list of available checks look at the `error_messages.rb` file.
|
226
224
|
#
|
227
225
|
# @param name [Symbol] check name
|
228
226
|
# @return [void]
|
@@ -233,7 +231,7 @@ module OnlineMigrations
|
|
233
231
|
|
234
232
|
# Test whether specific check is enabled
|
235
233
|
#
|
236
|
-
# For the list of available checks look at `
|
234
|
+
# For the list of available checks look at the `error_messages.rb` file.
|
237
235
|
#
|
238
236
|
# @param name [Symbol] check name
|
239
237
|
# @param version [Integer] migration version
|
@@ -272,12 +270,6 @@ module OnlineMigrations
|
|
272
270
|
end
|
273
271
|
|
274
272
|
private
|
275
|
-
def ensure_supports_multiple_dbs
|
276
|
-
if !Utils.supports_multiple_dbs?
|
277
|
-
raise "OnlineMigrations does not support multiple databases for Active Record < 6.1"
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
273
|
def db_config_name
|
282
274
|
connection = OnlineMigrations.current_migration.connection
|
283
275
|
connection.pool.db_config.name
|
@@ -18,12 +18,12 @@ module OnlineMigrations
|
|
18
18
|
"trigger_#{hashed_identifier}"
|
19
19
|
end
|
20
20
|
|
21
|
-
def create(from_columns, to_columns)
|
21
|
+
def create(from_columns, to_columns, type_cast_functions: {})
|
22
22
|
from_columns, to_columns = normalize_column_names(from_columns, to_columns)
|
23
23
|
trigger_name = name(from_columns, to_columns)
|
24
|
-
assignment_clauses = assignment_clauses_for_columns(from_columns, to_columns)
|
24
|
+
assignment_clauses = assignment_clauses_for_columns(from_columns, to_columns, type_cast_functions)
|
25
25
|
|
26
|
-
connection.execute(
|
26
|
+
connection.execute(<<~SQL)
|
27
27
|
CREATE OR REPLACE FUNCTION #{trigger_name}() RETURNS TRIGGER AS $$
|
28
28
|
BEGIN
|
29
29
|
#{assignment_clauses};
|
@@ -32,11 +32,11 @@ module OnlineMigrations
|
|
32
32
|
$$ LANGUAGE plpgsql;
|
33
33
|
SQL
|
34
34
|
|
35
|
-
connection.execute(
|
35
|
+
connection.execute(<<~SQL)
|
36
36
|
DROP TRIGGER IF EXISTS #{trigger_name} ON #{quoted_table_name}
|
37
37
|
SQL
|
38
38
|
|
39
|
-
connection.execute(
|
39
|
+
connection.execute(<<~SQL)
|
40
40
|
CREATE TRIGGER #{trigger_name}
|
41
41
|
BEFORE INSERT OR UPDATE
|
42
42
|
ON #{quoted_table_name}
|
@@ -75,14 +75,19 @@ module OnlineMigrations
|
|
75
75
|
[from_columns, to_columns]
|
76
76
|
end
|
77
77
|
|
78
|
-
def assignment_clauses_for_columns(from_columns, to_columns)
|
78
|
+
def assignment_clauses_for_columns(from_columns, to_columns, type_cast_functions)
|
79
79
|
combined_column_names = to_columns.zip(from_columns)
|
80
80
|
|
81
81
|
assignment_clauses = combined_column_names.map do |(new_name, old_name)|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
82
|
+
quoted_new_name = connection.quote_column_name(new_name)
|
83
|
+
quoted_old_name = connection.quote_column_name(old_name)
|
84
|
+
type_cast_function = type_cast_functions[old_name]
|
85
|
+
|
86
|
+
if type_cast_function
|
87
|
+
"NEW.#{quoted_new_name} := #{type_cast_function.gsub(old_name.to_s, "NEW.#{quoted_old_name}")}"
|
88
|
+
else
|
89
|
+
"NEW.#{quoted_new_name} := NEW.#{quoted_old_name}"
|
90
|
+
end
|
86
91
|
end
|
87
92
|
|
88
93
|
assignment_clauses.join(";\n ")
|
@@ -109,14 +109,8 @@ A safer approach is to:
|
|
109
109
|
|
110
110
|
1. ignore the column:
|
111
111
|
|
112
|
-
class <%= model %> <
|
113
|
-
<% if ar_version >= 5 %>
|
112
|
+
class <%= model %> < ApplicationRecord
|
114
113
|
self.ignored_columns = [\"<%= column_name %>\"]
|
115
|
-
<% else %>
|
116
|
-
def self.columns
|
117
|
-
super.reject { |c| c.name == \"<%= column_name %>\" }
|
118
|
-
end
|
119
|
-
<% end %>
|
120
114
|
end
|
121
115
|
|
122
116
|
2. deploy
|
@@ -157,13 +151,7 @@ It will use a combination of a VIEW and column aliasing to work with both column
|
|
157
151
|
<% if enumerate_columns_in_select_statements %>
|
158
152
|
5. Ignore old column
|
159
153
|
|
160
|
-
<% if ar_version >= 5 %>
|
161
154
|
self.ignored_columns = [:<%= column_name %>]
|
162
|
-
<% else %>
|
163
|
-
def self.columns
|
164
|
-
super.reject { |c| c.name == \"<%= column_name %>\" }
|
165
|
-
end
|
166
|
-
<% end %>
|
167
155
|
|
168
156
|
6. Deploy
|
169
157
|
7. Remove the column rename config from step 1
|
@@ -216,6 +204,8 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
216
204
|
|
217
205
|
def up
|
218
206
|
<%= backfill_code %>
|
207
|
+
# You can use `backfill_column_for_type_change_in_background` if want to
|
208
|
+
# backfill using background migrations.
|
219
209
|
end
|
220
210
|
|
221
211
|
def down
|
@@ -223,7 +213,10 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
223
213
|
end
|
224
214
|
end
|
225
215
|
|
226
|
-
3.
|
216
|
+
3. Make sure your application works with values in both formats (when read from the database, converting
|
217
|
+
during writes works automatically). For most column type changes, this does not need any updates in the app.
|
218
|
+
4. Deploy
|
219
|
+
5. Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place:
|
227
220
|
|
228
221
|
class Finalize<%= migration_name %> < <%= migration_parent %>
|
229
222
|
disable_ddl_transaction!
|
@@ -233,8 +226,8 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
233
226
|
end
|
234
227
|
end
|
235
228
|
|
236
|
-
|
237
|
-
|
229
|
+
6. Deploy
|
230
|
+
7. Finally, if everything works as expected, remove copy trigger and old column:
|
238
231
|
|
239
232
|
class Cleanup<%= migration_name %> < <%= migration_parent %>
|
240
233
|
def up
|
@@ -246,7 +239,8 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
246
239
|
end
|
247
240
|
end
|
248
241
|
|
249
|
-
|
242
|
+
8. Remove changes from step 3, if any
|
243
|
+
9. Deploy",
|
250
244
|
|
251
245
|
change_column_default:
|
252
246
|
"Partial writes are enabled, which can cause incorrect values
|
@@ -303,14 +297,8 @@ A safer approach is to:
|
|
303
297
|
|
304
298
|
1. Ignore the column(s):
|
305
299
|
|
306
|
-
class <%= model %> <
|
307
|
-
<% if ar_version >= 5 %>
|
300
|
+
class <%= model %> < ApplicationRecord
|
308
301
|
self.ignored_columns = <%= columns %>
|
309
|
-
<% else %>
|
310
|
-
def self.columns
|
311
|
-
super.reject { |c| <%= columns %>.include?(c.name) }
|
312
|
-
end
|
313
|
-
<% end %>
|
314
302
|
end
|
315
303
|
|
316
304
|
2. Deploy
|
@@ -504,7 +492,7 @@ execute call, so cannot help you here. Make really sure that what
|
|
504
492
|
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
|
505
493
|
|
506
494
|
multiple_foreign_keys:
|
507
|
-
"Adding multiple foreign keys in a single migration blocks
|
495
|
+
"Adding multiple foreign keys in a single migration blocks writes on all involved tables until migration is completed.
|
508
496
|
Avoid adding foreign key more than once per migration file, unless the source and target tables are identical.",
|
509
497
|
|
510
498
|
drop_table_multiple_foreign_keys:
|
@@ -12,8 +12,8 @@ module OnlineMigrations
|
|
12
12
|
@indexes = []
|
13
13
|
end
|
14
14
|
|
15
|
-
def collect
|
16
|
-
|
15
|
+
def collect
|
16
|
+
yield self
|
17
17
|
end
|
18
18
|
|
19
19
|
def index(_column_name, **options)
|
@@ -21,7 +21,7 @@ module OnlineMigrations
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def references(*_ref_names, **options)
|
24
|
-
index = options.fetch(:index)
|
24
|
+
index = options.fetch(:index, true)
|
25
25
|
|
26
26
|
if index
|
27
27
|
using = index.is_a?(Hash) ? index[:using].to_s : nil
|