online_migrations 0.9.2 → 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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +155 -150
  4. data/docs/background_migrations.md +43 -10
  5. data/docs/configuring.md +23 -18
  6. data/lib/generators/online_migrations/install_generator.rb +3 -7
  7. data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +18 -0
  8. data/lib/generators/online_migrations/templates/initializer.rb.tt +12 -3
  9. data/lib/generators/online_migrations/templates/migration.rb.tt +8 -3
  10. data/lib/generators/online_migrations/upgrade_generator.rb +33 -0
  11. data/lib/online_migrations/background_migrations/application_record.rb +13 -0
  12. data/lib/online_migrations/background_migrations/backfill_column.rb +1 -1
  13. data/lib/online_migrations/background_migrations/copy_column.rb +11 -19
  14. data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +2 -20
  15. data/lib/online_migrations/background_migrations/migration.rb +123 -34
  16. data/lib/online_migrations/background_migrations/migration_helpers.rb +0 -4
  17. data/lib/online_migrations/background_migrations/migration_job.rb +15 -12
  18. data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -2
  19. data/lib/online_migrations/background_migrations/migration_runner.rb +56 -11
  20. data/lib/online_migrations/background_migrations/reset_counters.rb +3 -9
  21. data/lib/online_migrations/background_migrations/scheduler.rb +5 -15
  22. data/lib/online_migrations/change_column_type_helpers.rb +71 -86
  23. data/lib/online_migrations/command_checker.rb +50 -46
  24. data/lib/online_migrations/config.rb +19 -15
  25. data/lib/online_migrations/copy_trigger.rb +15 -10
  26. data/lib/online_migrations/error_messages.rb +13 -25
  27. data/lib/online_migrations/foreign_keys_collector.rb +2 -2
  28. data/lib/online_migrations/indexes_collector.rb +3 -3
  29. data/lib/online_migrations/lock_retrier.rb +4 -9
  30. data/lib/online_migrations/schema_cache.rb +0 -6
  31. data/lib/online_migrations/schema_dumper.rb +21 -0
  32. data/lib/online_migrations/schema_statements.rb +80 -256
  33. data/lib/online_migrations/utils.rb +36 -55
  34. data/lib/online_migrations/verbose_sql_logs.rb +3 -2
  35. data/lib/online_migrations/version.rb +1 -1
  36. data/lib/online_migrations.rb +9 -6
  37. metadata +9 -7
  38. data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
  39. data/lib/online_migrations/foreign_key_definition.rb +0 -17
data/docs/configuring.md CHANGED
@@ -3,6 +3,8 @@
3
3
  There are a few configurable options for the gem. Custom configurations should be placed in a `online_migrations.rb` initializer.
4
4
 
5
5
  ```ruby
6
+ # config/initializers/online_migrations.rb
7
+
6
8
  OnlineMigrations.configure do |config|
7
9
  # ...
8
10
  end
@@ -15,8 +17,6 @@ end
15
17
  Add your own custom checks with:
16
18
 
17
19
  ```ruby
18
- # config/initializers/online_migrations.rb
19
-
20
20
  config.add_check do |method, args|
21
21
  if method == :add_column && args[0].to_s == "users"
22
22
  stop!("No more columns on the users table")
@@ -33,8 +33,6 @@ Use the `stop!` method to stop migrations.
33
33
  Disable specific checks with:
34
34
 
35
35
  ```ruby
36
- # config/initializers/online_migrations.rb
37
-
38
36
  config.disable_check(:remove_index)
39
37
  ```
40
38
 
@@ -45,8 +43,6 @@ Check the [source code](https://github.com/fatkodima/online_migrations/blob/mast
45
43
  By default, checks are disabled when migrating down. Enable them with:
46
44
 
47
45
  ```ruby
48
- # config/initializers/online_migrations.rb
49
-
50
46
  config.check_down = true
51
47
  ```
52
48
 
@@ -55,8 +51,6 @@ config.check_down = true
55
51
  You can customize specific error messages:
56
52
 
57
53
  ```ruby
58
- # config/initializers/online_migrations.rb
59
-
60
54
  config.error_messages[:add_column_default] = "Your custom instructions"
61
55
  ```
62
56
 
@@ -88,20 +82,19 @@ ALTER ROLE myuser SET statement_timeout = '15s';
88
82
  You can configure this gem to automatically retry statements that exceed the lock timeout:
89
83
 
90
84
  ```ruby
91
- # config/initializers/online_migrations.rb
92
-
93
85
  config.lock_retrier = OnlineMigrations::ExponentialLockRetrier.new(
94
86
  attempts: 30, # attempt 30 retries
95
87
  base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
96
88
  max_delay: 1.minute, # maximum delay is 1 minute
97
- lock_timeout: 0.05.seconds # and 50ms set as lock timeout for each try
89
+ lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try
98
90
  )
99
91
  ```
100
92
 
101
93
  When statement within transaction fails - the whole transaction is retried.
102
94
 
103
95
  To permanently disable lock retries, you can set `lock_retrier` to `nil`.
104
- To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_RETRIES` env variable.
96
+
97
+ To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_RETRIES` env variable. This is useful when you are deploying a hotfix and do not want to wait too long while the lock retrier safely tries to acquire the lock, but try to acquire the lock immediately with the default configured lock timeout value.
105
98
 
106
99
  **Note**: Statements are retried by default, unless lock retries are disabled. It is possible to implement more sophisticated lock retriers. See [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/lock_retrier.rb) for the examples.
107
100
 
@@ -110,8 +103,6 @@ To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_
110
103
  To mark migrations as safe that were created before installing this gem, configure the migration version starting after which checks are performed:
111
104
 
112
105
  ```ruby
113
- # config/initializers/online_migrations.rb
114
-
115
106
  config.start_after = 20220101000000
116
107
 
117
108
  # or if you use multiple databases (Active Record 6+)
@@ -125,8 +116,6 @@ Use the version from your latest migration.
125
116
  If your development database version is different from production, you can specify the production version so the right checks run in development.
126
117
 
127
118
  ```ruby
128
- # config/initializers/online_migrations.rb
129
-
130
119
  config.target_version = 10 # or "12.9" etc
131
120
 
132
121
  # or if you use multiple databases (Active Record 6+)
@@ -200,9 +189,25 @@ So you can actually check which steps are performed.
200
189
  To enable verbose sql logs:
201
190
 
202
191
  ```ruby
203
- # config/initializers/online_migrations.rb
204
-
205
192
  config.verbose_sql_logs = true
206
193
  ```
207
194
 
208
195
  This feature is enabled by default in a production Rails environment. You can override this setting via `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
196
+
197
+ ## Analyze Tables
198
+
199
+ Analyze tables automatically (to update planner statistics) after an index is added.
200
+ Add to an initializer file:
201
+
202
+ ```ruby
203
+ config.auto_analyze = true
204
+ ```
205
+
206
+ ## Schema Sanity
207
+
208
+ Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/).
209
+ To alphabetize columns:
210
+
211
+ ```ruby
212
+ config.alphabetize_schema = true
213
+ ```
@@ -15,20 +15,16 @@ module OnlineMigrations
15
15
  end
16
16
 
17
17
  def create_migration_file
18
- migration_template("migration.rb", File.join(migrations_dir, "install_online_migrations.rb"))
18
+ migration_template("migration.rb", File.join(db_migrate_path, "install_online_migrations.rb"))
19
19
  end
20
20
 
21
21
  private
22
22
  def migration_parent
23
- Utils.migration_parent_string
23
+ "ActiveRecord::Migration[#{Utils.ar_version}]"
24
24
  end
25
25
 
26
26
  def start_after
27
- self.class.current_migration_number(migrations_dir)
28
- end
29
-
30
- def migrations_dir
31
- Utils.ar_version >= 5.1 ? db_migrate_path : "db/migrate"
27
+ self.class.current_migration_number(db_migrate_path)
32
28
  end
33
29
  end
34
30
  end
@@ -0,0 +1,18 @@
1
+ class AddShardingToOnlineMigrations < <%= migration_parent %>
2
+ def change
3
+ safety_assured do
4
+ remove_index :background_migrations, [:migration_name, :arguments], unique: true
5
+
6
+ change_table :background_migrations do |t|
7
+ t.bigint :parent_id
8
+ t.string :shard
9
+ t.boolean :composite, default: false, null: false
10
+
11
+ t.foreign_key :background_migrations, column: :parent_id, on_delete: :cascade
12
+
13
+ t.index [:migration_name, :arguments, :shard],
14
+ unique: true, name: :index_background_migrations_on_unique_configuration
15
+ end
16
+ end
17
+ end
18
+ end
@@ -23,13 +23,22 @@ OnlineMigrations.configure do |config|
23
23
  # It is considered safe to perform most of the dangerous operations on them.
24
24
  # config.small_tables = []
25
25
 
26
+ # Analyze tables after indexes are added.
27
+ # Outdated statistics can sometimes hurt performance.
28
+ # config.auto_analyze = true
29
+
30
+ # Alphabetize table columns when dumping the schema.
31
+ # config.alphabetize_schema = true
32
+
26
33
  # Disable specific checks.
27
- # For the list of available checks look at `lib/error_messages` folder.
34
+ # For the list of available checks look at the `error_messages.rb` file inside
35
+ # the `online_migrations` gem.
28
36
  # config.disable_check(:remove_index)
29
37
 
30
38
  # Enable specific checks. All checks are enabled by default,
31
39
  # but this may change in the future.
32
- # For the list of available checks look at `lib/error_messages` folder.
40
+ # For the list of available checks look at the `error_messages.rb` file inside
41
+ # the `online_migrations` gem.
33
42
  # config.enable_check(:remove_index)
34
43
 
35
44
  # Configure whether to log every SQL query happening in a migration.
@@ -48,7 +57,7 @@ OnlineMigrations.configure do |config|
48
57
  attempts: 30, # attempt 30 retries
49
58
  base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
50
59
  max_delay: 1.minute, # up to the maximum delay of 1 minute
51
- lock_timeout: 0.05.seconds # and 50ms set as lock timeout for each try
60
+ lock_timeout: 0.2.seconds # and 200ms set as lock timeout for each try
52
61
  )
53
62
 
54
63
  # Configure tables that are in the process of being renamed.
@@ -1,6 +1,7 @@
1
1
  class InstallOnlineMigrations < <%= migration_parent %>
2
2
  def change
3
3
  create_table :background_migrations do |t|
4
+ t.bigint :parent_id
4
5
  t.string :migration_name, null: false
5
6
  t.jsonb :arguments, default: [], null: false
6
7
  t.string :batch_column_name, null: false
@@ -13,9 +14,13 @@ class InstallOnlineMigrations < <%= migration_parent %>
13
14
  t.integer :sub_batch_pause_ms, null: false
14
15
  t.integer :batch_max_attempts, null: false
15
16
  t.string :status, default: "enqueued", null: false
16
- t.timestamps null: false
17
+ t.string :shard
18
+ t.boolean :composite, default: false, null: false
19
+ t.timestamps
17
20
 
18
- t.index [:migration_name, :arguments],
21
+ t.foreign_key :background_migrations, column: :parent_id, on_delete: :cascade
22
+
23
+ t.index [:migration_name, :arguments, :shard],
19
24
  unique: true, name: :index_background_migrations_on_unique_configuration
20
25
  end
21
26
 
@@ -34,7 +39,7 @@ class InstallOnlineMigrations < <%= migration_parent %>
34
39
  t.string :error_class
35
40
  t.string :error_message
36
41
  t.string :backtrace, array: true
37
- t.timestamps null: false
42
+ t.timestamps
38
43
 
39
44
  t.foreign_key :background_migrations, column: :migration_id, on_delete: :cascade
40
45
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record/migration"
5
+
6
+ module OnlineMigrations
7
+ # @private
8
+ class UpgradeGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ def copy_templates
14
+ migrations_to_be_applied.each do |migration|
15
+ migration_template("#{migration}.rb", File.join(db_migrate_path, "#{migration}.rb"))
16
+ end
17
+ end
18
+
19
+ private
20
+ def migrations_to_be_applied
21
+ connection = BackgroundMigrations::Migration.connection
22
+ columns = connection.columns(BackgroundMigrations::Migration.table_name).map(&:name)
23
+
24
+ migrations = []
25
+ migrations << "add_sharding_to_online_migrations" if !columns.include?("shard")
26
+ migrations
27
+ end
28
+
29
+ def migration_parent
30
+ "ActiveRecord::Migration[#{Utils.ar_version}]"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ module BackgroundMigrations
5
+ # Base class for all records used by this gem.
6
+ #
7
+ # Can be extended to setup different database where all tables related to
8
+ # online_migrations will live.
9
+ class ApplicationRecord < ActiveRecord::Base
10
+ self.abstract_class = true
11
+ end
12
+ end
13
+ end
@@ -22,7 +22,7 @@ module OnlineMigrations
22
22
  quoted_column = connection.quote_column_name(column)
23
23
  model.unscoped.where("#{quoted_column} != ? OR #{quoted_column} IS NULL", value)
24
24
  else
25
- Utils.ar_where_not_multiple_conditions(model.unscoped, updates)
25
+ model.unscoped.where.not(updates)
26
26
  end
27
27
  end
28
28
 
@@ -25,14 +25,10 @@ module OnlineMigrations
25
25
  end
26
26
 
27
27
  def relation
28
- relation = model
28
+ model
29
29
  .unscoped
30
- .where(copy_to.map { |to_column| [to_column, nil] }.to_h)
31
-
32
- Utils.ar_where_not_multiple_conditions(
33
- relation,
34
- copy_from.map { |from_column| [from_column, nil] }.to_h
35
- )
30
+ .where(copy_to.index_with(nil))
31
+ .where.not(copy_from.index_with(nil))
36
32
  end
37
33
 
38
34
  def process_batch(relation)
@@ -42,22 +38,18 @@ module OnlineMigrations
42
38
  old_values = copy_from.map do |from_column|
43
39
  old_value = arel_table[from_column]
44
40
  if (type_cast_function = type_cast_functions[from_column])
45
- if Utils.ar_version <= 5.2
46
- # Active Record <= 5.2 does not support quoting of Arel::Nodes::NamedFunction
47
- old_value = Arel.sql("#{type_cast_function}(#{connection.quote_column_name(from_column)})")
48
- else
49
- old_value = Arel::Nodes::NamedFunction.new(type_cast_function, [old_value])
50
- end
41
+ old_value =
42
+ if type_cast_function.match?(/\A\w+\z/)
43
+ Arel::Nodes::NamedFunction.new(type_cast_function, [old_value])
44
+ else
45
+ # We got a cast expression.
46
+ Arel.sql(type_cast_function)
47
+ end
51
48
  end
52
49
  old_value
53
50
  end
54
51
 
55
- if Utils.ar_version <= 4.2
56
- stmt = Arel::UpdateManager.new(arel.engine)
57
- else
58
- stmt = Arel::UpdateManager.new
59
- end
60
-
52
+ stmt = Arel::UpdateManager.new
61
53
  stmt.table(arel_table)
62
54
  stmt.wheres = arel.constraints
63
55
 
@@ -12,29 +12,11 @@ module OnlineMigrations
12
12
  end
13
13
 
14
14
  def relation
15
- # For Active Record 6.1+ we can use `where.missing`
16
- # https://github.com/rails/rails/pull/34727
17
- associations.inject(model.unscoped) do |relation, association|
18
- reflection = model.reflect_on_association(association)
19
- if reflection.nil?
20
- raise ArgumentError, "'#{model.name}' has no association called '#{association}'"
21
- end
22
-
23
- # left_joins was added in Active Record 5.0 - https://github.com/rails/rails/pull/12071
24
- relation
25
- .left_joins(association)
26
- .where(reflection.table_name => { reflection.association_primary_key => nil })
27
- end
15
+ model.unscoped.where.missing(*associations)
28
16
  end
29
17
 
30
18
  def process_batch(relation)
31
- if Utils.ar_version > 5.0
32
- relation.delete_all
33
- else
34
- # Older Active Record generates incorrect query when running delete_all
35
- primary_key = model.primary_key
36
- model.unscoped.where(primary_key => relation.select(primary_key)).delete_all
37
- end
19
+ relation.delete_all
38
20
  end
39
21
 
40
22
  def count
@@ -2,7 +2,7 @@
2
2
 
3
3
  module OnlineMigrations
4
4
  module BackgroundMigrations
5
- class Migration < ActiveRecord::Base
5
+ class Migration < ApplicationRecord
6
6
  STATUSES = [
7
7
  :enqueued, # The migration has been enqueued by the user.
8
8
  :running, # The migration is being performed by a migration executor.
@@ -15,25 +15,29 @@ module OnlineMigrations
15
15
  self.table_name = :background_migrations
16
16
 
17
17
  scope :queue_order, -> { order(created_at: :asc) }
18
+ scope :runnable, -> { where(composite: false) }
18
19
  scope :active, -> { where(status: [statuses[:enqueued], statuses[:running]]) }
20
+ scope :except_succeeded, -> { where.not(status: :succeeded) }
19
21
  scope :for_migration_name, ->(migration_name) { where(migration_name: normalize_migration_name(migration_name)) }
20
22
  scope :for_configuration, ->(migration_name, arguments) do
21
23
  for_migration_name(migration_name).where("arguments = ?", arguments.to_json)
22
24
  end
23
25
 
24
- enum status: STATUSES.map { |status| [status, status.to_s] }.to_h
26
+ enum status: STATUSES.index_with(&:to_s)
25
27
 
28
+ belongs_to :parent, class_name: name, optional: true
29
+ has_many :children, class_name: name, foreign_key: :parent_id
26
30
  has_many :migration_jobs
27
31
 
28
32
  validates :migration_name, :batch_column_name, presence: true
29
33
 
30
- validates :min_value, :max_value, :batch_size, :sub_batch_size,
31
- presence: true, numericality: { greater_than: 0 }
34
+ validates :batch_size, :sub_batch_size, presence: true, numericality: { greater_than: 0 }
35
+ validates :min_value, :max_value, presence: true, numericality: { greater_than: 0, unless: :composite? }
32
36
 
33
37
  validates :batch_pause, :sub_batch_pause_ms, presence: true,
34
38
  numericality: { greater_than_or_equal_to: 0 }
35
- validates :rows_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
36
- validates :arguments, uniqueness: { scope: :migration_name }
39
+ validates :rows_count, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true, unless: :composite?
40
+ validates :arguments, uniqueness: { scope: [:migration_name, :shard] }
37
41
 
38
42
  validate :validate_batch_column_values
39
43
  validate :validate_batch_sizes
@@ -43,6 +47,8 @@ module OnlineMigrations
43
47
  validates_with MigrationStatusValidator, on: :update
44
48
 
45
49
  before_validation :set_defaults
50
+ before_create :create_child_migrations, if: :composite?
51
+ before_update :copy_attributes_to_children, if: :composite?
46
52
 
47
53
  # @private
48
54
  def self.normalize_migration_name(migration_name)
@@ -58,28 +64,53 @@ module OnlineMigrations
58
64
  succeeded? || failed?
59
65
  end
60
66
 
67
+ # Overwrite enum's generated method to correctly work for composite migrations.
68
+ def paused!
69
+ return super if !composite?
70
+
71
+ transaction do
72
+ super
73
+ children.each { |child| child.paused! if child.enqueued? || child.running? }
74
+ end
75
+ end
76
+
77
+ # Overwrite enum's generated method to correctly work for composite migrations.
78
+ def running!
79
+ return super if !composite?
80
+
81
+ transaction do
82
+ super
83
+ children.each { |child| child.running! if child.paused? }
84
+ end
85
+ end
86
+
61
87
  def last_job
62
- migration_jobs.order(max_value: :desc).first
88
+ migration_jobs.order(:max_value).last
63
89
  end
64
90
 
65
91
  def last_completed_job
66
- migration_jobs.completed.order(finished_at: :desc).first
92
+ migration_jobs.completed.order(:finished_at).last
67
93
  end
68
94
 
69
95
  # Returns the progress of the background migration.
70
96
  #
71
97
  # @return [Float, nil]
72
- # - when background migration is configured to not to track progress, returns `nil`
73
- # - otherwise returns value in range of 0.0 and 1.0
98
+ # - when background migration is configured to not track progress, returns `nil`
99
+ # - otherwise returns value in range from 0.0 to 100.0
74
100
  #
75
101
  def progress
76
102
  if succeeded?
77
- 1.0
103
+ 100.0
104
+ elsif composite?
105
+ progresses = children.map(&:progress).compact
106
+ if progresses.any?
107
+ (progresses.sum / progresses.size).round(2)
108
+ end
78
109
  elsif rows_count
79
110
  jobs_rows_count = migration_jobs.succeeded.sum(:batch_size)
80
111
  # The last migration job may need to process the amount of rows
81
112
  # less than the batch size, so we can get a value > 1.0.
82
- [jobs_rows_count.to_f / rows_count, 1.0].min
113
+ ([jobs_rows_count.to_f / rows_count, 1.0].min * 100).round(2)
83
114
  end
84
115
  end
85
116
 
@@ -95,13 +126,19 @@ module OnlineMigrations
95
126
  migration_object.relation
96
127
  end
97
128
 
129
+ def migration_model
130
+ migration_relation.model
131
+ end
132
+
98
133
  # Returns whether the interval between previous step run has passed.
99
134
  # @return [Boolean]
100
135
  #
101
136
  def interval_elapsed?
102
- if migration_jobs.running.exists?
137
+ last_active_job = migration_jobs.active.order(:updated_at).last
138
+
139
+ if last_active_job && !last_active_job.stuck?
103
140
  false
104
- elsif (job = last_completed_job)
141
+ elsif batch_pause > 0 && (job = last_completed_job)
105
142
  job.finished_at + batch_pause <= Time.current
106
143
  else
107
144
  true
@@ -123,6 +160,14 @@ module OnlineMigrations
123
160
  end
124
161
  end
125
162
 
163
+ # @private
164
+ def on_shard(&block)
165
+ abstract_class = find_abstract_class(migration_model)
166
+
167
+ shard = (self.shard || abstract_class.default_shard).to_sym
168
+ abstract_class.connected_to(shard: shard, role: :writing, &block)
169
+ end
170
+
126
171
  # @private
127
172
  def reset_failed_jobs_attempts
128
173
  iterator = BatchIterator.new(migration_jobs.failed.attempts_exceeded)
@@ -138,16 +183,10 @@ module OnlineMigrations
138
183
 
139
184
  # rubocop:disable Lint/UnreachableLoop
140
185
  iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value) do |relation|
141
- if Utils.ar_version <= 4.2
142
- # Active Record <= 4.2 does not support pluck with Arel nodes
143
- quoted_column = self.class.connection.quote_column_name(batch_column_name)
144
- batch_range = relation.pluck("MIN(#{quoted_column}), MAX(#{quoted_column})").first
145
- else
146
- min = relation.arel_table[batch_column_name].minimum
147
- max = relation.arel_table[batch_column_name].maximum
186
+ min = relation.arel_table[batch_column_name].minimum
187
+ max = relation.arel_table[batch_column_name].maximum
188
+ batch_range = relation.pick(min, max)
148
189
 
149
- batch_range = relation.pluck(min, max).first
150
- end
151
190
  break
152
191
  end
153
192
  # rubocop:enable Lint/UnreachableLoop
@@ -162,6 +201,10 @@ module OnlineMigrations
162
201
  [min_value, max_value]
163
202
  end
164
203
 
204
+ protected
205
+ attr_accessor :child
206
+ alias child? child
207
+
165
208
  private
166
209
  def validate_batch_column_values
167
210
  if max_value.to_i < min_value.to_i
@@ -176,7 +219,13 @@ module OnlineMigrations
176
219
  end
177
220
 
178
221
  def validate_jobs_status
179
- if succeeded? && migration_jobs.except_succeeded.exists?
222
+ if composite?
223
+ if succeeded? && children.except_succeeded.exists?
224
+ errors.add(:base, "all child migrations must be succeeded")
225
+ elsif failed? && !children.failed.exists?
226
+ errors.add(:base, "at least one child migration must be failed")
227
+ end
228
+ elsif succeeded? && migration_jobs.except_succeeded.exists?
180
229
  errors.add(:base, "all migration jobs must be succeeded")
181
230
  elsif failed? && !migration_jobs.failed.exists?
182
231
  errors.add(:base, "at least one migration job must be failed")
@@ -185,12 +234,30 @@ module OnlineMigrations
185
234
 
186
235
  def set_defaults
187
236
  if migration_relation.is_a?(ActiveRecord::Relation)
188
- self.batch_column_name ||= migration_relation.primary_key
189
- self.min_value ||= migration_relation.minimum(batch_column_name)
190
- self.max_value ||= migration_relation.maximum(batch_column_name)
191
-
192
- count = migration_object.count
193
- self.rows_count = count if count != :no_count
237
+ if !child?
238
+ shards = Utils.shard_names(migration_model)
239
+ self.composite = shards.size > 1
240
+ end
241
+
242
+ self.batch_column_name ||= migration_relation.primary_key
243
+
244
+ if composite?
245
+ self.min_value = self.max_value = self.rows_count = -1 # not relevant
246
+ else
247
+ on_shard do
248
+ self.min_value ||= migration_relation.minimum(batch_column_name)
249
+ self.max_value ||= migration_relation.maximum(batch_column_name)
250
+
251
+ # This can be the case when run in development on empty tables
252
+ if min_value.nil?
253
+ # integer IDs minimum value is 1
254
+ self.min_value = self.max_value = 1
255
+ end
256
+
257
+ count = migration_object.count
258
+ self.rows_count = count if count != :no_count
259
+ end
260
+ end
194
261
  end
195
262
 
196
263
  config = ::OnlineMigrations.config.background_migrations
@@ -199,12 +266,27 @@ module OnlineMigrations
199
266
  self.batch_pause ||= config.batch_pause
200
267
  self.sub_batch_pause_ms ||= config.sub_batch_pause_ms
201
268
  self.batch_max_attempts ||= config.batch_max_attempts
269
+ end
270
+
271
+ def create_child_migrations
272
+ shards = Utils.shard_names(migration_model)
202
273
 
203
- # This can be the case when run in development on empty tables
204
- if min_value.nil?
205
- # integer IDs minimum value is 1
206
- self.min_value = self.max_value = 1
274
+ children = shards.map do |shard|
275
+ child = Migration.new(migration_name: migration_name, arguments: arguments, shard: shard)
276
+ child.child = true
277
+ child
207
278
  end
279
+
280
+ self.children = children
281
+ end
282
+
283
+ def copy_attributes_to_children
284
+ attributes = [:batch_size, :sub_batch_size, :batch_pause, :sub_batch_pause_ms, :batch_max_attempts]
285
+ updates = {}
286
+ attributes.each do |attribute|
287
+ updates[attribute] = read_attribute(attribute) if attribute_changed?(attribute)
288
+ end
289
+ children.update_all(updates) if updates.any?
208
290
  end
209
291
 
210
292
  def next_min_value
@@ -214,6 +296,13 @@ module OnlineMigrations
214
296
  min_value
215
297
  end
216
298
  end
299
+
300
+ def find_abstract_class(model)
301
+ model.ancestors.find do |parent|
302
+ parent == ActiveRecord::Base ||
303
+ (parent.is_a?(Class) && parent.abstract_class?)
304
+ end
305
+ end
217
306
  end
218
307
  end
219
308
  end
@@ -223,10 +223,6 @@ module OnlineMigrations
223
223
  # For smaller tables it is probably better and easier to directly find and delete orpahed records.
224
224
  #
225
225
  def delete_orphaned_records_in_background(model_name, *associations, **options)
226
- if Utils.ar_version <= 4.2
227
- raise "#{__method__} does not support Active Record <= 4.2 yet"
228
- end
229
-
230
226
  model_name = model_name.name if model_name.is_a?(Class)
231
227
 
232
228
  enqueue_background_migration(