online_migrations 0.10.0 → 0.11.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 +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
|