online_migrations 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a5db4da5657a6887113af46baa5c5ec9a6906ba4f8e3d33afc3d81f378f9b5ae
4
- data.tar.gz: 0acf2706e6b453055d41f338908323a0672066b6e742724b8a6d588e969ca05f
3
+ metadata.gz: 34bd3f3fc2c18bc49962183603c7f2c85e167d978eafd3fb840b639fa98de60d
4
+ data.tar.gz: 0ae0b82440ea7dcec1183c987395742fab0c93618b384f770d7acd3fecd41cdf
5
5
  SHA512:
6
- metadata.gz: 4a1b97dceb858d5c6ebd6ddccab4ea0b3ed8d257074f1e7f6562f101b3ae002ac19576053a44fe765624350c40d6e269d753fd24be5b5419474036a308b5cce5
7
- data.tar.gz: 9b6a3ece26c9e3e679c5d8d0f827eda7aa10b591242440a18d2a0fb18aa8f16b16935db6654859aab9b6f5008160e117c2eb32e98571261127d1a6ac1e936f2c
6
+ metadata.gz: 321b877ffe09ccf2edb94b43d1060f5ebc8d83c1c058a41dc1d248f4d52c161b5a4576186363349c6c027d20890375728590271d24bdb75647d0556a57202aa9
7
+ data.tar.gz: d6a25cf51e31b772e4556b18de85ed88c5019b0469a8d6299e955063042c2e939f4f3f228e05ce4fdc8a4060b2ee2f4160a1e5d477a8769439b103f03b103fb8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.13.0 (2024-01-22)
4
+
5
+ - Add ability to configure the path where generated background migrations will be placed
6
+
7
+ ```ruby
8
+ # It is placed in lib/ by default.
9
+ config.background_migrations.migrations_path = "app/lib"
10
+ ```
11
+
12
+ - Reduce number of queries needed to calculate batch ranges for background migrations
13
+ - Fix `finalize_column_type_change` to not recreate already existing indexes on the temporary column
14
+ - Remove potentially heavy queries used to get the ranges of a background migration
15
+
3
16
  ## 0.12.0 (2024-01-18)
4
17
 
5
18
  - Require passing model name for background migration helpers when using multiple databases
@@ -255,7 +255,7 @@ Specify the throttle condition as a block:
255
255
  ```ruby
256
256
  # config/initializers/online_migrations.rb
257
257
 
258
- OnlineMigrations.config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
258
+ config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
259
259
  ```
260
260
 
261
261
  Note that it's up to you to define a throttling condition that makes sense for your app. For example, you can check various PostgreSQL metrics such as replication lag, DB threads, whether DB writes are available, etc.
@@ -269,7 +269,7 @@ If you want to integrate with an exception monitoring service (e.g. Bugsnag), yo
269
269
  ```ruby
270
270
  # config/initializers/online_migrations.rb
271
271
 
272
- OnlineMigrations.config.background_migrations.error_handler = ->(error, errored_job) do
272
+ config.background_migrations.error_handler = ->(error, errored_job) do
273
273
  Bugsnag.notify(error) do |notification|
274
274
  notification.add_metadata(:background_migration, { name: errored_job.migration_name })
275
275
  end
@@ -281,22 +281,34 @@ The error handler should be a lambda that accepts 2 arguments:
281
281
  * `error`: The exception that was raised.
282
282
  * `errored_job`: An `OnlineMigrations::BackgroundMigrations::MigrationJob` object that represents a failed batch.
283
283
 
284
+ ### Customizing the background migrations path
285
+
286
+ `OnlineMigrations.config.background_migrations.migrations_path` can be configured to define where generated background migrations will be placed.
287
+
288
+ ```ruby
289
+ # config/initializers/online_migrations.rb
290
+
291
+ config.background_migrations.migrations_path = "app/lib"
292
+ ```
293
+
294
+ If no value is specified, it will default to `"lib"`.
295
+
284
296
  ### Customizing the background migrations module
285
297
 
286
- `OnlineMigrations.config.background_migrations.migrations_module` can be configured to define the module in which
298
+ `config.background_migrations.migrations_module` can be configured to define the module in which
287
299
  background migrations will be placed.
288
300
 
289
301
  ```ruby
290
302
  # config/initializers/online_migrations.rb
291
303
 
292
- OnlineMigrations.config.background_migrations.migrations_module = "BackgroundMigrationsModule"
304
+ config.background_migrations.migrations_module = "BackgroundMigrationsModule"
293
305
  ```
294
306
 
295
- If no value is specified, it will default to `OnlineMigrations::BackgroundMigrations`.
307
+ If no value is specified, it will default to `"OnlineMigrations::BackgroundMigrations"`.
296
308
 
297
309
  ### Customizing the backtrace cleaner
298
310
 
299
- `OnlineMigrations.config.background_migrations.backtrace_cleaner` can be configured to specify a backtrace cleaner to use when a Background Migration errors and the backtrace is cleaned and persisted. An `ActiveSupport::BacktraceCleaner` should be used.
311
+ `config.background_migrations.backtrace_cleaner` can be configured to specify a backtrace cleaner to use when a Background Migration errors and the backtrace is cleaned and persisted. An `ActiveSupport::BacktraceCleaner` should be used.
300
312
 
301
313
  ```ruby
302
314
  # config/initializers/online_migrations.rb
@@ -304,7 +316,7 @@ If no value is specified, it will default to `OnlineMigrations::BackgroundMigrat
304
316
  cleaner = ActiveSupport::BacktraceCleaner.new
305
317
  cleaner.add_silencer { |line| line =~ /ignore_this_dir/ }
306
318
 
307
- OnlineMigrations.config.background_migrations.backtrace_cleaner = cleaner
319
+ config.background_migrations.backtrace_cleaner = cleaner
308
320
  ```
309
321
 
310
322
  If none is specified, the default `Rails.backtrace_cleaner` will be used to clean backtraces.
@@ -9,8 +9,11 @@ module OnlineMigrations
9
9
  desc "This generator creates a background migration file."
10
10
 
11
11
  def create_background_migration_file
12
+ migrations_module_file_path = migrations_module.underscore
13
+
12
14
  template_file = File.join(
13
- "lib/#{migrations_module_file_path}",
15
+ config.migrations_path,
16
+ migrations_module_file_path,
14
17
  class_path,
15
18
  "#{file_name}.rb"
16
19
  )
@@ -18,12 +21,12 @@ module OnlineMigrations
18
21
  end
19
22
 
20
23
  private
21
- def migrations_module_file_path
22
- migrations_module.underscore
24
+ def migrations_module
25
+ config.migrations_module
23
26
  end
24
27
 
25
- def migrations_module
26
- OnlineMigrations.config.background_migrations.migrations_module
28
+ def config
29
+ OnlineMigrations.config.background_migrations
27
30
  end
28
31
  end
29
32
  end
@@ -79,6 +79,9 @@ OnlineMigrations.configure do |config|
79
79
 
80
80
  # ==> Background migrations configuration
81
81
 
82
+ # The path where generated background migrations will be placed.
83
+ # config.background_migrations.migrations_path = "lib"
84
+
82
85
  # The module in which background migrations will be placed.
83
86
  # config.background_migrations.migrations_module = "OnlineMigrations::BackgroundMigrations"
84
87
 
@@ -4,6 +4,10 @@ module OnlineMigrations
4
4
  module BackgroundMigrations
5
5
  # Class representing configuration options for background migrations.
6
6
  class Config
7
+ # The path where generated background migrations will be placed
8
+ # @return [String] defaults to "lib"
9
+ attr_accessor :migrations_path
10
+
7
11
  # The module in which background migrations will be placed
8
12
  # @return [String] defaults to "OnlineMigrations::BackgroundMigrations"
9
13
  attr_accessor :migrations_module
@@ -75,6 +79,7 @@ module OnlineMigrations
75
79
  attr_accessor :error_handler
76
80
 
77
81
  def initialize
82
+ @migrations_path = "lib"
78
83
  @migrations_module = "OnlineMigrations::BackgroundMigrations"
79
84
  @batch_size = 20_000
80
85
  @sub_batch_size = 1000
@@ -28,6 +28,8 @@ module OnlineMigrations
28
28
  for_migration_name(migration_name).where("arguments = ?", arguments.to_json)
29
29
  end
30
30
 
31
+ alias_attribute :name, :migration_name
32
+
31
33
  enum status: STATUSES.index_with(&:to_s)
32
34
 
33
35
  belongs_to :parent, class_name: name, optional: true
@@ -64,6 +66,7 @@ module OnlineMigrations
64
66
  class_name = class_name.name if class_name.is_a?(Class)
65
67
  write_attribute(:migration_name, self.class.normalize_migration_name(class_name))
66
68
  end
69
+ alias name= migration_name=
67
70
 
68
71
  def completed?
69
72
  succeeded? || failed?
@@ -194,10 +197,11 @@ module OnlineMigrations
194
197
 
195
198
  on_shard do
196
199
  # rubocop:disable Lint/UnreachableLoop
197
- iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value) do |relation|
198
- min = relation.arel_table[batch_column_name].minimum
199
- max = relation.arel_table[batch_column_name].maximum
200
- batch_range = relation.pick(min, max)
200
+ iterator.each_batch(of: batch_size, column: batch_column_name, start: next_min_value) do |relation, min_value, max_value|
201
+ if max_value.nil?
202
+ max_value = relation.pick(relation.arel_table[batch_column_name].maximum)
203
+ end
204
+ batch_range = [min_value, max_value]
201
205
 
202
206
  break
203
207
  end
@@ -249,14 +253,10 @@ module OnlineMigrations
249
253
  self.min_value = self.max_value = self.rows_count = -1 # not relevant
250
254
  else
251
255
  on_shard do
252
- self.min_value ||= migration_relation.minimum(batch_column_name)
253
- self.max_value ||= migration_relation.maximum(batch_column_name)
254
-
255
- # This can be the case when run in development on empty tables
256
- if min_value.nil?
257
- # integer IDs minimum value is 1
258
- self.min_value = self.max_value = 1
259
- end
256
+ # Getting exact min/max values can be a very heavy operation
257
+ # and is not needed practically.
258
+ self.min_value ||= 1
259
+ self.max_value ||= migration_model.unscoped.maximum(batch_column_name) || self.min_value
260
260
 
261
261
  count = migration_object.count
262
262
  self.rows_count = count if count != :no_count
@@ -19,37 +19,42 @@ module OnlineMigrations
19
19
  end
20
20
 
21
21
  relation = apply_limits(self.relation, column, start, finish, order)
22
+ base_relation = relation.reselect(column).reorder(column => order)
22
23
 
23
- base_relation = relation.except(:select)
24
- .select(column)
25
- .reorder(column => order)
26
-
27
- start_row = base_relation.uncached { base_relation.first }
28
-
29
- return if !start_row
24
+ start_id = start || begin
25
+ start_row = base_relation.uncached { base_relation.first }
26
+ start_row[column] if start_row
27
+ end
30
28
 
31
- start_id = start_row[column]
32
29
  arel_table = relation.arel_table
33
30
 
34
- 0.step do |index|
31
+ while start_id
35
32
  if order == :asc
36
33
  start_cond = arel_table[column].gteq(start_id)
37
34
  else
38
35
  start_cond = arel_table[column].lteq(start_id)
39
36
  end
40
37
 
41
- stop_row = base_relation.uncached do
38
+ last_row, stop_row = base_relation.uncached do
42
39
  base_relation
43
40
  .where(start_cond)
44
- .offset(of)
45
- .first
41
+ .offset(of - 1)
42
+ .first(2)
43
+ end
44
+
45
+ if last_row.nil?
46
+ # We are at the end of the table.
47
+ last_row, stop_row = base_relation.uncached do
48
+ base_relation
49
+ .where(start_cond)
50
+ .last(2)
51
+ end
46
52
  end
47
53
 
48
54
  batch_relation = relation.where(start_cond)
49
55
 
50
56
  if stop_row
51
57
  stop_id = stop_row[column]
52
- start_id = stop_id
53
58
 
54
59
  if order == :asc
55
60
  stop_cond = arel_table[column].lt(stop_id)
@@ -64,10 +69,14 @@ module OnlineMigrations
64
69
  # efficient UPDATE queries, hence we get rid of it.
65
70
  batch_relation = batch_relation.except(:order)
66
71
 
72
+ last_id = (last_row && last_row[column]) || start_id
73
+
67
74
  # Retaining the results in the query cache would undermine the point of batching.
68
- batch_relation.uncached { yield batch_relation, index }
75
+ batch_relation.uncached { yield batch_relation, start_id, last_id }
76
+
77
+ break if stop_row.nil?
69
78
 
70
- break if !stop_row
79
+ start_id = stop_id
71
80
  end
72
81
  end
73
82
 
@@ -416,15 +416,8 @@ module OnlineMigrations
416
416
  end
417
417
  end
418
418
 
419
- if index.name.include?(from_column)
420
- name = index.name.gsub(from_column, to_column)
421
- end
422
-
423
- name = index_name(table_name, new_columns) if !name || name.length > max_identifier_length
424
-
425
419
  options = {
426
420
  unique: index.unique,
427
- name: name,
428
421
  length: index.lengths,
429
422
  order: index.orders,
430
423
  }
@@ -681,24 +681,20 @@ module OnlineMigrations
681
681
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index
682
682
  #
683
683
  def add_index(table_name, column_name, **options)
684
- algorithm = options[:algorithm]
685
-
686
- __ensure_not_in_transaction! if algorithm == :concurrently
687
-
688
- column_names = index_column_names(column_name || options[:column])
689
-
690
- index_name = options[:name]
691
- index_name ||= index_name(table_name, column_names)
684
+ __ensure_not_in_transaction! if options[:algorithm] == :concurrently
692
685
 
693
- if index_exists?(table_name, column_name, **options)
686
+ # Rewrite this with `IndexDefinition#defined_for?` when Active Record >= 7.1 is supported.
687
+ # See https://github.com/rails/rails/pull/45160.
688
+ index = indexes(table_name).find { |i| __index_defined_for?(i, column_name, **options) }
689
+ if index
694
690
  schema = __schema_for_table(table_name)
695
691
 
696
- if __index_valid?(index_name, schema: schema)
692
+ if __index_valid?(index.name, schema: schema)
697
693
  Utils.say("Index was not created because it already exists.")
698
694
  return
699
695
  else
700
696
  Utils.say("Recreating invalid index: table_name: #{table_name}, column_name: #{column_name}")
701
- remove_index(table_name, column_name, name: index_name, algorithm: algorithm)
697
+ remove_index(table_name, column_name, **options)
702
698
  end
703
699
  end
704
700
 
@@ -706,7 +702,7 @@ module OnlineMigrations
706
702
  # "CREATE INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
707
703
  # It only conflicts with constraint validations, creating/removing indexes,
708
704
  # and some other "ALTER TABLE"s.
709
- super(table_name, column_name, **options.merge(name: index_name))
705
+ super
710
706
  else
711
707
  OnlineMigrations.deprecator.warn(<<~MSG)
712
708
  Running `add_index` without a statement timeout is deprecated.
@@ -716,7 +712,7 @@ module OnlineMigrations
716
712
  MSG
717
713
 
718
714
  disable_statement_timeout do
719
- super(table_name, column_name, **options.merge(name: index_name))
715
+ super
720
716
  end
721
717
  end
722
718
  end
@@ -944,6 +940,17 @@ module OnlineMigrations
944
940
  end
945
941
  end
946
942
 
943
+ # Will not be needed for Active Record >= 7.1
944
+ def __index_defined_for?(index, columns = nil, name: nil, unique: nil, valid: nil, include: nil, nulls_not_distinct: nil, **options)
945
+ columns = options[:column] if columns.blank?
946
+ (columns.nil? || Array(index.columns) == Array(columns).map(&:to_s)) &&
947
+ (name.nil? || index.name == name.to_s) &&
948
+ (unique.nil? || index.unique == unique) &&
949
+ (valid.nil? || index.valid == valid) &&
950
+ (include.nil? || Array(index.include) == Array(include).map(&:to_s)) &&
951
+ (nulls_not_distinct.nil? || index.nulls_not_distinct == nulls_not_distinct)
952
+ end
953
+
947
954
  def __not_null_constraint_exists?(table_name, column_name, name: nil)
948
955
  name ||= __not_null_constraint_name(table_name, column_name)
949
956
  __check_constraint_exists?(table_name, name: name)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: online_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-17 00:00:00.000000000 Z
11
+ date: 2024-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord