online_migrations 0.34.0 → 0.36.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 +15 -0
- data/README.md +3 -1
- data/lib/generators/online_migrations/templates/background_data_migrations_convert_cursor.rb.tt +9 -0
- data/lib/generators/online_migrations/upgrade_generator.rb +3 -1
- data/lib/online_migrations/background_data_migrations/migration.rb +29 -5
- data/lib/online_migrations/background_data_migrations/migration_job.rb +1 -0
- data/lib/online_migrations/background_data_migrations/migration_status_validator.rb +0 -2
- data/lib/online_migrations/background_data_migrations/scheduler.rb +2 -3
- data/lib/online_migrations/background_schema_migrations/migration.rb +3 -7
- data/lib/online_migrations/background_schema_migrations/migration_runner.rb +2 -3
- data/lib/online_migrations/background_schema_migrations/scheduler.rb +2 -2
- data/lib/online_migrations/change_column_type_helpers.rb +10 -5
- data/lib/online_migrations/command_checker.rb +17 -10
- data/lib/online_migrations/command_recorder.rb +1 -1
- data/lib/online_migrations/index_definition.rb +5 -4
- data/lib/online_migrations/migration.rb +1 -1
- data/lib/online_migrations/schema_statements.rb +2 -2
- data/lib/online_migrations/utils.rb +1 -1
- data/lib/online_migrations/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6740adaa6d20273e8d148e8e0788a6e27c6c6701187fe60e006ff3ba8e2129d2
|
|
4
|
+
data.tar.gz: 445a3ef72888dd358cc5d917ca31de7499df3368797c9c73324704e0e2d8307a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 338cf3a26b64efb827b5691addc67987048e0522f467d89c5ff4a18494f6b0e3cbeb93513e419a01b04be643d9f7e7a36c1aaf1b0a9d7f5c8158ecff1eb315e7
|
|
7
|
+
data.tar.gz: 4a6b9282a87124e037b3f101a5856d4a8756e86f9fb5b8bf3a4e42e7e2790e0edbd78b7263b8e9c653bedc25700d8a3fb8ade671e2f99e1e74dbfd49780c20eb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
## master (unreleased)
|
|
2
2
|
|
|
3
|
+
## 0.36.0 (2026-07-20)
|
|
4
|
+
|
|
5
|
+
- Serialize to JSON `cursor` value for background data migrations
|
|
6
|
+
|
|
7
|
+
Note: Run `bin/rails generate online_migrations:upgrade` if using background migrations.
|
|
8
|
+
|
|
9
|
+
- Do not consider running background migrations as stuck
|
|
10
|
+
- Do not automatically retry stuck background migrations
|
|
11
|
+
|
|
12
|
+
## 0.35.0 (2026-07-01)
|
|
13
|
+
|
|
14
|
+
- Drop support for Ruby < 3.3
|
|
15
|
+
- Defer `tick_total` calculation to the background data migration starting time instead of enqueueing time
|
|
16
|
+
- Make "replacing an index" check independent of the live database state
|
|
17
|
+
|
|
3
18
|
## 0.34.0 (2026-05-29)
|
|
4
19
|
|
|
5
20
|
- Drop support for Rails < 7.2
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)
|
|
|
16
16
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
19
|
-
- Ruby 3.
|
|
19
|
+
- Ruby 3.3+
|
|
20
20
|
- Rails 7.2+
|
|
21
21
|
- PostgreSQL 12+
|
|
22
22
|
|
|
@@ -342,6 +342,8 @@ during writes works automatically). For most column type changes, this does not
|
|
|
342
342
|
|
|
343
343
|
```ruby
|
|
344
344
|
class CleanupChangeFilesSizeType < ActiveRecord::Migration[8.0]
|
|
345
|
+
disable_ddl_transaction!
|
|
346
|
+
|
|
345
347
|
def up
|
|
346
348
|
cleanup_column_type_change :files, :size
|
|
347
349
|
end
|
data/lib/generators/online_migrations/templates/background_data_migrations_convert_cursor.rb.tt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class BackgroundDataMigrationsConvertCursor < <%= migration_parent %>
|
|
2
|
+
def change
|
|
3
|
+
up_only do
|
|
4
|
+
OnlineMigrations::DataMigrations::Migration.find_each do |migration|
|
|
5
|
+
OnlineMigrations::DataMigrations::Migration.where(id: migration.id).update_all(cursor: migration.cursor)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -48,7 +48,7 @@ module OnlineMigrations
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
iteration_pause_column = connection.columns(:background_data_migrations).find { |c| c.name == "iteration_pause" }
|
|
51
|
-
if iteration_pause_column
|
|
51
|
+
if iteration_pause_column&.default
|
|
52
52
|
migrations << "background_data_migrations_remove_iteration_pause_default"
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -57,6 +57,8 @@ module OnlineMigrations
|
|
|
57
57
|
migrations << "background_migrations_change_status_default"
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
migrations << "background_data_migrations_convert_cursor"
|
|
61
|
+
|
|
60
62
|
migrations
|
|
61
63
|
end
|
|
62
64
|
|
|
@@ -52,6 +52,8 @@ module OnlineMigrations
|
|
|
52
52
|
|
|
53
53
|
enum :status, STATUSES.index_with(&:to_s)
|
|
54
54
|
|
|
55
|
+
serialize :cursor, coder: JSON
|
|
56
|
+
|
|
55
57
|
validates :migration_name, presence: true
|
|
56
58
|
validates :arguments, uniqueness: { scope: [:migration_name, :shard] }
|
|
57
59
|
|
|
@@ -66,6 +68,13 @@ module OnlineMigrations
|
|
|
66
68
|
migration_name.sub(/^(::)?#{namespace}::/, "")
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
# TODO: delete in some future version
|
|
72
|
+
def cursor
|
|
73
|
+
self[:cursor]
|
|
74
|
+
rescue JSON::ParserError
|
|
75
|
+
cursor_before_type_cast
|
|
76
|
+
end
|
|
77
|
+
|
|
69
78
|
def migration_name=(class_name)
|
|
70
79
|
class_name = class_name.name if class_name.is_a?(Class)
|
|
71
80
|
write_attribute(:migration_name, self.class.normalize_migration_name(class_name))
|
|
@@ -111,19 +120,25 @@ module OnlineMigrations
|
|
|
111
120
|
end
|
|
112
121
|
|
|
113
122
|
# Returns whether a migration is stuck, which is defined as having a status of
|
|
114
|
-
#
|
|
123
|
+
# cancelling or pausing, and not having been updated in the last 5 minutes.
|
|
115
124
|
#
|
|
116
125
|
# @return [Boolean] whether the migration is stuck.
|
|
117
126
|
#
|
|
118
127
|
def stuck?
|
|
119
128
|
stuck_timeout = OnlineMigrations.config.background_data_migrations.stuck_timeout
|
|
120
|
-
(
|
|
129
|
+
(cancelling? || pausing?) && updated_at <= stuck_timeout.ago
|
|
121
130
|
end
|
|
122
131
|
|
|
123
132
|
# @private
|
|
124
133
|
def start
|
|
125
134
|
if enqueued?
|
|
126
|
-
|
|
135
|
+
# Defer `tick_total` calculation to the migration starting time
|
|
136
|
+
# instead of the enqueueing time to avoid failed deploys.
|
|
137
|
+
self.tick_total ||= safely_calculate_tick_total
|
|
138
|
+
self.status = :running
|
|
139
|
+
self.started_at = Time.current
|
|
140
|
+
save!
|
|
141
|
+
|
|
127
142
|
data_migration.after_start
|
|
128
143
|
true
|
|
129
144
|
else
|
|
@@ -309,11 +324,20 @@ module OnlineMigrations
|
|
|
309
324
|
def set_defaults
|
|
310
325
|
config = ::OnlineMigrations.config.background_data_migrations
|
|
311
326
|
self.max_attempts ||= config.max_attempts
|
|
312
|
-
self.
|
|
327
|
+
self.iteration_pause ||= config.iteration_pause
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def safely_calculate_tick_total
|
|
331
|
+
on_shard_if_present do
|
|
313
332
|
data_migration.count
|
|
314
333
|
end
|
|
334
|
+
rescue ActiveRecord::QueryCanceled => e
|
|
335
|
+
OnlineMigrations.config.background_data_migrations.error_handler.call(e, self)
|
|
315
336
|
|
|
316
|
-
|
|
337
|
+
# `tick_total` is not required and is used only for progress tracking.
|
|
338
|
+
# Probably the `count` method was implemented in a non-efficient way.
|
|
339
|
+
# Better to not track progress than have a failing migration.
|
|
340
|
+
nil
|
|
317
341
|
end
|
|
318
342
|
|
|
319
343
|
def instrument_status_change
|
|
@@ -13,14 +13,12 @@ module OnlineMigrations
|
|
|
13
13
|
# enqueued -> failed occurs when the migration job fails to be enqueued, or
|
|
14
14
|
# if the migration is deleted before is starts running.
|
|
15
15
|
"enqueued" => ["running", "paused", "cancelled", "failed"],
|
|
16
|
-
# running -> enqueued occurs when the migration is stuck and rescheduled by the scheduler.
|
|
17
16
|
# running -> succeeded occurs when the migration completes successfully.
|
|
18
17
|
# running -> pausing occurs when a user pauses the migration as it's performing.
|
|
19
18
|
# running -> cancelling occurs when a user cancels the migration as it's performing.
|
|
20
19
|
# running -> errored occurs when the migration raised an error during the last run.
|
|
21
20
|
# running -> failed occurs when the migration raises an error when running and retry attempts exceeded.
|
|
22
21
|
"running" => [
|
|
23
|
-
"enqueued",
|
|
24
22
|
"succeeded",
|
|
25
23
|
"pausing",
|
|
26
24
|
"cancelling",
|
|
@@ -36,11 +36,10 @@ module OnlineMigrations
|
|
|
36
36
|
migrations_to_enqueue = []
|
|
37
37
|
|
|
38
38
|
with_lock do
|
|
39
|
-
|
|
40
|
-
runnable_migrations = migrations_with_existing_classes(relation.pending) + stuck_migrations
|
|
39
|
+
runnable_migrations = migrations_with_existing_classes(relation.pending)
|
|
41
40
|
|
|
42
41
|
# Ensure no more than 'concurrency' migrations are running at the same time.
|
|
43
|
-
remaining_to_enqueue = concurrency -
|
|
42
|
+
remaining_to_enqueue = concurrency - relation.running.count
|
|
44
43
|
if remaining_to_enqueue > 0
|
|
45
44
|
runnable_migrations.take(remaining_to_enqueue).each do |migration|
|
|
46
45
|
migration.update!(status: :enqueued)
|
|
@@ -23,6 +23,7 @@ module OnlineMigrations
|
|
|
23
23
|
MAX_IDENTIFIER_LENGTH = 63
|
|
24
24
|
|
|
25
25
|
self.table_name = :background_schema_migrations
|
|
26
|
+
self.ignored_columns += ["parent_id", "composite"]
|
|
26
27
|
|
|
27
28
|
scope :queue_order, -> { order(created_at: :asc) }
|
|
28
29
|
scope :active, -> { where(status: [:pending, :running, :errored]) }
|
|
@@ -80,15 +81,10 @@ module OnlineMigrations
|
|
|
80
81
|
def progress
|
|
81
82
|
end
|
|
82
83
|
|
|
83
|
-
# Whether the migration is considered stuck
|
|
84
|
+
# Whether the migration is considered stuck.
|
|
84
85
|
#
|
|
85
86
|
def stuck?
|
|
86
|
-
|
|
87
|
-
running? && !index_build_in_progress?
|
|
88
|
-
else
|
|
89
|
-
stuck_timeout = (statement_timeout || 1.day) + 10.minutes
|
|
90
|
-
running? && updated_at <= stuck_timeout.seconds.ago
|
|
91
|
-
end
|
|
87
|
+
index_addition? && running? && !index_build_in_progress?
|
|
92
88
|
end
|
|
93
89
|
|
|
94
90
|
# Mark this migration as ready to be processed again.
|
|
@@ -44,9 +44,8 @@ module OnlineMigrations
|
|
|
44
44
|
# Background schema migrations could take a while to run. It is possible, that the process
|
|
45
45
|
# never reaches this (or the rescue below) line of code. E.g., when it is force quitted
|
|
46
46
|
# (SIGKILL etc.) and so the migration will end up in the "running" state and the query is
|
|
47
|
-
# still executing (or already finished) in the database. This migration can
|
|
48
|
-
# manually retried
|
|
49
|
-
# this migration is stuck.
|
|
47
|
+
# still executing (or already finished) in the database. This migration can be safely
|
|
48
|
+
# manually retried.
|
|
50
49
|
|
|
51
50
|
migration.update!(status: :succeeded, finished_at: Time.current)
|
|
52
51
|
|
|
@@ -35,8 +35,8 @@ module OnlineMigrations
|
|
|
35
35
|
|
|
36
36
|
private
|
|
37
37
|
def find_migration(**options)
|
|
38
|
-
|
|
39
|
-
runnable_migrations = (Migration.pending + Migration.errored
|
|
38
|
+
active_migrations = Migration.running.to_a
|
|
39
|
+
runnable_migrations = (Migration.pending + Migration.errored).sort_by(&:created_at)
|
|
40
40
|
|
|
41
41
|
if options.key?(:shard)
|
|
42
42
|
runnable_migrations = runnable_migrations.select { |migration| migration.shard.to_s == options[:shard].to_s }
|
|
@@ -212,7 +212,14 @@ module OnlineMigrations
|
|
|
212
212
|
conversions = column_names.map do |column_name|
|
|
213
213
|
tmp_column = __change_type_column(column_name)
|
|
214
214
|
|
|
215
|
-
old_value =
|
|
215
|
+
old_value = begin
|
|
216
|
+
# Delete after supporting only ActiveRecord >= 8.2
|
|
217
|
+
Arel::Table.new(table_name)[column_name]
|
|
218
|
+
rescue ArgumentError
|
|
219
|
+
# https://github.com/rails/rails/commit/b1650993b02497ae7d0d8b984d40bc036e62c681
|
|
220
|
+
Arel::Table.new(name: table_name)[column_name]
|
|
221
|
+
end
|
|
222
|
+
|
|
216
223
|
if (type_cast_function = type_cast_functions.with_indifferent_access[column_name])
|
|
217
224
|
old_value =
|
|
218
225
|
case type_cast_function
|
|
@@ -393,10 +400,8 @@ module OnlineMigrations
|
|
|
393
400
|
def __options_from_column(column, options)
|
|
394
401
|
result = {}
|
|
395
402
|
options.each do |option|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
result[option] = value if !value.nil?
|
|
399
|
-
end
|
|
403
|
+
value = column.public_send(option)
|
|
404
|
+
result[option] = value if !value.nil?
|
|
400
405
|
end
|
|
401
406
|
result
|
|
402
407
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "erb"
|
|
4
|
-
require "set"
|
|
5
4
|
|
|
6
5
|
module OnlineMigrations
|
|
7
6
|
# @private
|
|
@@ -46,7 +45,7 @@ module OnlineMigrations
|
|
|
46
45
|
|
|
47
46
|
true
|
|
48
47
|
end
|
|
49
|
-
ruby2_keywords(:check)
|
|
48
|
+
ruby2_keywords(:check)
|
|
50
49
|
|
|
51
50
|
def version_safe?
|
|
52
51
|
version && version <= OnlineMigrations.config.start_after
|
|
@@ -574,13 +573,21 @@ module OnlineMigrations
|
|
|
574
573
|
Array(index.columns).map(&:to_s) == Array(options[:column]).map(&:to_s)
|
|
575
574
|
end
|
|
576
575
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
[option, index_def.public_send(option)] if index_def.respond_to?(option)
|
|
580
|
-
end.to_h
|
|
576
|
+
# IndexDefinition expects "columns", but "remove_index" API uses "column".
|
|
577
|
+
options[:columns] = options[:column]
|
|
581
578
|
|
|
582
|
-
|
|
583
|
-
|
|
579
|
+
# Fall back to the declared options when the index isn't present in the database,
|
|
580
|
+
# so an index remove → add is still recorded and caught.
|
|
581
|
+
existing_options =
|
|
582
|
+
[:name, :columns, :unique, :where, :type, :using, :opclasses].index_with do |option|
|
|
583
|
+
if index_def
|
|
584
|
+
index_def.public_send(option)
|
|
585
|
+
else
|
|
586
|
+
options[option]
|
|
587
|
+
end
|
|
588
|
+
end.compact
|
|
589
|
+
|
|
590
|
+
@removed_indexes << IndexDefinition.new(table: table_name, **existing_options)
|
|
584
591
|
end
|
|
585
592
|
|
|
586
593
|
def add_foreign_key(from_table, to_table, **options)
|
|
@@ -844,8 +851,8 @@ module OnlineMigrations
|
|
|
844
851
|
|
|
845
852
|
def index_include_column?(index, column)
|
|
846
853
|
index.columns.include?(column) ||
|
|
847
|
-
|
|
848
|
-
|
|
854
|
+
index.include&.include?(column) ||
|
|
855
|
+
index.where&.include?(column)
|
|
849
856
|
end
|
|
850
857
|
|
|
851
858
|
def run_custom_checks(method, args)
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
module OnlineMigrations
|
|
4
4
|
# @private
|
|
5
5
|
class IndexDefinition
|
|
6
|
-
attr_reader :table, :columns, :unique, :opclasses, :where, :type, :using
|
|
6
|
+
attr_reader :table, :name, :columns, :unique, :opclasses, :where, :type, :using
|
|
7
7
|
|
|
8
8
|
def initialize(**options)
|
|
9
|
-
@table = options[:table]
|
|
9
|
+
@table = options[:table]&.to_s
|
|
10
|
+
@name = options[:name]&.to_s
|
|
10
11
|
@columns = Array(options[:columns]).map(&:to_s)
|
|
11
12
|
@unique = options[:unique]
|
|
12
13
|
@opclasses = options[:opclass] || {}
|
|
@@ -20,7 +21,7 @@ module OnlineMigrations
|
|
|
20
21
|
# For ActiveRecord::ConnectionAdapters::IndexDefinition is for expression indexes,
|
|
21
22
|
# `columns` is a string
|
|
22
23
|
table == other.table &&
|
|
23
|
-
columns.intersect?(Array(other.columns))
|
|
24
|
+
((name && name == other.name) || columns.intersect?(Array(other.columns)))
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# @param other [OnlineMigrations::IndexDefinition, ActiveRecord::ConnectionAdapters::IndexDefinition]
|
|
@@ -28,7 +29,7 @@ module OnlineMigrations
|
|
|
28
29
|
return false if type != other.type
|
|
29
30
|
return false if using != other.using
|
|
30
31
|
return false if where != other.where
|
|
31
|
-
return false if
|
|
32
|
+
return false if opclasses != other.opclasses
|
|
32
33
|
|
|
33
34
|
if unique && !other.unique
|
|
34
35
|
false
|
|
@@ -111,7 +111,7 @@ module OnlineMigrations
|
|
|
111
111
|
|
|
112
112
|
relation.update_all(updates)
|
|
113
113
|
|
|
114
|
-
progress.call(relation) if progress
|
|
114
|
+
progress.call(relation) if progress.present?
|
|
115
115
|
|
|
116
116
|
sleep(pause_ms * 0.001) if pause_ms > 0
|
|
117
117
|
end
|
|
@@ -635,7 +635,7 @@ module OnlineMigrations
|
|
|
635
635
|
# @see https://api.rubyonrails.org/v8.1/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_column
|
|
636
636
|
#
|
|
637
637
|
def add_column(table_name, column_name, type, **options)
|
|
638
|
-
if column_exists?(table_name, column_name
|
|
638
|
+
if column_exists?(table_name, column_name)
|
|
639
639
|
Utils.say("Column was not added because it already exists (this may be due to an aborted migration " \
|
|
640
640
|
"or similar) table_name: #{table_name}, column_name: #{column_name}")
|
|
641
641
|
else
|
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.
|
|
4
|
+
version: 0.36.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fatkodima
|
|
@@ -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_convert_cursor.rb.tt
|
|
44
45
|
- lib/generators/online_migrations/templates/background_data_migrations_remove_iteration_pause_default.rb.tt
|
|
45
46
|
- lib/generators/online_migrations/templates/background_migrations_change_status_default.rb.tt
|
|
46
47
|
- lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt
|
|
@@ -108,14 +109,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
108
109
|
requirements:
|
|
109
110
|
- - ">="
|
|
110
111
|
- !ruby/object:Gem::Version
|
|
111
|
-
version: '3.
|
|
112
|
+
version: '3.3'
|
|
112
113
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
114
|
requirements:
|
|
114
115
|
- - ">="
|
|
115
116
|
- !ruby/object:Gem::Version
|
|
116
117
|
version: '0'
|
|
117
118
|
requirements: []
|
|
118
|
-
rubygems_version: 4.0.
|
|
119
|
+
rubygems_version: 4.0.14
|
|
119
120
|
specification_version: 4
|
|
120
121
|
summary: Catch unsafe PostgreSQL migrations in development and run them easier in
|
|
121
122
|
production
|