online_migrations 0.31.2 → 0.32.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: 4afb5d9ab9916861d263de3c38a2436d2ee131ae9988edf282866145a274356d
4
- data.tar.gz: 761d30f417479db296429af0668bf62e47581aa4a73fce1d9ba72bd62c66173f
3
+ metadata.gz: 52861ed084d1bd3fd77bc08ea3f1b6aae8c26b58f1fc5ef08f181c18feb8d390
4
+ data.tar.gz: 92b111ad1679c421cbe97e206b18b2e3126cfb2c2fe4247de70e793a49ac112a
5
5
  SHA512:
6
- metadata.gz: 9e6930e8069732e7988b731d826a9e82b023c8f4bbebd5827a31efa10747ada86af64e7e75553df5a8d2e318688002e9c194cdf07e9124ef4231986197d885e7
7
- data.tar.gz: fe46acbfd31436293ead08b37258c877ca4eddf3bb6c41e462a74c89153d4f632e244c2fa5b98b0887fbfe024a3ab8a9ada8ad101fffb9bebfd10dbbd9f5c9bc
6
+ metadata.gz: 0eeeae19aec88ff880a076e3f82c60a99fb210b62ac3aeb881fb601ea14d34de8139ad9b42f839ffc29a83ffa65f6e787f36d9940ec06d2602982e562cda6e13
7
+ data.tar.gz: 82d54ab9eff606c039e96f8f158c4991ffd861456237fcfcef9578e63cc212e8db3a9abc1be0ec92d40e51e2ac37c8748e2396db560a7737d680bb01357b4519
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.32.0 (2026-01-07)
4
+
5
+ - Add ability to create delayed background migrations
6
+
7
+ ```ruby
8
+ add_index_in_background(:users, :name, delay: true)
9
+ enqueue_background_data_migration("MyMigration", delay: true)
10
+ ```
11
+
12
+ They will start only after approval from the user.
13
+
14
+ - Fix enqueueing background data migrations with the same name on different tables
15
+
16
+ Note: Run `bin/rails generate online_migrations:upgrade` if using background schema migrations.
17
+
3
18
  ## 0.31.2 (2025-11-13)
4
19
 
5
20
  - Fix running background data migrations inline
@@ -324,6 +324,14 @@ Data Migrations can be in various states during its execution:
324
324
  migration.cancel
325
325
  ```
326
326
 
327
+ * **delayed**: A migration was created, but waiting approval from the user to start running.
328
+
329
+ To create a delayed migration, you can pass a `delayed: true` option:
330
+
331
+ ```ruby
332
+ enqueue_background_data_migration("MyMigration", delay: true)
333
+ ```
334
+
327
335
  To get the progress (assuming `#count` method on data migration class was defined):
328
336
 
329
337
  ```ruby
@@ -139,6 +139,14 @@ Background Schema Migrations can be in various states during its execution:
139
139
  * **succeeded**: A migration finished without error.
140
140
  * **cancelled**: A migration was cancelled by the user.
141
141
 
142
+ * **delayed**: A migration was created, but waiting approval from the user to start running.
143
+
144
+ To create a delayed migration, you can pass a `delayed: true` option:
145
+
146
+ ```ruby
147
+ add_index_in_background(:users, :name, delay: true)
148
+ ```
149
+
142
150
  ## Configuring
143
151
 
144
152
  There are a few configurable options for the Background Schema Migrations. Custom configurations should be placed in a `online_migrations.rb` initializer.
@@ -0,0 +1,7 @@
1
+ class BackgroundDataMigrationsRemoveIterationPauseDefault < <%= migration_parent %>
2
+ def change
3
+ safety_assured("Table is small") do
4
+ change_column_default :background_data_migrations, :iteration_pause, nil
5
+ end
6
+ end
7
+ end
@@ -2,7 +2,7 @@ class BackgroundSchemaMigrationsChangeUniqueIndex < <%= migration_parent %>
2
2
  def change
3
3
  safety_assured("Table is small") do
4
4
  remove_index :background_schema_migrations, name: :index_background_schema_migrations_on_unique_configuration
5
- add_index :background_schema_migrations, [:migration_name, :shard, :connection_class_name], unique: true,
5
+ add_index :background_schema_migrations, [:migration_name, :table_name, :shard, :connection_class_name], unique: true,
6
6
  name: :index_background_schema_migrations_on_unique_configuration
7
7
  end
8
8
  end
@@ -13,7 +13,7 @@ class InstallOnlineMigrations < <%= migration_parent %>
13
13
  t.bigint :tick_count, default: 0, null: false
14
14
  t.float :time_running, default: 0.0, null: false
15
15
  t.integer :max_attempts, null: false
16
- t.float :iteration_pause, default: 0.0, null: false
16
+ t.float :iteration_pause, null: false
17
17
  t.string :error_class
18
18
  t.string :error_message
19
19
  t.string :backtrace, array: true
@@ -41,7 +41,7 @@ class InstallOnlineMigrations < <%= migration_parent %>
41
41
  t.string :connection_class_name
42
42
  t.timestamps
43
43
 
44
- t.index [:migration_name, :shard, :connection_class_name], unique: true,
44
+ t.index [:migration_name, :table_name, :shard, :connection_class_name], unique: true,
45
45
  name: :index_background_schema_migrations_on_unique_configuration
46
46
  end
47
47
  end
@@ -30,7 +30,7 @@ module OnlineMigrations
30
30
  end
31
31
 
32
32
  indexes = connection.indexes(:background_schema_migrations)
33
- unique_index = indexes.find { |i| i.unique && i.columns.sort == ["connection_class_name", "migration_name", "shard"] }
33
+ unique_index = indexes.find { |i| i.unique && i.columns.sort == ["connection_class_name", "migration_name", "shard", "table_name"] }
34
34
  if !unique_index
35
35
  migrations << "background_schema_migrations_change_unique_index"
36
36
  end
@@ -47,6 +47,11 @@ module OnlineMigrations
47
47
  migrations << "background_data_migrations_add_iteration_pause"
48
48
  end
49
49
 
50
+ iteration_pause_column = connection.columns(:background_data_migrations).find { |c| c.name == "iteration_pause" }
51
+ if iteration_pause_column && iteration_pause_column.default
52
+ migrations << "background_data_migrations_remove_iteration_pause_default"
53
+ end
54
+
50
55
  migrations
51
56
  end
52
57
 
@@ -19,6 +19,7 @@ module OnlineMigrations
19
19
  "succeeded", # The migration finished without error.
20
20
  "cancelling", # The migration has been told to cancel but is finishing work.
21
21
  "cancelled", # The migration was cancelled by the user.
22
+ "delayed", # The migration was created, but waiting approval from the user to start running.
22
23
  ]
23
24
 
24
25
  COMPLETED_STATUSES = ["succeeded", "failed", "cancelled"]
@@ -130,6 +131,19 @@ module OnlineMigrations
130
131
  end
131
132
  end
132
133
 
134
+ # Enqueue this data migration. No-op if migration is not delayed.
135
+ #
136
+ # @return [Boolean] whether this data migration was enqueued.
137
+ #
138
+ def enqueue
139
+ if delayed?
140
+ enqueued!
141
+ true
142
+ else
143
+ false
144
+ end
145
+ end
146
+
133
147
  # Cancel this data migration. No-op if migration is completed.
134
148
  #
135
149
  # @return [Boolean] whether this data migration was cancelled.
@@ -137,7 +151,7 @@ module OnlineMigrations
137
151
  def cancel
138
152
  return false if completed?
139
153
 
140
- if paused? || stuck?
154
+ if paused? || delayed? || stuck?
141
155
  update!(status: :cancelled, finished_at: Time.current)
142
156
  elsif enqueued?
143
157
  cancelled!
@@ -155,7 +169,7 @@ module OnlineMigrations
155
169
  def pause
156
170
  return false if completed?
157
171
 
158
- if enqueued? || stuck?
172
+ if enqueued? || delayed? || stuck?
159
173
  paused!
160
174
  else
161
175
  pausing!
@@ -338,6 +338,7 @@ module OnlineMigrations
338
338
  #
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
+ # @param delay [Boolean] Whether this migration should be delayed and approved by the user to start running.
341
342
  # @option options [Integer] :max_attempts (5) Maximum number of batch run attempts
342
343
  # @option options [String, nil] :connection_class_name Class name to use to get connections
343
344
  #
@@ -368,7 +369,7 @@ module OnlineMigrations
368
369
  # @note For convenience, the enqueued background data migration is run inline
369
370
  # in development and test environments
370
371
  #
371
- def enqueue_background_data_migration(migration_name, *arguments, **options)
372
+ def enqueue_background_data_migration(migration_name, *arguments, delay: false, **options)
372
373
  options.assert_valid_keys(:max_attempts, :iteration_pause, :connection_class_name)
373
374
 
374
375
  migration_name = migration_name.name if migration_name.is_a?(Class)
@@ -381,13 +382,15 @@ module OnlineMigrations
381
382
  connection_class = options[:connection_class_name].constantize
382
383
  shards = Utils.shard_names(connection_class)
383
384
  shards = [nil] if shards.size == 1
385
+ status = delay ? :delayed : :enqueued
384
386
 
385
387
  shards.each do |shard|
386
388
  # Can't use `find_or_create_by` or hash syntax here, because it does not correctly work with json `arguments`.
387
389
  migration = Migration.where(migration_name: migration_name, shard: shard).where("arguments = ?", arguments.to_json).first
388
- migration ||= Migration.create!(**options, migration_name: migration_name, arguments: arguments, shard: shard)
390
+ migration ||= Migration.create!(**options, status: status, migration_name: migration_name, arguments: arguments, shard: shard)
389
391
 
390
392
  if Utils.run_background_migrations_inline? && !migration.succeeded?
393
+ migration.update_column(:status, :enqueued) if !migration.enqueued?
391
394
  job = OnlineMigrations.config.background_data_migrations.job
392
395
  job.constantize.perform_inline(migration.id)
393
396
  end
@@ -140,7 +140,7 @@ module OnlineMigrations
140
140
  @migration.data_migration.process(item)
141
141
 
142
142
  # Migration is refreshed regularly by ticker.
143
- pause = @migration.iteration_pause
143
+ pause = @migration.iteration_pause.to_f
144
144
  sleep(pause) if pause > 0
145
145
  end
146
146
  @ticker.tick
@@ -45,6 +45,9 @@ module OnlineMigrations
45
45
  # cancelling -> failed occurs when the job raises an exception after the
46
46
  # user has cancelled it.
47
47
  "cancelling" => ["cancelled", "succeeded", "failed"],
48
+ # delayed -> enqueued occurs when the delayed migration was approved by the user to start running.
49
+ # delayed -> cancelled occurs when the delayed migration was cancelled.
50
+ "delayed" => ["enqueued", "cancelled"],
48
51
  }
49
52
 
50
53
  def validate(record)
@@ -17,6 +17,7 @@ module OnlineMigrations
17
17
  "failed", # The migration raises an error when running and retry attempts exceeded.
18
18
  "succeeded", # The migration finished without error.
19
19
  "cancelled", # The migration was cancelled by the user.
20
+ "delayed", # The migration was created, but waiting approval from the user to start running.
20
21
  ]
21
22
 
22
23
  MAX_IDENTIFIER_LENGTH = 63
@@ -33,7 +34,7 @@ module OnlineMigrations
33
34
  validates :table_name, presence: true, length: { maximum: MAX_IDENTIFIER_LENGTH }
34
35
  validates :definition, presence: true
35
36
  validates :migration_name, presence: true, uniqueness: {
36
- scope: [:connection_class_name, :shard],
37
+ scope: [:table_name, :connection_class_name, :shard],
37
38
  message: ->(object, data) do
38
39
  message = "(#{data[:value]}) has already been taken."
39
40
  if object.index_addition?
@@ -66,8 +67,6 @@ module OnlineMigrations
66
67
  enqueued? || running?
67
68
  end
68
69
 
69
- alias cancel cancelled!
70
-
71
70
  # Returns whether this migration is pausable.
72
71
  #
73
72
  def pausable?
@@ -114,6 +113,32 @@ module OnlineMigrations
114
113
  end
115
114
  end
116
115
 
116
+ # Enqueue this data migration. No-op if migration is not delayed.
117
+ #
118
+ # @return [Boolean] whether this data migration was enqueued.
119
+ #
120
+ def enqueue
121
+ if delayed?
122
+ enqueued!
123
+ true
124
+ else
125
+ false
126
+ end
127
+ end
128
+
129
+ # Cancel this schema migration. No-op if migration is completed.
130
+ #
131
+ # @return [Boolean] whether this schema migration was cancelled.
132
+ #
133
+ def cancel
134
+ if completed?
135
+ false
136
+ elsif enqueued? || errored? || delayed? || stuck?
137
+ cancelled!
138
+ true
139
+ end
140
+ end
141
+
117
142
  def index_addition?
118
143
  definition.match?(/create (unique )?index/i)
119
144
  end
@@ -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)
7
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
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)
37
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
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)
49
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
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, **options)
90
+ def enqueue_background_schema_migration(migration_name, table_name, connection_class_name: nil, delay: false, **options)
91
91
  options.assert_valid_keys(:definition, :max_attempts, :statement_timeout)
92
92
 
93
93
  if Utils.multiple_databases? && !connection_class_name
@@ -107,9 +107,11 @@ module OnlineMigrations
107
107
  shards = Utils.shard_names(connection_class)
108
108
  shards = [nil] if shards.size == 1
109
109
 
110
+ status = delay ? :delayed : :enqueued
111
+
110
112
  shards.each do |shard|
111
- migration = Migration.create_with(**options, table_name: table_name)
112
- .find_or_create_by!(migration_name: migration_name, shard: shard, connection_class_name: connection_class_name)
113
+ migration = Migration.create_with(**options, status: status)
114
+ .find_or_create_by!(migration_name: migration_name, table_name: table_name, shard: shard, connection_class_name: connection_class_name)
113
115
 
114
116
  if Utils.run_background_migrations_inline?
115
117
  # Run migration again in development.
@@ -17,6 +17,9 @@ module OnlineMigrations
17
17
  # failed -> enqueued occurs when the failed migration is enqueued to be retried.
18
18
  # failed -> running occurs when the failed migration is retried.
19
19
  "failed" => ["enqueued", "running", "cancelled"],
20
+ # delayed -> enqueued occurs when the delayed migration was approved by the user to start running.
21
+ # delayed -> cancelled occurs when the delayed migration was cancelled.
22
+ "delayed" => ["enqueued", "cancelled"],
20
23
  }
21
24
 
22
25
  def validate(record)
@@ -426,7 +426,7 @@ module OnlineMigrations
426
426
  validate_constraint_code: command_str(:validate_not_null_constraint, table_name, column_name, name: constraint_name),
427
427
  table_name: table_name,
428
428
  column_name: column_name,
429
- default: default,
429
+ default_value: default,
430
430
  remove_constraint_code: command_str(:remove_check_constraint, table_name, name: constraint_name),
431
431
  change_column_null_code: command_str(:change_column_null, table_name, column_name, false),
432
432
  }
@@ -277,11 +277,11 @@ class <%= migration_name %> < <%= migration_parent %>
277
277
 
278
278
  def change
279
279
  <%= add_constraint_code %>
280
- <% unless default.nil? %>
280
+ <% unless default_value.nil? %>
281
281
 
282
282
  # Passing a default value to change_column_null runs a single UPDATE query,
283
283
  # which can cause downtime. Instead, backfill the existing rows in batches.
284
- update_column_in_batches(:<%= table_name %>, :<%= column_name %>, <%= default.inspect %>) do |relation|
284
+ update_column_in_batches(:<%= table_name %>, :<%= column_name %>, <%= default_value.inspect %>) do |relation|
285
285
  relation.where(<%= column_name %>: nil)
286
286
  end
287
287
 
@@ -852,6 +852,33 @@ module OnlineMigrations
852
852
  end
853
853
 
854
854
  # @private
855
+ # From rails 8.2 this will be used by fixtures code.
856
+ # https://github.com/rails/rails/commit/3415223ed2765c61ae348622dc8d2681efd910d7
857
+ def reset_column_sequences!(tables)
858
+ views = self.views
859
+
860
+ table_renames = OnlineMigrations.config.table_renames
861
+ renamed_tables = table_renames.slice(*views)
862
+
863
+ column_renames = OnlineMigrations.config.column_renames
864
+ renamed_columns = column_renames.slice(*views)
865
+
866
+ tables = tables.map do |table|
867
+ if renamed_tables.key?(table)
868
+ renamed_tables[table]
869
+ elsif renamed_columns.key?(table)
870
+ __tmp_table_name_for_column_rename(table)
871
+ else
872
+ table
873
+ end
874
+ end
875
+
876
+ super
877
+ end
878
+
879
+ # @private
880
+ # Was used by fixtures code in rails < 8.2.
881
+ # Delete when rails < 8.2 is no longer supported.
855
882
  def pk_and_sequence_for(table)
856
883
  views = self.views
857
884
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.31.2"
4
+ VERSION = "0.32.0"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: online_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.2
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-11-12 00:00:00.000000000 Z
10
+ date: 2026-01-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activerecord
@@ -41,6 +41,7 @@ files:
41
41
  - lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt
42
42
  - lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt
43
43
  - lib/generators/online_migrations/templates/background_data_migrations_add_iteration_pause.rb.tt
44
+ - lib/generators/online_migrations/templates/background_data_migrations_remove_iteration_pause_default.rb.tt
44
45
  - lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt
45
46
  - lib/generators/online_migrations/templates/change_background_data_migrations.rb.tt
46
47
  - lib/generators/online_migrations/templates/create_background_schema_migrations.rb.tt