online_migrations 0.25.0 → 0.27.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 +31 -0
- data/README.md +18 -73
- data/docs/0.27-upgrade.md +24 -0
- data/docs/background_data_migrations.md +200 -101
- data/docs/background_schema_migrations.md +2 -2
- data/docs/configuring.md +8 -0
- data/lib/generators/online_migrations/{background_migration_generator.rb → data_migration_generator.rb} +4 -4
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/change_background_data_migrations.rb.tt +34 -0
- data/lib/generators/online_migrations/templates/{background_data_migration.rb.tt → data_migration.rb.tt} +8 -9
- data/lib/generators/online_migrations/templates/initializer.rb.tt +22 -25
- data/lib/generators/online_migrations/templates/install_migration.rb.tt +9 -40
- data/lib/generators/online_migrations/upgrade_generator.rb +16 -8
- data/lib/online_migrations/active_record_batch_enumerator.rb +8 -0
- data/lib/online_migrations/background_data_migrations/backfill_column.rb +50 -0
- data/lib/online_migrations/background_data_migrations/config.rb +62 -0
- data/lib/online_migrations/{background_migrations → background_data_migrations}/copy_column.rb +15 -28
- data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_associated_records.rb +9 -5
- data/lib/online_migrations/{background_migrations → background_data_migrations}/delete_orphaned_records.rb +5 -9
- data/lib/online_migrations/background_data_migrations/migration.rb +312 -0
- data/lib/online_migrations/{background_migrations → background_data_migrations}/migration_helpers.rb +72 -61
- data/lib/online_migrations/background_data_migrations/migration_job.rb +158 -0
- data/lib/online_migrations/background_data_migrations/migration_status_validator.rb +65 -0
- data/lib/online_migrations/{background_migrations → background_data_migrations}/perform_action_on_relation.rb +5 -5
- data/lib/online_migrations/{background_migrations → background_data_migrations}/reset_counters.rb +5 -5
- data/lib/online_migrations/background_data_migrations/scheduler.rb +78 -0
- data/lib/online_migrations/background_data_migrations/ticker.rb +62 -0
- data/lib/online_migrations/background_schema_migrations/config.rb +2 -2
- data/lib/online_migrations/background_schema_migrations/migration.rb +57 -127
- data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +26 -47
- data/lib/online_migrations/background_schema_migrations/migration_runner.rb +43 -97
- data/lib/online_migrations/background_schema_migrations/scheduler.rb +2 -2
- data/lib/online_migrations/batch_iterator.rb +7 -4
- data/lib/online_migrations/change_column_type_helpers.rb +75 -14
- data/lib/online_migrations/command_checker.rb +32 -20
- data/lib/online_migrations/config.rb +12 -4
- data/lib/online_migrations/data_migration.rb +127 -0
- data/lib/online_migrations/error_messages.rb +16 -0
- data/lib/online_migrations/index_definition.rb +1 -1
- data/lib/online_migrations/lock_retrier.rb +5 -2
- data/lib/online_migrations/migration.rb +8 -1
- data/lib/online_migrations/schema_cache.rb +0 -78
- data/lib/online_migrations/schema_statements.rb +18 -74
- data/lib/online_migrations/shard_aware.rb +44 -0
- data/lib/online_migrations/utils.rb +1 -20
- data/lib/online_migrations/verbose_sql_logs.rb +1 -7
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +19 -19
- metadata +25 -24
- data/lib/online_migrations/background_migration.rb +0 -64
- data/lib/online_migrations/background_migrations/backfill_column.rb +0 -54
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +0 -29
- data/lib/online_migrations/background_migrations/config.rb +0 -74
- data/lib/online_migrations/background_migrations/migration.rb +0 -329
- data/lib/online_migrations/background_migrations/migration_job.rb +0 -109
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +0 -66
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +0 -29
- data/lib/online_migrations/background_migrations/migration_runner.rb +0 -161
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +0 -48
- data/lib/online_migrations/background_migrations/scheduler.rb +0 -42
@@ -267,11 +267,7 @@ module OnlineMigrations
|
|
267
267
|
__copy_indexes(table_name, column_name, tmp_column_name)
|
268
268
|
__copy_foreign_keys(table_name, column_name, tmp_column_name)
|
269
269
|
__copy_check_constraints(table_name, column_name, tmp_column_name)
|
270
|
-
|
271
|
-
# Exclusion constraints were added in https://github.com/rails/rails/pull/40224.
|
272
|
-
if Utils.ar_version >= 7.1
|
273
|
-
__copy_exclusion_constraints(table_name, column_name, tmp_column_name)
|
274
|
-
end
|
270
|
+
__copy_exclusion_constraints(table_name, column_name, tmp_column_name)
|
275
271
|
|
276
272
|
if column_name == primary_key
|
277
273
|
__finalize_primary_key_type_change(table_name, column_name, column_names)
|
@@ -358,13 +354,22 @@ module OnlineMigrations
|
|
358
354
|
# @see #cleanup_column_type_change
|
359
355
|
#
|
360
356
|
def cleanup_columns_type_change(table_name, *column_names)
|
361
|
-
|
362
|
-
|
357
|
+
tmp_column_names = column_names.map { |column_name| __change_type_column(column_name) }
|
358
|
+
|
359
|
+
# Safely remove existing indexes and foreign keys first, if any.
|
360
|
+
tmp_column_names.each do |column_name|
|
361
|
+
__indexes_for(table_name, column_name).each do |index|
|
362
|
+
remove_index(table_name, name: index.name, algorithm: :concurrently)
|
363
|
+
end
|
364
|
+
|
365
|
+
__foreign_keys_for(table_name, column_name).each do |fk|
|
366
|
+
remove_foreign_key(table_name, name: fk.name)
|
367
|
+
end
|
363
368
|
end
|
364
369
|
|
365
370
|
transaction do
|
366
|
-
__remove_copy_triggers(table_name,
|
367
|
-
remove_columns(table_name, *
|
371
|
+
__remove_copy_triggers(table_name, column_names, tmp_column_names)
|
372
|
+
remove_columns(table_name, *tmp_column_names)
|
368
373
|
end
|
369
374
|
end
|
370
375
|
|
@@ -401,12 +406,13 @@ module OnlineMigrations
|
|
401
406
|
to_column = to_column.to_s
|
402
407
|
|
403
408
|
__indexes_for(table_name, from_column).each do |index|
|
409
|
+
columns = index.columns
|
404
410
|
new_columns =
|
405
411
|
# Expression index.
|
406
|
-
if
|
407
|
-
|
412
|
+
if columns.is_a?(String)
|
413
|
+
columns.gsub(/\b#{from_column}\b/, to_column)
|
408
414
|
else
|
409
|
-
|
415
|
+
columns.map do |column|
|
410
416
|
column == from_column ? to_column : column
|
411
417
|
end
|
412
418
|
end
|
@@ -429,6 +435,16 @@ module OnlineMigrations
|
|
429
435
|
options[:opclass] = opclasses
|
430
436
|
end
|
431
437
|
|
438
|
+
# If the index name is custom - do not rely on auto generated index names, because this
|
439
|
+
# doesn't work (idempotency check does not work, rails does not consider ':where' option)
|
440
|
+
# when there are partial and "classic" indexes on the same columns.
|
441
|
+
if index.name == index_name(table_name, columns)
|
442
|
+
options[:name] = index_name(table_name, new_columns)
|
443
|
+
else
|
444
|
+
truncated_index_name = index.name[0, max_identifier_length - "_2".length]
|
445
|
+
options[:name] = "#{truncated_index_name}_2"
|
446
|
+
end
|
447
|
+
|
432
448
|
add_index(table_name, new_columns, **options, algorithm: :concurrently)
|
433
449
|
end
|
434
450
|
end
|
@@ -461,10 +477,11 @@ module OnlineMigrations
|
|
461
477
|
end
|
462
478
|
|
463
479
|
def __copy_check_constraints(table_name, from_column, to_column)
|
464
|
-
|
480
|
+
from_column_re = /\b#{from_column}\b/
|
481
|
+
check_constraints = check_constraints(table_name).select { |c| c.expression.match?(from_column_re) }
|
465
482
|
|
466
483
|
check_constraints.each do |check|
|
467
|
-
new_expression = check.expression.gsub(
|
484
|
+
new_expression = check.expression.gsub(from_column_re, to_column)
|
468
485
|
|
469
486
|
add_check_constraint(table_name, new_expression, validate: false)
|
470
487
|
|
@@ -511,6 +528,13 @@ module OnlineMigrations
|
|
511
528
|
# Lock the table explicitly to prevent new rows being inserted
|
512
529
|
execute("LOCK TABLE #{quoted_table_name} IN ACCESS EXCLUSIVE MODE")
|
513
530
|
|
531
|
+
# https://stackoverflow.com/questions/47301722/how-can-view-depends-on-primary-key-constraint-in-postgres
|
532
|
+
#
|
533
|
+
# PG::DependentObjectsStillExist: ERROR: cannot drop constraint appointments_pkey on table appointments because other objects depend on it (PG::DependentObjectsStillExist)
|
534
|
+
# DETAIL: view appointment_statuses depends on constraint appointments_pkey on table appointments
|
535
|
+
# HINT: Use DROP ... CASCADE to drop the dependent objects too.
|
536
|
+
views = __drop_dependent_views(table_name)
|
537
|
+
|
514
538
|
swap_column_names(table_name, column_name, tmp_column_name)
|
515
539
|
|
516
540
|
# We need to update the trigger function in order to make PostgreSQL to
|
@@ -530,6 +554,8 @@ module OnlineMigrations
|
|
530
554
|
execute("ALTER TABLE #{quoted_table_name} DROP CONSTRAINT #{quote_table_name(pkey_constraint_name)}")
|
531
555
|
rename_index(table_name, pkey_index_name, pkey_constraint_name)
|
532
556
|
execute("ALTER TABLE #{quoted_table_name} ADD CONSTRAINT #{quote_table_name(pkey_constraint_name)} PRIMARY KEY USING INDEX #{quote_table_name(pkey_constraint_name)}")
|
557
|
+
|
558
|
+
__recreate_views(views)
|
533
559
|
end
|
534
560
|
end
|
535
561
|
|
@@ -579,5 +605,40 @@ module OnlineMigrations
|
|
579
605
|
function_name = __copy_triggers_name(table_name, column_names, tmp_column_names)
|
580
606
|
execute("ALTER FUNCTION #{quote_table_name(function_name)}() RESET ALL")
|
581
607
|
end
|
608
|
+
|
609
|
+
# https://stackoverflow.com/questions/69458819/is-there-any-way-to-list-all-the-views-related-to-a-table-in-the-existing-postgr
|
610
|
+
def __drop_dependent_views(table_name)
|
611
|
+
views = select_all(<<~SQL)
|
612
|
+
SELECT
|
613
|
+
u.view_schema AS schema,
|
614
|
+
u.view_name AS name,
|
615
|
+
v.view_definition AS definition,
|
616
|
+
c.relkind = 'm' AS materialized
|
617
|
+
FROM information_schema.view_table_usage u
|
618
|
+
JOIN information_schema.views v ON u.view_schema = v.table_schema
|
619
|
+
AND u.view_name = v.table_name
|
620
|
+
JOIN pg_class c ON c.relname = u.view_name
|
621
|
+
WHERE u.table_schema NOT IN ('information_schema', 'pg_catalog')
|
622
|
+
AND u.table_name = #{quote(table_name)}
|
623
|
+
ORDER BY u.view_schema, u.view_name
|
624
|
+
SQL
|
625
|
+
|
626
|
+
views.each do |row|
|
627
|
+
execute("DROP VIEW #{quote_table_name(row['schema'])}.#{quote_table_name(row['name'])}")
|
628
|
+
end
|
629
|
+
|
630
|
+
views
|
631
|
+
end
|
632
|
+
|
633
|
+
def __recreate_views(views)
|
634
|
+
views.each do |row|
|
635
|
+
schema, name, definition, materialized = row.values_at("schema", "name", "definition", "materialized")
|
636
|
+
|
637
|
+
execute(<<~SQL)
|
638
|
+
CREATE#{' MATERIALIZED' if materialized} VIEW #{quote_table_name(schema)}.#{quote_table_name(name)} AS
|
639
|
+
#{definition}
|
640
|
+
SQL
|
641
|
+
end
|
642
|
+
end
|
582
643
|
end
|
583
644
|
end
|
@@ -80,6 +80,7 @@ module OnlineMigrations
|
|
80
80
|
add_inheritance_column: "adding-a-single-table-inheritance-column",
|
81
81
|
mismatched_foreign_key_type: "mismatched-reference-column-types",
|
82
82
|
}
|
83
|
+
private_constant :ERROR_MESSAGE_TO_LINK
|
83
84
|
|
84
85
|
def check_database_version
|
85
86
|
return if defined?(@database_version_checked)
|
@@ -375,6 +376,31 @@ module OnlineMigrations
|
|
375
376
|
cleanup_code: command_str(:cleanup_column_type_change, table_name, column_name),
|
376
377
|
cleanup_down_code: command_str(:initialize_column_type_change, table_name, column_name, existing_type)
|
377
378
|
end
|
379
|
+
|
380
|
+
# Constraints must be rechecked.
|
381
|
+
# PostgreSQL recommends dropping constraints before and adding them back.
|
382
|
+
# https://www.postgresql.org/docs/current/ddl-alter.html#DDL-ALTER-COLUMN-TYPE
|
383
|
+
constraints = connection.check_constraints(table_name).select do |c|
|
384
|
+
c.validated? && c.expression.match?(/\b#{column_name}\b/)
|
385
|
+
end
|
386
|
+
|
387
|
+
if constraints.any?
|
388
|
+
change_commands = constraints.map do |c|
|
389
|
+
command_str(:remove_check_constraint, table_name, c.expression, { name: c.name })
|
390
|
+
end
|
391
|
+
change_commands << command_str(:change_column, table_name, column_name, type, **options)
|
392
|
+
constraints.each do |c|
|
393
|
+
change_commands << command_str(:add_check_constraint, table_name, c.expression, { name: c.name, validate: false })
|
394
|
+
end
|
395
|
+
|
396
|
+
validate_commands = constraints.map do |c|
|
397
|
+
command_str(:validate_check_constraint, table_name, { name: c.name })
|
398
|
+
end
|
399
|
+
|
400
|
+
raise_error :change_column_constraint,
|
401
|
+
change_column_code: change_commands.join("\n "),
|
402
|
+
validate_constraint_code: validate_commands.join("\n ")
|
403
|
+
end
|
378
404
|
end
|
379
405
|
end
|
380
406
|
|
@@ -388,9 +414,9 @@ module OnlineMigrations
|
|
388
414
|
if !allow_null && !new_or_small_table?(table_name)
|
389
415
|
# In PostgreSQL 12+ you can add a check constraint to the table
|
390
416
|
# and then "promote" it to NOT NULL for the column.
|
391
|
-
safe = check_constraints(table_name).any? do |c|
|
392
|
-
c
|
393
|
-
c
|
417
|
+
safe = connection.check_constraints(table_name).select(&:validated?).any? do |c|
|
418
|
+
c.expression == "#{column_name} IS NOT NULL" ||
|
419
|
+
c.expression == "#{connection.quote_column_name(column_name)} IS NOT NULL"
|
394
420
|
end
|
395
421
|
|
396
422
|
if !safe
|
@@ -601,7 +627,7 @@ module OnlineMigrations
|
|
601
627
|
def add_unique_constraint(table_name, column_name = nil, **options)
|
602
628
|
return if new_or_small_table?(table_name) || options[:using_index] || !column_name
|
603
629
|
|
604
|
-
index_name =
|
630
|
+
index_name = connection.index_name(table_name, column_name)
|
605
631
|
|
606
632
|
raise_error :add_unique_constraint,
|
607
633
|
add_index_code: command_str(:add_index, table_name, column_name, unique: true, name: index_name, algorithm: :concurrently),
|
@@ -765,18 +791,6 @@ module OnlineMigrations
|
|
765
791
|
"chk_rails_#{hashed_identifier}"
|
766
792
|
end
|
767
793
|
|
768
|
-
def check_constraints(table_name)
|
769
|
-
constraints_query = <<~SQL
|
770
|
-
SELECT pg_get_constraintdef(oid) AS def
|
771
|
-
FROM pg_constraint
|
772
|
-
WHERE contype = 'c'
|
773
|
-
AND convalidated
|
774
|
-
AND conrelid = #{connection.quote(table_name)}::regclass
|
775
|
-
SQL
|
776
|
-
|
777
|
-
connection.select_all(constraints_query).to_a
|
778
|
-
end
|
779
|
-
|
780
794
|
def check_inheritance_column(table_name, column_name, default)
|
781
795
|
if column_name.to_s == ActiveRecord::Base.inheritance_column && !default.nil?
|
782
796
|
raise_error :add_inheritance_column,
|
@@ -834,10 +848,8 @@ module OnlineMigrations
|
|
834
848
|
end
|
835
849
|
|
836
850
|
def index_include_column?(index, column)
|
837
|
-
|
838
|
-
|
839
|
-
index.columns.include?(column) ||
|
840
|
-
(Utils.ar_version >= 7.1 && index.include && index.include.include?(column)) ||
|
851
|
+
index.columns.include?(column) ||
|
852
|
+
(index.include && index.include.include?(column)) ||
|
841
853
|
(index.where && index.where.include?(column))
|
842
854
|
end
|
843
855
|
|
@@ -73,6 +73,13 @@ module OnlineMigrations
|
|
73
73
|
end
|
74
74
|
end
|
75
75
|
|
76
|
+
# Whether to require safety reason explanation when calling #safery_assured
|
77
|
+
#
|
78
|
+
# Disabled by default
|
79
|
+
# @return [Boolean]
|
80
|
+
#
|
81
|
+
attr_accessor :require_safety_assured_reason
|
82
|
+
|
76
83
|
# Whether to perform checks when migrating down
|
77
84
|
#
|
78
85
|
# Disabled by default
|
@@ -203,10 +210,10 @@ module OnlineMigrations
|
|
203
210
|
|
204
211
|
# Configuration object to configure background migrations
|
205
212
|
#
|
206
|
-
# @return [
|
207
|
-
# @see
|
213
|
+
# @return [BackgroundDataMigrations::Config]
|
214
|
+
# @see BackgroundDataMigrations::Config
|
208
215
|
#
|
209
|
-
attr_reader :
|
216
|
+
attr_reader :background_data_migrations
|
210
217
|
|
211
218
|
attr_reader :background_schema_migrations
|
212
219
|
|
@@ -223,13 +230,14 @@ module OnlineMigrations
|
|
223
230
|
lock_timeout: 0.2.seconds
|
224
231
|
)
|
225
232
|
|
226
|
-
@
|
233
|
+
@background_data_migrations = BackgroundDataMigrations::Config.new
|
227
234
|
@background_schema_migrations = BackgroundSchemaMigrations::Config.new
|
228
235
|
|
229
236
|
@checks = []
|
230
237
|
@start_after = 0
|
231
238
|
@target_version = nil
|
232
239
|
@small_tables = []
|
240
|
+
@require_safety_assured_reason = false
|
233
241
|
@check_down = false
|
234
242
|
@auto_analyze = false
|
235
243
|
@alphabetize_schema = false
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
gem "sidekiq", ">= 7.3.3"
|
4
|
+
require "sidekiq"
|
5
|
+
|
6
|
+
module OnlineMigrations
|
7
|
+
# Base class that is inherited by the host application's data migration classes.
|
8
|
+
class DataMigration
|
9
|
+
class NotFoundError < NameError; end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Finds a Data Migration with the given name.
|
13
|
+
#
|
14
|
+
# @param name [String] the name of the Data Migration to be found.
|
15
|
+
#
|
16
|
+
# @return [DataMigration] the Data Migration with the given name.
|
17
|
+
#
|
18
|
+
# @raise [NotFoundError] if a Data Migration with the given name does not exist.
|
19
|
+
#
|
20
|
+
def named(name)
|
21
|
+
namespace = OnlineMigrations.config.background_data_migrations.migrations_module.constantize
|
22
|
+
internal_namespace = ::OnlineMigrations::BackgroundDataMigrations
|
23
|
+
|
24
|
+
migration = "#{namespace}::#{name}".safe_constantize ||
|
25
|
+
"#{internal_namespace}::#{name}".safe_constantize
|
26
|
+
|
27
|
+
raise NotFoundError.new("Data Migration #{name} not found", name) if migration.nil?
|
28
|
+
if !(migration.is_a?(Class) && migration < self)
|
29
|
+
raise NotFoundError.new("#{name} is not a Data Migration", name)
|
30
|
+
end
|
31
|
+
|
32
|
+
migration
|
33
|
+
end
|
34
|
+
|
35
|
+
# @private
|
36
|
+
attr_accessor :active_record_enumerator_batch_size
|
37
|
+
|
38
|
+
# Limit the number of records that will be fetched in a single query when
|
39
|
+
# iterating over an Active Record collection migration.
|
40
|
+
#
|
41
|
+
# @param size [Integer] the number of records to fetch in a single query.
|
42
|
+
#
|
43
|
+
def collection_batch_size(size)
|
44
|
+
self.active_record_enumerator_batch_size = size
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# A hook to override that will be called when the migration starts running.
|
49
|
+
#
|
50
|
+
def after_start
|
51
|
+
end
|
52
|
+
|
53
|
+
# A hook to override that will be called around 'process' each time.
|
54
|
+
#
|
55
|
+
# Can be useful for some metrics collection, performance tracking etc.
|
56
|
+
#
|
57
|
+
def around_process
|
58
|
+
yield
|
59
|
+
end
|
60
|
+
|
61
|
+
# A hook to override that will be called when the migration resumes its work.
|
62
|
+
#
|
63
|
+
def after_resume
|
64
|
+
end
|
65
|
+
|
66
|
+
# A hook to override that will be called each time the migration is interrupted.
|
67
|
+
#
|
68
|
+
# This can be due to interruption or sidekiq stopping.
|
69
|
+
#
|
70
|
+
def after_stop
|
71
|
+
end
|
72
|
+
|
73
|
+
# A hook to override that will be called when the migration finished its work.
|
74
|
+
#
|
75
|
+
def after_complete
|
76
|
+
end
|
77
|
+
|
78
|
+
# A hook to override that will be called when the migration is paused.
|
79
|
+
#
|
80
|
+
def after_pause
|
81
|
+
end
|
82
|
+
|
83
|
+
# A hook to override that will be called when the migration is cancelled.
|
84
|
+
#
|
85
|
+
def after_cancel
|
86
|
+
end
|
87
|
+
|
88
|
+
# The collection to be processed.
|
89
|
+
#
|
90
|
+
# @return [ActiveRecord::Relation, ActiveRecord::Batches::BatchEnumerator, Array, Enumerator]
|
91
|
+
#
|
92
|
+
# @raise [NotImplementedError] with a message advising subclasses to override this method.
|
93
|
+
#
|
94
|
+
def collection
|
95
|
+
raise NotImplementedError, "#{self.class.name} must implement a 'collection' method"
|
96
|
+
end
|
97
|
+
|
98
|
+
# The action to be performed on each item from the collection.
|
99
|
+
#
|
100
|
+
# @param _item the current item from the collection being iterated
|
101
|
+
# @raise [NotImplementedError] with a message advising subclasses to override this method.
|
102
|
+
#
|
103
|
+
def process(_item)
|
104
|
+
raise NotImplementedError, "#{self.class.name} must implement a 'process' method"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Total count of iterations to be performed (optional, to be able to show progress).
|
108
|
+
#
|
109
|
+
# @return [Integer, nil]
|
110
|
+
#
|
111
|
+
def count
|
112
|
+
end
|
113
|
+
|
114
|
+
# Enumerator builder. You may override this method to return any Enumerator yielding
|
115
|
+
# pairs of `[item, item_cursor]`, instead of using `collection`.
|
116
|
+
#
|
117
|
+
# It is useful when it is not practical or impossible to define an explicit collection
|
118
|
+
# in the `collection` method.
|
119
|
+
#
|
120
|
+
# @param cursor [Object, nil] cursor position to resume from, or nil on initial call.
|
121
|
+
#
|
122
|
+
# @return [Enumerator]
|
123
|
+
#
|
124
|
+
def build_enumerator(cursor:)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -242,6 +242,22 @@ during writes works automatically). For most column type changes, this does not
|
|
242
242
|
8. Remove changes from step 3, if any
|
243
243
|
9. Deploy",
|
244
244
|
|
245
|
+
change_column_constraint: "Changing the type of a column that has check constraints blocks reads and writes
|
246
|
+
while every row is checked. Drop the check constraints on the column before
|
247
|
+
changing the type and add them back afterwards.
|
248
|
+
|
249
|
+
class <%= migration_name %> < <%= migration_parent %>
|
250
|
+
def change
|
251
|
+
<%= change_column_code %>
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
class Validate<%= migration_name %> < <%= migration_parent %>
|
256
|
+
def change
|
257
|
+
<%= validate_constraint_code %>
|
258
|
+
end
|
259
|
+
end",
|
260
|
+
|
245
261
|
change_column_default:
|
246
262
|
"Partial writes are enabled, which can cause incorrect values
|
247
263
|
to be inserted when changing the default value of a column.
|
@@ -20,7 +20,7 @@ module OnlineMigrations
|
|
20
20
|
# For ActiveRecord::ConnectionAdapters::IndexDefinition is for expression indexes,
|
21
21
|
# `columns` is a string
|
22
22
|
table == other.table &&
|
23
|
-
(
|
23
|
+
columns.intersect?(Array(other.columns))
|
24
24
|
end
|
25
25
|
|
26
26
|
# @param other [OnlineMigrations::IndexDefinition, ActiveRecord::ConnectionAdapters::IndexDefinition]
|
@@ -91,10 +91,13 @@ module OnlineMigrations
|
|
91
91
|
else
|
92
92
|
yield
|
93
93
|
end
|
94
|
-
rescue ActiveRecord::LockWaitTimeout
|
94
|
+
rescue ActiveRecord::LockWaitTimeout, ActiveRecord::Deadlocked => e
|
95
95
|
if current_attempt <= attempts
|
96
96
|
current_delay = delay(current_attempt)
|
97
|
-
|
97
|
+
|
98
|
+
problem = e.is_a?(ActiveRecord::Deadlocked) ? "Deadlock detected." : "Lock timeout."
|
99
|
+
Utils.say("#{problem} Retrying in #{current_delay} seconds...")
|
100
|
+
|
98
101
|
sleep(current_delay)
|
99
102
|
retry
|
100
103
|
end
|
@@ -45,7 +45,14 @@ module OnlineMigrations
|
|
45
45
|
# @example
|
46
46
|
# safety_assured { remove_column(:users, :some_column) }
|
47
47
|
#
|
48
|
-
def safety_assured(&block)
|
48
|
+
def safety_assured(reason = nil, &block)
|
49
|
+
config = OnlineMigrations.config
|
50
|
+
safe_version = version && version <= config.start_after
|
51
|
+
|
52
|
+
if config.require_safety_assured_reason && reason.blank? && !safe_version
|
53
|
+
raise OnlineMigrations::Error, "Specify a safety reason explanation when calling #safety_assured."
|
54
|
+
end
|
55
|
+
|
49
56
|
command_checker.class.safety_assured(&block)
|
50
57
|
end
|
51
58
|
|
@@ -3,84 +3,6 @@
|
|
3
3
|
module OnlineMigrations
|
4
4
|
# @private
|
5
5
|
module SchemaCache
|
6
|
-
def primary_keys(table_name)
|
7
|
-
if (renamed_table = renamed_table?(table_name))
|
8
|
-
super(renamed_table)
|
9
|
-
elsif renamed_column?(table_name)
|
10
|
-
super(column_rename_table(table_name))
|
11
|
-
else
|
12
|
-
super
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def columns(table_name)
|
17
|
-
if (renamed_table = renamed_table?(table_name))
|
18
|
-
super(renamed_table)
|
19
|
-
elsif renamed_column?(table_name)
|
20
|
-
columns = super(column_rename_table(table_name))
|
21
|
-
OnlineMigrations.config.column_renames[table_name].each do |old_column_name, new_column_name|
|
22
|
-
duplicate_column(old_column_name, new_column_name, columns)
|
23
|
-
end
|
24
|
-
columns
|
25
|
-
else
|
26
|
-
super.reject { |column| column.name.end_with?("_for_type_change") }
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def indexes(table_name)
|
31
|
-
if (renamed_table = renamed_table?(table_name))
|
32
|
-
super(renamed_table)
|
33
|
-
elsif renamed_column?(table_name)
|
34
|
-
super(column_rename_table(table_name))
|
35
|
-
else
|
36
|
-
super
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def clear_data_source_cache!(name)
|
41
|
-
if (renamed_table = renamed_table?(name))
|
42
|
-
super(renamed_table)
|
43
|
-
end
|
44
|
-
|
45
|
-
if renamed_column?(name)
|
46
|
-
super(column_rename_table(name))
|
47
|
-
end
|
48
|
-
|
49
|
-
super
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
def renamed_table?(table_name)
|
54
|
-
table_renames = OnlineMigrations.config.table_renames
|
55
|
-
if table_renames.key?(table_name)
|
56
|
-
views = connection.views
|
57
|
-
table_renames[table_name] if views.include?(table_name)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def renamed_column?(table_name)
|
62
|
-
column_renames = OnlineMigrations.config.column_renames
|
63
|
-
column_renames.key?(table_name) && connection.views.include?(table_name)
|
64
|
-
end
|
65
|
-
|
66
|
-
def column_rename_table(table_name)
|
67
|
-
"#{table_name}_column_rename"
|
68
|
-
end
|
69
|
-
|
70
|
-
def duplicate_column(old_column_name, new_column_name, columns)
|
71
|
-
old_column = columns.find { |column| column.name == old_column_name }
|
72
|
-
new_column = old_column.dup
|
73
|
-
# Active Record defines only reader for :name
|
74
|
-
new_column.instance_variable_set(:@name, new_column_name)
|
75
|
-
# Correspond to the Active Record freezing of each column
|
76
|
-
columns << new_column.freeze
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
# @private
|
81
|
-
module SchemaCache71
|
82
|
-
# Active Record >= 7.1 changed signature of the methods,
|
83
|
-
# see https://github.com/rails/rails/pull/48716.
|
84
6
|
def primary_keys(connection, table_name)
|
85
7
|
if (renamed_table = renamed_table?(connection, table_name))
|
86
8
|
super(connection, renamed_table)
|