online_migrations 0.33.2 → 0.34.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54cc719e4b9c6d22226c4393fa81362cec973f1f368553c2797bc5a7e0b3720b
4
- data.tar.gz: f8dba7a97c0e4bbb744d552f783ad9407aa65aec16d1020933becaf83a9779a4
3
+ metadata.gz: b7f1dd28a4540742f77c2a33d7336f4f6f2b12a4f267a1cc556975f7b7122ba4
4
+ data.tar.gz: db448dca841dc39418cd92e5d86108ee0b59b69b72eeda391b0a81a30aa7ce71
5
5
  SHA512:
6
- metadata.gz: bd3a4195693d1f48a585c96ec66a0ae5775f585a2ce66c2e226a1148847cc63292e64a9704268a7a40e84a094c50e6a1990b91009fc22527db4ba004ef8aae95
7
- data.tar.gz: 8f8cf24be5a12a4e68dadaeae47373a05ed2426bcc61f7bbe325e3e714c884ee353c48afd7696b7da3bd0393a2c88e11993afa081b99a63bedf839fb88e8eec4
6
+ metadata.gz: b423580e44eefc2f1ff71b2ebdcd15e4ff449f869231aaebe38c4598dc364d817eb3b484a82b7a0cc493727f94d956610ced75ac409092227c2e5cc727935eee
7
+ data.tar.gz: 3287e23ec07a09a1026790ab4747427eafbc70f675fd2cbf7382f302a86f748c3fde45f5a7546b3e66848cd1e12eb86f073ff3e5461471bb35ba79f5b0a160ec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.34.0 (2026-05-29)
4
+
5
+ - Drop support for Rails < 7.2
6
+
7
+ - Do not schedule background data migrations with non existent data migration classes
8
+
9
+ This prevents a situation when the background data migration was enqueued and started running
10
+ before the deploy completes and the data migration class is available.
11
+
12
+ - Add ability to enqueue background migrations for specific shards (if using sharding)
13
+
14
+ ```ruby
15
+ add_index_in_background(:users, :name, connection_class_name: "ApplicationRecord", shard: :shard_one)
16
+ enqueue_background_data_migration("MyMigration", shard: :shard_one)
17
+ ```
18
+
19
+ - Fix validating foreign key in `add_reference_concurrently` when multiple foreign keys target the same table
20
+
3
21
  ## 0.33.2 (2026-03-16)
4
22
 
5
23
  - Fix message for adding to `ignored_columns` when renaming a column
@@ -22,7 +40,7 @@
22
40
  - Add ability to create delayed background migrations
23
41
 
24
42
  ```ruby
25
- add_index_in_background(:users, :name, delay: true)
43
+ add_index_in_background(:users, :name, connection_class_name: "ApplicationRecord", delay: true)
26
44
  enqueue_background_data_migration("MyMigration", delay: true)
27
45
  ```
28
46
 
data/README.md CHANGED
@@ -17,7 +17,7 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)
17
17
  ## Requirements
18
18
 
19
19
  - Ruby 3.1+
20
- - Rails 7.1+
20
+ - Rails 7.2+
21
21
  - PostgreSQL 12+
22
22
 
23
23
  For older Ruby and Rails versions you can use older versions of this gem.
@@ -339,6 +339,7 @@ module OnlineMigrations
339
339
  # @param migration_name [String, Class] Background migration class name
340
340
  # @param arguments [Array] Extra arguments to pass to the migration instance when the migration runs
341
341
  # @param delay [Boolean] Whether this migration should be delayed and approved by the user to start running.
342
+ # @param shard [String, Symbol] Specific shard this migration will be enqueued for. Defaults to all shards.
342
343
  # @option options [Integer] :max_attempts (5) Maximum number of batch run attempts
343
344
  # @option options [String, nil] :connection_class_name Class name to use to get connections
344
345
  #
@@ -369,7 +370,7 @@ module OnlineMigrations
369
370
  # @note For convenience, the enqueued background data migration is run inline
370
371
  # in development and test environments
371
372
  #
372
- def enqueue_background_data_migration(migration_name, *arguments, delay: false, **options)
373
+ def enqueue_background_data_migration(migration_name, *arguments, delay: false, shard: nil, **options)
373
374
  options.assert_valid_keys(:max_attempts, :iteration_pause, :connection_class_name)
374
375
 
375
376
  migration_name = migration_name.name if migration_name.is_a?(Class)
@@ -380,8 +381,16 @@ module OnlineMigrations
380
381
  end
381
382
 
382
383
  connection_class = options[:connection_class_name].constantize
383
- shards = Utils.shard_names(connection_class)
384
- shards = [nil] if shards.size == 1
384
+ shards = Utils.shard_names(connection_class).map(&:to_s)
385
+ if shards.size == 1
386
+ shards = [nil]
387
+ elsif shard
388
+ shard = shard.to_s
389
+ raise "Unknown shard: #{shard}" if !shards.include?(shard)
390
+
391
+ shards = [shard]
392
+ end
393
+
385
394
  status = delay ? :delayed : :pending
386
395
 
387
396
  shards.each do |shard|
@@ -37,7 +37,7 @@ module OnlineMigrations
37
37
 
38
38
  with_lock do
39
39
  stuck_migrations, active_migrations = relation.running.partition(&:stuck?)
40
- runnable_migrations = relation.pending + stuck_migrations
40
+ runnable_migrations = migrations_with_existing_classes(relation.pending) + stuck_migrations
41
41
 
42
42
  # Ensure no more than 'concurrency' migrations are running at the same time.
43
43
  remaining_to_enqueue = concurrency - active_migrations.count
@@ -71,6 +71,17 @@ module OnlineMigrations
71
71
  end
72
72
  end
73
73
 
74
+ def migrations_with_existing_classes(migrations)
75
+ migrations.select do |migration|
76
+ # Detect if the data migration class exists.
77
+ # It may not yet exist if the data migration was enqueued before the deploy finished.
78
+ migration.data_migration
79
+ true
80
+ rescue DataMigration::NotFoundError
81
+ false
82
+ end
83
+ end
84
+
74
85
  def enqueue_migration(migration)
75
86
  job = OnlineMigrations.config.background_data_migrations.job
76
87
  job_class = job.constantize
@@ -208,12 +208,8 @@ module OnlineMigrations
208
208
 
209
209
  # Extension point, do not remove this method.
210
210
  def with_connection(&block)
211
- if Utils.ar_version >= 7.2
212
- # https://github.com/rails/rails/pull/51083
213
- connection_class.with_connection(&block)
214
- else
215
- yield connection_class.connection
216
- end
211
+ # https://github.com/rails/rails/pull/51083
212
+ connection_class.with_connection(&block)
217
213
  end
218
214
 
219
215
  def with_statement_timeout(connection, timeout)
@@ -4,7 +4,7 @@ module OnlineMigrations
4
4
  module BackgroundSchemaMigrations
5
5
  module MigrationHelpers
6
6
  def add_index_in_background(table_name, column_name, **options)
7
- migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
7
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay, :shard)
8
8
 
9
9
  options[:algorithm] = :concurrently
10
10
  index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
@@ -34,7 +34,7 @@ module OnlineMigrations
34
34
  def remove_index_in_background(table_name, column_name = nil, name:, **options)
35
35
  raise ArgumentError, "Index name must be specified" if name.blank?
36
36
 
37
- migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
37
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay, :shard)
38
38
 
39
39
  if !index_exists?(table_name, column_name, **options, name: name)
40
40
  Utils.raise_or_say("Index deletion was not enqueued because the index does not exist.")
@@ -46,7 +46,7 @@ module OnlineMigrations
46
46
  end
47
47
 
48
48
  def validate_foreign_key_in_background(from_table, to_table = nil, **options)
49
- migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
49
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay, :shard)
50
50
 
51
51
  if !foreign_key_exists?(from_table, to_table, **options)
52
52
  Utils.raise_or_say("Foreign key validation was not enqueued because the foreign key does not exist.")
@@ -87,7 +87,7 @@ module OnlineMigrations
87
87
  end
88
88
  end
89
89
 
90
- def enqueue_background_schema_migration(migration_name, table_name, connection_class_name: nil, delay: false, **options)
90
+ def enqueue_background_schema_migration(migration_name, table_name, connection_class_name: nil, delay: false, shard: nil, **options)
91
91
  options.assert_valid_keys(:definition, :max_attempts, :statement_timeout)
92
92
 
93
93
  if Utils.multiple_databases? && !connection_class_name
@@ -104,8 +104,15 @@ module OnlineMigrations
104
104
  # Normalize to the real connection class name.
105
105
  connection_class_name = connection_class.name
106
106
 
107
- shards = Utils.shard_names(connection_class)
108
- shards = [nil] if shards.size == 1
107
+ shards = Utils.shard_names(connection_class).map(&:to_s)
108
+ if shards.size == 1
109
+ shards = [nil]
110
+ elsif shard
111
+ shard = shard.to_s
112
+ raise "Unknown shard: #{shard}" if !shards.include?(shard)
113
+
114
+ shards = [shard]
115
+ end
109
116
 
110
117
  status = delay ? :delayed : :pending
111
118
 
@@ -16,6 +16,7 @@ module OnlineMigrations
16
16
  # @return [DataMigration] the Data Migration with the given name.
17
17
  #
18
18
  # @raise [NotFoundError] if a Data Migration with the given name does not exist.
19
+ # @raise [ArgumentError] if a Data Migration with the given name is not a subclass of DataMigration.
19
20
  #
20
21
  def named(name)
21
22
  namespace = OnlineMigrations.config.background_data_migrations.migrations_module.constantize
@@ -26,7 +27,7 @@ module OnlineMigrations
26
27
 
27
28
  raise NotFoundError.new("Data Migration #{name} not found", name) if migration.nil?
28
29
  if !(migration.is_a?(Class) && migration < self)
29
- raise NotFoundError.new("#{name} is not a Data Migration", name)
30
+ raise ArgumentError, "#{name} is not a Data Migration"
30
31
  end
31
32
 
32
33
  migration
@@ -3,83 +3,6 @@
3
3
  module OnlineMigrations
4
4
  # @private
5
5
  module SchemaCache
6
- def primary_keys(connection, table_name)
7
- if (renamed_table = renamed_table?(connection, table_name))
8
- super(connection, renamed_table)
9
- elsif renamed_column?(connection, table_name)
10
- super(connection, column_rename_table(table_name))
11
- else
12
- super
13
- end
14
- end
15
-
16
- def columns(connection, table_name)
17
- if (renamed_table = renamed_table?(connection, table_name))
18
- super(connection, renamed_table)
19
- elsif renamed_column?(connection, table_name)
20
- columns = super(connection, 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(connection, table_name)
31
- if (renamed_table = renamed_table?(connection, table_name))
32
- super(connection, renamed_table)
33
- elsif renamed_column?(connection, table_name)
34
- super(connection, column_rename_table(table_name))
35
- else
36
- super
37
- end
38
- end
39
-
40
- def clear_data_source_cache!(connection, name)
41
- if (renamed_table = renamed_table?(connection, name))
42
- super(connection, renamed_table)
43
- end
44
-
45
- if renamed_column?(connection, name)
46
- super(connection, column_rename_table(name))
47
- end
48
-
49
- super
50
- end
51
-
52
- private
53
- def renamed_table?(connection, table_name)
54
- table_renames = OnlineMigrations.config.table_renames
55
- if table_renames.key?(table_name) && connection.view_exists?(table_name)
56
- table_renames[table_name]
57
- end
58
- end
59
-
60
- def renamed_column?(connection, table_name)
61
- column_renames = OnlineMigrations.config.column_renames
62
- column_renames.key?(table_name) && connection.view_exists?(table_name)
63
- end
64
-
65
- def column_rename_table(table_name)
66
- "#{table_name}_column_rename"
67
- end
68
-
69
- def duplicate_column(old_column_name, new_column_name, columns)
70
- old_column = columns.find { |column| column.name == old_column_name }
71
- new_column = old_column.dup
72
- # Active Record defines only reader for :name
73
- new_column.instance_variable_set(:@name, new_column_name)
74
- # Correspond to the Active Record freezing of each column
75
- columns << new_column.freeze
76
- end
77
- end
78
-
79
- # @private
80
- module SchemaCache72
81
- # Active Record >= 7.2 changed signature of the methods,
82
- # see https://github.com/rails/rails/pull/48716.
83
6
  def primary_keys(pool, table_name)
84
7
  if (renamed_table = renamed_table?(pool, table_name))
85
8
  super(pool, renamed_table)
@@ -705,7 +705,7 @@ module OnlineMigrations
705
705
  add_foreign_key(table_name, foreign_table_name, **foreign_key, column: column_name, validate: false)
706
706
 
707
707
  if foreign_key[:validate] != false
708
- validate_foreign_key(table_name, foreign_table_name, **foreign_key)
708
+ validate_foreign_key(table_name, foreign_table_name, **foreign_key, column: column_name)
709
709
  end
710
710
  end
711
711
  end
@@ -992,12 +992,7 @@ module OnlineMigrations
992
992
 
993
993
  def __tmp_table_name_for_column_rename(table_name)
994
994
  suffix = "_column_rename"
995
-
996
- # On ActiveRecord 7.1 can use table_name_length instead of max_identifier_length,
997
- # see https://github.com/rails/rails/pull/45136.
998
- # Also we need to account for "_pkey", because older versions does not correctly rename
999
- # tables with long names. Remove when supporting newer versions only.
1000
- prefix_length = max_identifier_length - "_pkey".size - suffix.length
995
+ prefix_length = max_identifier_length - suffix.length
1001
996
  table_name[0, prefix_length] + suffix
1002
997
  end
1003
998
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.33.2"
4
+ VERSION = "0.34.0"
5
5
  end
@@ -116,16 +116,11 @@ module OnlineMigrations
116
116
  ActiveRecord::Migration.prepend(OnlineMigrations::Migration)
117
117
  ActiveRecord::Migrator.prepend(OnlineMigrations::Migrator)
118
118
  ActiveRecord::SchemaDumper.prepend(OnlineMigrations::SchemaDumper)
119
+ ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache)
119
120
 
120
121
  ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(OnlineMigrations::DatabaseTasks)
121
122
  ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder)
122
123
 
123
- if OnlineMigrations::Utils.ar_version >= 7.2
124
- ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache72)
125
- else
126
- ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache)
127
- end
128
-
129
124
  if !ActiveRecord::Batches::BatchEnumerator.method_defined?(:use_ranges)
130
125
  ActiveRecord::Batches::BatchEnumerator.include(OnlineMigrations::ActiveRecordBatchEnumerator)
131
126
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: online_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.2
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.1'
18
+ version: '7.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '7.1'
25
+ version: '7.2'
26
26
  email:
27
27
  - fatkodima123@gmail.com
28
28
  executables: []
@@ -115,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
115
  - !ruby/object:Gem::Version
116
116
  version: '0'
117
117
  requirements: []
118
- rubygems_version: 4.0.3
118
+ rubygems_version: 4.0.10
119
119
  specification_version: 4
120
120
  summary: Catch unsafe PostgreSQL migrations in development and run them easier in
121
121
  production