online_migrations 0.22.0 → 0.23.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: 493a3f1a84eb30974c533f81ca5baf8b739d69d4ec6e30810d891284791632f1
4
- data.tar.gz: 9dae1370ea3073146888d3757314bac6c8e968c5e35a2c2be345f3617f610881
3
+ metadata.gz: 1052d2cc58898bc561b5e755daccc137d7d6e1d35afa3af5968c2b655ebe5805
4
+ data.tar.gz: ad72d601c36ea9192f192fb8941d30a2cfbcf0ed87e0717fb5d4ebf47b20f169
5
5
  SHA512:
6
- metadata.gz: d40cae89f1be37a97567d7bb05d1ded17479ce89b9783ed0c606d6bdfdec2d940d251de4de66557af774fc88e1262ae492539fe3fae5f4cb105d88526e7315d3
7
- data.tar.gz: 26316851d405210a67576ea0ae35ba0419574832aacc43a2f0ddd00a8f22223f0d1ed73ff1635f6f9aa4168877c48aebb56d0f93f05472b531d80f3fc19881e9
6
+ metadata.gz: 697ac61bcb41e00744cfa87965910a3e1d68eaa571ad5c6dc3b3d8a4d14deb16ae0cbc0d9771d8d57668be8529f200736a14c1769d7d8cf6c64fc083060ff74c
7
+ data.tar.gz: 7b4e383f7628ba5cc74c2bb86f4460890b70f3052738f0f20db3571ee770e22c49bc548f5c0ef3fd47caa456a84eb4174f031742869612ee3b27505faf49ecec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.23.0 (2025-01-13)
4
+
5
+ - Prevent multiple instances of schedulers from being running simultaneously
6
+
7
+ - Reduce default batch sizes for background data migrations
8
+
9
+ `batch_size` was 20_000, now 1_000; `sub_batch_size` was 1_000, now 100
10
+
11
+ - Remove deprecated code
12
+ - Drop support for PostgreSQL < 12
13
+ - Drop support for Ruby < 3.0 and Rails < 7.0
14
+
3
15
  ## 0.22.0 (2025-01-03)
4
16
 
5
17
  - Make background data migrations scheduler run a single migration at a time
data/README.md CHANGED
@@ -16,11 +16,11 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Ruby 2.7+
20
- - Rails 6.1+
21
- - PostgreSQL 9.6+
19
+ - Ruby 3.0+
20
+ - Rails 7.0+
21
+ - PostgreSQL 12+
22
22
 
23
- For older Ruby and Rails versions you can use '< 0.11' version of this gem.
23
+ For older Ruby and Rails versions you can use older versions of this gem.
24
24
 
25
25
  **Note**: Since some migration helpers use database `VIEW`s to implement their logic, it is recommended to use `structure.sql` schema format, or otherwise add some gem (like [scenic](https://github.com/scenic-views/scenic)) to be able to dump them into the `schema.rb`.
26
26
 
@@ -4,11 +4,13 @@ OnlineMigrations.configure do |config|
4
4
  # Configure the migration version starting after which checks are performed.
5
5
  # config.start_after = <%= start_after %>
6
6
 
7
- # Configure statement timeout used for migrations.
7
+ # Configure statement timeout for migrations (in seconds).
8
+ # Note: Background data migrations use application specific timeouts and
9
+ # background schema migrations use their custom timeouts.
8
10
  config.statement_timeout = 1.hour
9
11
 
10
12
  # Set the version of the production database so the right checks are run in development.
11
- # config.target_version = 10
13
+ # config.target_version = 17
12
14
 
13
15
  # Configure whether to perform checks when migrating down.
14
16
  config.check_down = false
@@ -19,7 +21,7 @@ OnlineMigrations.configure do |config|
19
21
 
20
22
  # Maximum allowed lock timeout value (in seconds).
21
23
  # If set lock timeout is greater than this value, the migration will fail.
22
- # config.lock_timeout_limit = 10.seconds
24
+ config.lock_timeout_limit = 10.seconds
23
25
 
24
26
  # Configure list of tables with permanently small number of records.
25
27
  # This tables are usually tables like "settings", "prices", "plans" etc.
@@ -28,10 +30,10 @@ OnlineMigrations.configure do |config|
28
30
 
29
31
  # Analyze tables after indexes are added.
30
32
  # Outdated statistics can sometimes hurt performance.
31
- # config.auto_analyze = true
33
+ config.auto_analyze = false
32
34
 
33
35
  # Alphabetize table columns when dumping the schema.
34
- # config.alphabetize_schema = true
36
+ config.alphabetize_schema = false
35
37
 
36
38
  # Disable specific checks.
37
39
  # For the list of available checks look at the `error_messages.rb` file inside
@@ -92,29 +94,29 @@ OnlineMigrations.configure do |config|
92
94
 
93
95
  # ==> Background data migrations configuration
94
96
  # The path where generated background migrations will be placed.
95
- # config.background_migrations.migrations_path = "lib"
97
+ config.background_migrations.migrations_path = "lib"
96
98
 
97
99
  # The module in which background migrations will be placed.
98
- # config.background_migrations.migrations_module = "OnlineMigrations::BackgroundMigrations"
100
+ config.background_migrations.migrations_module = "OnlineMigrations::BackgroundMigrations"
99
101
 
100
102
  # The number of rows to process in a single background migration run.
101
- # config.background_migrations.batch_size = 20_000
103
+ config.background_migrations.batch_size = 1_000
102
104
 
103
105
  # The smaller batches size that the batches will be divided into.
104
- # config.background_migrations.sub_batch_size = 1000
106
+ config.background_migrations.sub_batch_size = 100
105
107
 
106
108
  # The pause interval between each background migration job's execution (in seconds).
107
- # config.background_migrations.batch_pause = 0.seconds
109
+ config.background_migrations.batch_pause = 0.seconds
108
110
 
109
111
  # The number of milliseconds to sleep between each sub_batch execution.
110
- # config.background_migrations.sub_batch_pause_ms = 100
112
+ config.background_migrations.sub_batch_pause_ms = 100
111
113
 
112
114
  # Maximum number of batch run attempts.
113
115
  # When attempts are exhausted, the individual batch is marked as failed.
114
- # config.background_migrations.batch_max_attempts = 5
116
+ config.background_migrations.batch_max_attempts = 5
115
117
 
116
118
  # The number of seconds that must pass before the running job is considered stuck.
117
- # config.background_migrations.stuck_jobs_timeout = 1.hour
119
+ config.background_migrations.stuck_jobs_timeout = 1.hour
118
120
 
119
121
  # The callback to perform when an error occurs in the migration job.
120
122
  # config.background_migrations.error_handler = ->(error, errored_job) do
@@ -125,10 +127,10 @@ OnlineMigrations.configure do |config|
125
127
 
126
128
  # ==> Background schema migrations configuration
127
129
  # When attempts are exhausted, the failing migration stops to be retried.
128
- # config.background_schema_migrations.max_attempts = 5
130
+ config.background_schema_migrations.max_attempts = 5
129
131
 
130
132
  # Statement timeout value used when running background schema migration.
131
- # config.background_schema_migrations.statement_timeout = 1.hour
133
+ config.background_schema_migrations.statement_timeout = 1.hour
132
134
 
133
135
  # The callback to perform when an error occurs during the background schema migration.
134
136
  # config.background_schema_migrations.error_handler = ->(error, errored_migration) do
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ module OnlineMigrations
6
+ # @private
7
+ class AdvisoryLock
8
+ attr_reader :name, :connection
9
+
10
+ def initialize(name:, connection: ApplicationRecord.connection)
11
+ @name = name
12
+ @connection = connection
13
+ end
14
+
15
+ def try_lock
16
+ locked = connection.select_value("SELECT pg_try_advisory_lock(#{lock_key})")
17
+ Utils.to_bool(locked)
18
+ end
19
+
20
+ def unlock
21
+ connection.select_value("SELECT pg_advisory_unlock(#{lock_key})")
22
+ end
23
+
24
+ # Runs the given block if an advisory lock is able to be acquired.
25
+ def try_with_lock
26
+ if try_lock
27
+ begin
28
+ yield
29
+ ensure
30
+ unlock
31
+ end
32
+ end
33
+ end
34
+
35
+ def active?
36
+ objid = lock_key & 0xffffffff
37
+ classid = lock_key >> 32
38
+
39
+ active = connection.select_value(<<~SQL)
40
+ SELECT granted
41
+ FROM pg_locks
42
+ WHERE locktype = 'advisory'
43
+ AND pid = pg_backend_pid()
44
+ AND mode = 'ExclusiveLock'
45
+ AND classid = #{classid}
46
+ AND objid = #{objid}
47
+ SQL
48
+
49
+ Utils.to_bool(active)
50
+ end
51
+
52
+ private
53
+ SALT = 936723412
54
+
55
+ def lock_key
56
+ name_hash = Zlib.crc32(name)
57
+ SALT * name_hash
58
+ end
59
+ end
60
+ end
@@ -13,12 +13,12 @@ module OnlineMigrations
13
13
  attr_accessor :migrations_module
14
14
 
15
15
  # The number of rows to process in a single background migration run
16
- # @return [Integer] defaults to 20_000
16
+ # @return [Integer] defaults to 1_000
17
17
  #
18
18
  attr_accessor :batch_size
19
19
 
20
20
  # The smaller batches size that the batches will be divided into
21
- # @return [Integer] defaults to 1000
21
+ # @return [Integer] defaults to 100
22
22
  #
23
23
  attr_accessor :sub_batch_size
24
24
 
@@ -39,36 +39,12 @@ module OnlineMigrations
39
39
  #
40
40
  attr_accessor :batch_max_attempts
41
41
 
42
- def throttler
43
- OnlineMigrations.deprecator.warn(<<~MSG)
44
- `config.background_migrations.throttler` is deprecated and will be removed.
45
- Use `config.throttler` instead.
46
- MSG
47
- OnlineMigrations.config.throttler
48
- end
49
-
50
42
  # The number of seconds that must pass before the running job is considered stuck
51
43
  #
52
44
  # @return [Integer] defaults to 1 hour
53
45
  #
54
46
  attr_accessor :stuck_jobs_timeout
55
47
 
56
- def backtrace_cleaner
57
- OnlineMigrations.deprecator.warn(<<~MSG)
58
- `config.background_migrations.backtrace_cleaner` is deprecated and will be removed.
59
- Use `config.backtrace_cleaner` instead.
60
- MSG
61
- OnlineMigrations.config.backtrace_cleaner
62
- end
63
-
64
- def backtrace_cleaner=(value)
65
- OnlineMigrations.deprecator.warn(<<~MSG)
66
- `config.background_migrations.backtrace_cleaner=` is deprecated and will be removed.
67
- Use `config.backtrace_cleaner=` instead.
68
- MSG
69
- OnlineMigrations.config.backtrace_cleaner = value
70
- end
71
-
72
48
  # The callback to perform when an error occurs in the migration job.
73
49
  #
74
50
  # @example
@@ -85,22 +61,14 @@ module OnlineMigrations
85
61
  def initialize
86
62
  @migrations_path = "lib"
87
63
  @migrations_module = "OnlineMigrations::BackgroundMigrations"
88
- @batch_size = 20_000
89
- @sub_batch_size = 1000
64
+ @batch_size = 1_000
65
+ @sub_batch_size = 100
90
66
  @batch_pause = 0.seconds
91
67
  @sub_batch_pause_ms = 100
92
68
  @batch_max_attempts = 5
93
69
  @stuck_jobs_timeout = 1.hour
94
70
  @error_handler = ->(error, errored_job) {}
95
71
  end
96
-
97
- def throttler=(value)
98
- OnlineMigrations.deprecator.warn(<<~MSG)
99
- `config.background_migrations.throttler=` is deprecated and will be removed.
100
- Use `config.throttler=` instead.
101
- MSG
102
- OnlineMigrations.config.throttler = value
103
- end
104
72
  end
105
73
  end
106
74
  end
@@ -32,12 +32,7 @@ module OnlineMigrations
32
32
 
33
33
  alias_attribute :name, :migration_name
34
34
 
35
- # Avoid deprecation warnings.
36
- if Utils.ar_version >= 7
37
- enum :status, STATUSES.index_with(&:to_s)
38
- else
39
- enum status: STATUSES.index_with(&:to_s)
40
- end
35
+ enum :status, STATUSES.index_with(&:to_s)
41
36
 
42
37
  belongs_to :parent, class_name: name, optional: true, inverse_of: :children
43
38
  has_many :children, class_name: name, foreign_key: :parent_id, dependent: :delete_all, inverse_of: :parent
@@ -335,8 +335,8 @@ module OnlineMigrations
335
335
  # defaults to `SELECT MIN(batch_column_name)`
336
336
  # @option options [Integer] :max_value Value in the column the batching will end at,
337
337
  # defaults to `SELECT MAX(batch_column_name)`
338
- # @option options [Integer] :batch_size (20_000) Number of rows to process in a single background migration run
339
- # @option options [Integer] :sub_batch_size (1000) Smaller batches size that the batches will be divided into
338
+ # @option options [Integer] :batch_size (1_000) Number of rows to process in a single background migration run
339
+ # @option options [Integer] :sub_batch_size (100) Smaller batches size that the batches will be divided into
340
340
  # @option options [Integer] :batch_pause (0) Pause interval between each background migration job's execution (in seconds)
341
341
  # @option options [Integer] :sub_batch_pause_ms (100) Number of milliseconds to sleep between each sub_batch execution
342
342
  # @option options [Integer] :batch_max_attempts (5) Maximum number of batch run attempts
@@ -38,12 +38,7 @@ module OnlineMigrations
38
38
  scope :except_succeeded, -> { where.not(status: :succeeded) }
39
39
  scope :attempts_exceeded, -> { where("attempts >= max_attempts") }
40
40
 
41
- # Avoid deprecation warnings.
42
- if Utils.ar_version >= 7
43
- enum :status, STATUSES.index_with(&:to_s)
44
- else
45
- enum status: STATUSES.index_with(&:to_s)
46
- end
41
+ enum :status, STATUSES.index_with(&:to_s)
47
42
 
48
43
  delegate :migration_name, :migration_class, :migration_object, :migration_relation, :batch_column_name,
49
44
  :arguments, :batch_pause, to: :migration
@@ -27,9 +27,18 @@ module OnlineMigrations
27
27
 
28
28
  if runnable_migration
29
29
  runner = MigrationRunner.new(runnable_migration)
30
- runner.run_migration_job
30
+
31
+ try_with_lock do
32
+ runner.run_migration_job
33
+ end
31
34
  end
32
35
  end
36
+
37
+ private
38
+ def try_with_lock(&block)
39
+ lock = AdvisoryLock.new(name: "online_migrations_data_scheduler")
40
+ lock.try_with_lock(&block)
41
+ end
33
42
  end
34
43
  end
35
44
  end
@@ -49,12 +49,7 @@ module OnlineMigrations
49
49
 
50
50
  alias_attribute :name, :migration_name
51
51
 
52
- # Avoid deprecation warnings.
53
- if Utils.ar_version >= 7
54
- enum :status, STATUSES.index_with(&:to_s)
55
- else
56
- enum status: STATUSES.index_with(&:to_s)
57
- end
52
+ enum :status, STATUSES.index_with(&:to_s)
58
53
 
59
54
  belongs_to :parent, class_name: name, optional: true, inverse_of: :children
60
55
  has_many :children, class_name: name, foreign_key: :parent_id, inverse_of: :parent
@@ -23,7 +23,10 @@ module OnlineMigrations
23
23
  migration = find_migration
24
24
  if migration
25
25
  runner = MigrationRunner.new(migration)
26
- runner.run
26
+
27
+ try_with_lock do
28
+ runner.run
29
+ end
27
30
  end
28
31
  end
29
32
 
@@ -40,6 +43,11 @@ module OnlineMigrations
40
43
  end
41
44
  end
42
45
  end
46
+
47
+ def try_with_lock(&block)
48
+ lock = AdvisoryLock.new(name: "online_migrations_schema_scheduler")
49
+ lock.try_with_lock(&block)
50
+ end
43
51
  end
44
52
  end
45
53
  end
@@ -118,24 +118,19 @@ module OnlineMigrations
118
118
  type_cast_functions[column_name] = type_cast_function if type_cast_function
119
119
  tmp_column_name = conversions[column_name]
120
120
 
121
- if database_version >= 11_00_00
122
- if primary_key(table_name) == column_name.to_s && old_col.type == :integer
123
- # For PG < 11 and Primary Key conversions, setting a column as the PK
124
- # converts even check constraints to NOT NULL column constraints
125
- # and forces an inline re-verification of the whole table.
126
- # To avoid this, we instead set it to `NOT NULL DEFAULT 0` and we'll
127
- # copy the correct values when backfilling.
128
- add_column(table_name, tmp_column_name, new_type,
129
- **old_col_options, **column_options, default: old_col.default || 0, null: false)
130
- else
131
- if !old_col.default.nil?
132
- old_col_options = old_col_options.merge(default: old_col.default, null: old_col.null)
133
- end
134
- add_column(table_name, tmp_column_name, new_type, **old_col_options, **column_options)
135
- end
121
+ if primary_key(table_name) == column_name.to_s && old_col.type == :integer
122
+ # For PG < 11 and Primary Key conversions, setting a column as the PK
123
+ # converts even check constraints to NOT NULL column constraints
124
+ # and forces an inline re-verification of the whole table.
125
+ # To avoid this, we instead set it to `NOT NULL DEFAULT 0` and we'll
126
+ # copy the correct values when backfilling.
127
+ add_column(table_name, tmp_column_name, new_type,
128
+ **old_col_options, **column_options, default: old_col.default || 0, null: false)
136
129
  else
130
+ if !old_col.default.nil?
131
+ old_col_options = old_col_options.merge(default: old_col.default, null: old_col.null)
132
+ end
137
133
  add_column(table_name, tmp_column_name, new_type, **old_col_options, **column_options)
138
- change_column_default(table_name, tmp_column_name, old_col.default) if !old_col.default.nil?
139
134
  end
140
135
  end
141
136
 
@@ -264,7 +259,7 @@ module OnlineMigrations
264
259
 
265
260
  # At this point we are sure there are no NULLs in this column
266
261
  transaction do
267
- __set_not_null(table_name, tmp_column_name)
262
+ change_column_null(table_name, tmp_column_name, false)
268
263
  remove_not_null_constraint(table_name, tmp_column_name)
269
264
  end
270
265
  end
@@ -494,29 +489,6 @@ module OnlineMigrations
494
489
  end
495
490
  end
496
491
 
497
- def __set_not_null(table_name, column_name)
498
- # For PG >= 12 we can "promote" CHECK constraint to NOT NULL constraint:
499
- # https://github.com/postgres/postgres/commit/bbb96c3704c041d139181c6601e5bc770e045d26
500
- if database_version >= 12_00_00
501
- execute(<<~SQL)
502
- ALTER TABLE #{quote_table_name(table_name)}
503
- ALTER #{quote_column_name(column_name)}
504
- SET NOT NULL
505
- SQL
506
- else
507
- # For older versions we can set attribute as NOT NULL directly
508
- # through PG internal tables.
509
- # In-depth analysis of implications of this was made, so this approach
510
- # is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
511
- execute(<<~SQL)
512
- UPDATE pg_catalog.pg_attribute
513
- SET attnotnull = true
514
- WHERE attrelid = #{quote(table_name)}::regclass
515
- AND attname = #{quote(column_name)}
516
- SQL
517
- end
518
- end
519
-
520
492
  def __rename_constraint(table_name, old_name, new_name)
521
493
  execute(<<~SQL)
522
494
  ALTER TABLE #{quote_table_name(table_name)}
@@ -59,7 +59,6 @@ module OnlineMigrations
59
59
  short_primary_key_type: "using-primary-key-with-short-integer-type",
60
60
  drop_table_multiple_foreign_keys: "removing-a-table-with-multiple-foreign-keys",
61
61
  rename_table: "renaming-a-table",
62
- add_column_with_default_null: "adding-a-column-with-a-default-value",
63
62
  add_column_with_default: "adding-a-column-with-a-default-value",
64
63
  add_column_generated_stored: "adding-a-stored-generated-column",
65
64
  add_column_json: "adding-a-json-column",
@@ -69,7 +68,6 @@ module OnlineMigrations
69
68
  change_column_null: "setting-not-null-on-an-existing-column",
70
69
  remove_column: "removing-a-column",
71
70
  add_timestamps_with_default: "adding-a-column-with-a-default-value",
72
- add_hash_index: "hash-indexes",
73
71
  add_reference: "adding-a-reference",
74
72
  add_index: "adding-an-index-non-concurrently",
75
73
  replace_index: "replacing-an-index",
@@ -89,8 +87,8 @@ module OnlineMigrations
89
87
  adapter = connection.adapter_name
90
88
  case adapter
91
89
  when /postg/i
92
- if postgresql_version < Gem::Version.new("9.6")
93
- raise "#{adapter} < 9.6 is not supported"
90
+ if postgresql_version < Gem::Version.new("12")
91
+ raise "#{adapter} < 12 is not supported"
94
92
  end
95
93
  else
96
94
  raise "#{adapter} is not supported"
@@ -101,9 +99,12 @@ module OnlineMigrations
101
99
 
102
100
  def set_statement_timeout
103
101
  if !defined?(@statement_timeout_set)
104
- if (statement_timeout = OnlineMigrations.config.statement_timeout)
105
- # TODO: inline this method call after deprecated `disable_statement_timeout` method removal.
106
- connection.__set_statement_timeout(statement_timeout)
102
+ if (timeout = OnlineMigrations.config.statement_timeout)
103
+ # use ceil to prevent no timeout for values under 1 ms
104
+ timeout = (timeout * 1000).ceil if !timeout.is_a?(String)
105
+
106
+ # Can't use `execute`, because command checker marks it as a dangerous operation.
107
+ connection.select_value("SET statement_timeout TO #{connection.quote(timeout)}")
107
108
  end
108
109
  @statement_timeout_set = true
109
110
  end
@@ -181,11 +182,7 @@ module OnlineMigrations
181
182
  # But I think this check is enough for now.
182
183
  raise_error :short_primary_key_type if short_primary_key_type?(options)
183
184
 
184
- if block
185
- collect_foreign_keys(&block)
186
- check_for_hash_indexes(&block) if postgresql_version < Gem::Version.new("10")
187
- end
188
-
185
+ collect_foreign_keys(&block) if block
189
186
  @new_tables << table_name.to_s
190
187
  end
191
188
 
@@ -230,18 +227,11 @@ module OnlineMigrations
230
227
  @new_columns << [table_name.to_s, column_name.to_s]
231
228
 
232
229
  if !new_or_small_table?(table_name)
233
- if options.key?(:default) &&
234
- (postgresql_version < Gem::Version.new("11") || (!default.nil? && (volatile_default = Utils.volatile_default?(connection, type, default))))
235
-
236
- if default.nil?
237
- raise_error :add_column_with_default_null,
238
- code: command_str(:add_column, table_name, column_name, type, options.except(:default))
239
- else
240
- raise_error :add_column_with_default,
241
- code: command_str(:add_column_with_default, table_name, column_name, type, options),
242
- not_null: options[:null] == false,
243
- volatile_default: volatile_default
244
- end
230
+ if options.key?(:default) && !default.nil? && (volatile_default = Utils.volatile_default?(connection, type, default))
231
+ raise_error :add_column_with_default,
232
+ code: command_str(:add_column_with_default, table_name, column_name, type, options),
233
+ not_null: options[:null] == false,
234
+ volatile_default: volatile_default
245
235
  end
246
236
 
247
237
  if type == :virtual && options[:stored]
@@ -280,10 +270,7 @@ module OnlineMigrations
280
270
  table_name: table_name,
281
271
  column_name: column_name,
282
272
  new_column: new_column,
283
- model: table_name.to_s.classify,
284
- partial_writes: Utils.ar_partial_writes?,
285
- partial_writes_setting: Utils.ar_partial_writes_setting,
286
- enumerate_columns_in_select_statements: Utils.ar_enumerate_columns_in_select_statements
273
+ model: table_name.to_s.classify
287
274
  end
288
275
  end
289
276
 
@@ -360,9 +347,7 @@ module OnlineMigrations
360
347
 
361
348
  [:datetime, :timestamp, :timestamptz].include?(existing_type) &&
362
349
  precision >= existing_precision &&
363
- (type == existing_type ||
364
- (postgresql_version >= Gem::Version.new("12") &&
365
- connection.select_value("SHOW timezone") == "UTC"))
350
+ (type == existing_type || connection.select_value("SHOW timezone") == "UTC")
366
351
  when :interval
367
352
  precision = options[:precision] || options[:limit] || 6
368
353
  existing_precision = existing_column.precision || existing_column.limit || 6
@@ -394,22 +379,18 @@ module OnlineMigrations
394
379
  end
395
380
 
396
381
  def change_column_default(table_name, column_name, _default_or_changes)
397
- if Utils.ar_partial_writes? && !new_column?(table_name, column_name)
398
- raise_error :change_column_default,
399
- config: Utils.ar_partial_writes_setting
382
+ if ActiveRecord::Base.partial_inserts && !new_column?(table_name, column_name)
383
+ raise_error :change_column_default
400
384
  end
401
385
  end
402
386
 
403
387
  def change_column_null(table_name, column_name, allow_null, default = nil, **)
404
388
  if !allow_null && !new_or_small_table?(table_name)
405
- safe = false
406
389
  # In PostgreSQL 12+ you can add a check constraint to the table
407
390
  # and then "promote" it to NOT NULL for the column.
408
- if postgresql_version >= Gem::Version.new("12")
409
- safe = check_constraints(table_name).any? do |c|
410
- c["def"] == "CHECK ((#{column_name} IS NOT NULL))" ||
411
- c["def"] == "CHECK ((#{connection.quote_column_name(column_name)} IS NOT NULL))"
412
- end
391
+ safe = check_constraints(table_name).any? do |c|
392
+ c["def"] == "CHECK ((#{column_name} IS NOT NULL))" ||
393
+ c["def"] == "CHECK ((#{connection.quote_column_name(column_name)} IS NOT NULL))"
413
394
  end
414
395
 
415
396
  if !safe
@@ -417,17 +398,13 @@ module OnlineMigrations
417
398
  vars = {
418
399
  add_constraint_code: command_str(:add_not_null_constraint, table_name, column_name, name: constraint_name, validate: false),
419
400
  validate_constraint_code: command_str(:validate_not_null_constraint, table_name, column_name, name: constraint_name),
420
- remove_constraint_code: nil,
421
401
  table_name: table_name,
422
402
  column_name: column_name,
423
403
  default: default,
404
+ remove_constraint_code: command_str(:remove_check_constraint, table_name, name: constraint_name),
405
+ change_column_null_code: command_str(:change_column_null, table_name, column_name, false),
424
406
  }
425
407
 
426
- if postgresql_version >= Gem::Version.new("12")
427
- vars[:remove_constraint_code] = command_str(:remove_check_constraint, table_name, name: constraint_name)
428
- vars[:change_column_null_code] = command_str(:change_column_null, table_name, column_name, false)
429
- end
430
-
431
408
  raise_error :change_column_null, **vars
432
409
  end
433
410
  end
@@ -470,15 +447,13 @@ module OnlineMigrations
470
447
  @new_columns << [table_name.to_s, "created_at"]
471
448
  @new_columns << [table_name.to_s, "updated_at"]
472
449
 
473
- volatile_default = false
474
450
  if !new_or_small_table?(table_name) && !options[:default].nil? &&
475
- (postgresql_version < Gem::Version.new("11") || (volatile_default = Utils.volatile_default?(connection, :datetime, options[:default])))
451
+ Utils.volatile_default?(connection, :datetime, options[:default])
476
452
 
477
453
  raise_error :add_timestamps_with_default,
478
454
  code: [command_str(:add_column_with_default, table_name, :created_at, :datetime, options),
479
455
  command_str(:add_column_with_default, table_name, :updated_at, :datetime, options)].join("\n "),
480
- not_null: options[:null] == false,
481
- volatile_default: volatile_default
456
+ not_null: options[:null] == false
482
457
  end
483
458
  end
484
459
 
@@ -486,10 +461,6 @@ module OnlineMigrations
486
461
  # Always added by default in 5.0+
487
462
  index = options.fetch(:index, true)
488
463
 
489
- if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
490
- raise_error :add_hash_index
491
- end
492
-
493
464
  concurrently_set = index.is_a?(Hash) && index[:algorithm] == :concurrently
494
465
  bad_index = index && !concurrently_set
495
466
 
@@ -522,13 +493,6 @@ module OnlineMigrations
522
493
  alias add_belongs_to add_reference
523
494
 
524
495
  def add_reference_concurrently(table_name, ref_name, **options)
525
- # Always added by default in 5.0+
526
- index = options.fetch(:index, true)
527
-
528
- if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
529
- raise_error :add_hash_index
530
- end
531
-
532
496
  foreign_key = options.fetch(:foreign_key, false)
533
497
 
534
498
  if foreign_key
@@ -546,10 +510,6 @@ module OnlineMigrations
546
510
  end
547
511
 
548
512
  def add_index(table_name, column_name, **options)
549
- if options[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
550
- raise_error :add_hash_index
551
- end
552
-
553
513
  if !new_or_small_table?(table_name)
554
514
  if options[:algorithm] != :concurrently
555
515
  raise_error :add_index,
@@ -702,19 +662,6 @@ module OnlineMigrations
702
662
  @foreign_key_tables |= collector.referenced_tables
703
663
  end
704
664
 
705
- def check_for_hash_indexes(&block)
706
- indexes = collect_indexes(&block)
707
- if indexes.any? { |index| index.using == "hash" }
708
- raise_error :add_hash_index
709
- end
710
- end
711
-
712
- def collect_indexes(&block)
713
- collector = IndexesCollector.new
714
- collector.collect(&block)
715
- collector.indexes
716
- end
717
-
718
665
  def new_or_small_table?(table_name)
719
666
  new_table?(table_name) || small_table?(table_name)
720
667
  end
@@ -736,7 +683,7 @@ module OnlineMigrations
736
683
  if Utils.developer_env? && (target_version = OnlineMigrations.config.target_version)
737
684
  target_version.to_s
738
685
  else
739
- database_version = connection.select_value("SHOW server_version_num").to_i
686
+ database_version = connection.database_version
740
687
  major = database_version / 10000
741
688
  if database_version >= 100000
742
689
  minor = database_version % 10000
@@ -128,12 +128,12 @@ migration_helpers provides a safer approach to do this:
128
128
  <%= column_name.to_s.inspect %> => <%= new_column.to_s.inspect %>
129
129
  }
130
130
  }
131
- <% unless partial_writes %>
131
+ <% unless ActiveRecord::Base.partial_inserts %>
132
132
 
133
133
  NOTE: You also need to temporarily enable partial writes (is disabled by default in Active Record >= 7)
134
134
  until the process of column rename is fully done.
135
135
  # config/application.rb
136
- config.active_record.<%= partial_writes_setting %> = true
136
+ config.active_record.partial_inserts = true
137
137
  <% end %>
138
138
 
139
139
  2. Deploy
@@ -148,7 +148,7 @@ It will use a combination of a VIEW and column aliasing to work with both column
148
148
  end
149
149
 
150
150
  4. Replace usages of the old column with a new column in the codebase
151
- <% if enumerate_columns_in_select_statements %>
151
+ <% if ActiveRecord::Base.enumerate_columns_in_select_statements %>
152
152
  5. Ignore old column
153
153
 
154
154
  self.ignored_columns += [:<%= column_name %>]
@@ -247,7 +247,7 @@ during writes works automatically). For most column type changes, this does not
247
247
  to be inserted when changing the default value of a column.
248
248
  Disable partial writes in config/application.rb:
249
249
 
250
- config.active_record.<%= config %> = false",
250
+ config.active_record.partial_inserts = false",
251
251
 
252
252
  change_column_null:
253
253
  "Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
@@ -317,7 +317,7 @@ A safer approach is to:
317
317
  <% end %>",
318
318
 
319
319
  add_timestamps_with_default:
320
- "Adding timestamps columns with non-null defaults blocks reads and writes while the entire table is rewritten.
320
+ "Adding timestamp columns with volatile defaults blocks reads and writes while the entire table is rewritten.
321
321
 
322
322
  A safer approach is to, for both timestamps columns:
323
323
  1. add the column without a default value
@@ -327,7 +327,6 @@ A safer approach is to, for both timestamps columns:
327
327
  4. add the NOT NULL constraint
328
328
  <% end %>
329
329
 
330
- <% unless volatile_default %>
331
330
  add_column_with_default takes care of all this steps:
332
331
 
333
332
  class <%= migration_name %> < <%= migration_parent %>
@@ -336,8 +335,7 @@ class <%= migration_name %> < <%= migration_parent %>
336
335
  def change
337
336
  <%= code %>
338
337
  end
339
- end
340
- <% end %>",
338
+ end",
341
339
 
342
340
  add_reference:
343
341
  "<% if bad_foreign_key %>
@@ -356,13 +354,6 @@ class <%= migration_name %> < <%= migration_parent %>
356
354
  end
357
355
  end",
358
356
 
359
- add_hash_index:
360
- "Hash index operations are not WAL-logged, so hash indexes might need to be rebuilt with REINDEX
361
- after a database crash if there were unwritten changes. Also, changes to hash indexes are not replicated
362
- over streaming or file-based replication after the initial base backup, so they give wrong answers
363
- to queries that subsequently use them. For these reasons, hash index use is discouraged.
364
- Use B-tree indexes instead.",
365
-
366
357
  add_index:
367
358
  "Adding an index non-concurrently blocks writes. Instead, use:
368
359
 
@@ -12,7 +12,7 @@ module OnlineMigrations
12
12
  # @param column_name [String, Symbol]
13
13
  # @param value value for the column. It is typically a literal. To perform a computed
14
14
  # update, an Arel literal can be used instead
15
- # @option options [Integer] :batch_size (1000) size of the batch
15
+ # @option options [Integer] :batch_size (1_000) size of the batch
16
16
  # @option options [String, Symbol] :batch_column_name (primary key) option is for tables without primary key, in this
17
17
  # case another unique integer column can be used. Example: `:user_id`
18
18
  # @option options [Proc, Boolean] :progress (false) whether to show progress while running.
@@ -439,9 +439,7 @@ module OnlineMigrations
439
439
  def add_column_with_default(table_name, column_name, type, **options)
440
440
  default = options.fetch(:default)
441
441
 
442
- if database_version >= 11_00_00 && !Utils.volatile_default?(self, type, default)
443
- add_column(table_name, column_name, type, **options)
444
- else
442
+ if Utils.volatile_default?(self, type, default)
445
443
  __ensure_not_in_transaction!
446
444
 
447
445
  batch_options = options.extract!(:batch_size, :batch_column_name, :progress, :pause_ms)
@@ -465,12 +463,12 @@ module OnlineMigrations
465
463
  add_not_null_constraint(table_name, column_name, validate: false)
466
464
  validate_not_null_constraint(table_name, column_name)
467
465
 
468
- if database_version >= 12_00_00
469
- # In PostgreSQL 12+ it is safe to "promote" a CHECK constraint to `NOT NULL` for the column
470
- change_column_null(table_name, column_name, false)
471
- remove_not_null_constraint(table_name, column_name)
472
- end
466
+ # In PostgreSQL 12+ it is safe to "promote" a CHECK constraint to `NOT NULL` for the column
467
+ change_column_null(table_name, column_name, false)
468
+ remove_not_null_constraint(table_name, column_name)
473
469
  end
470
+ else
471
+ add_column(table_name, column_name, type, **options)
474
472
  end
475
473
  end
476
474
 
@@ -729,23 +727,10 @@ module OnlineMigrations
729
727
  end
730
728
  end
731
729
 
732
- if OnlineMigrations.config.statement_timeout
733
- # "CREATE INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
734
- # It only conflicts with constraint validations, creating/removing indexes,
735
- # and some other "ALTER TABLE"s.
736
- super
737
- else
738
- OnlineMigrations.deprecator.warn(<<~MSG)
739
- Running `add_index` without a statement timeout is deprecated.
740
- Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
741
- or the default database statement timeout will be used.
742
- Example, `config.statement_timeout = 1.hour`.
743
- MSG
744
-
745
- disable_statement_timeout do
746
- super
747
- end
748
- end
730
+ # "CREATE INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
731
+ # It only conflicts with constraint validations, creating/removing indexes,
732
+ # and some other "ALTER TABLE"s.
733
+ super
749
734
  end
750
735
 
751
736
  # Extends default method to be idempotent.
@@ -770,23 +755,10 @@ module OnlineMigrations
770
755
  end
771
756
 
772
757
  if index_exists
773
- if OnlineMigrations.config.statement_timeout
774
- # "DROP INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
775
- # It only conflicts with constraint validations, other creating/removing indexes,
776
- # and some "ALTER TABLE"s.
777
- super
778
- else
779
- OnlineMigrations.deprecator.warn(<<~MSG)
780
- Running `remove_index` without a statement timeout is deprecated.
781
- Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
782
- or the default database statement timeout will be used.
783
- Example, `config.statement_timeout = 1.hour`.
784
- MSG
785
-
786
- disable_statement_timeout do
787
- super
788
- end
789
- end
758
+ # "DROP INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
759
+ # It only conflicts with constraint validations, other creating/removing indexes,
760
+ # and some "ALTER TABLE"s.
761
+ super
790
762
  else
791
763
  Utils.say("Index was not removed because it does not exist.")
792
764
  end
@@ -835,23 +807,10 @@ module OnlineMigrations
835
807
  # Skip costly operation if already validated.
836
808
  return if foreign_key.validated?
837
809
 
838
- if OnlineMigrations.config.statement_timeout
839
- # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
840
- # It only conflicts with other validations, creating/removing indexes,
841
- # and some other "ALTER TABLE"s.
842
- super
843
- else
844
- OnlineMigrations.deprecator.warn(<<~MSG)
845
- Running `validate_foreign_key` without a statement timeout is deprecated.
846
- Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
847
- or the default database statement timeout will be used.
848
- Example, `config.statement_timeout = 1.hour`.
849
- MSG
850
-
851
- disable_statement_timeout do
852
- super
853
- end
854
- end
810
+ # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
811
+ # It only conflicts with other validations, creating/removing indexes,
812
+ # and some other "ALTER TABLE"s.
813
+ super
855
814
  end
856
815
 
857
816
  # Extends default method to be idempotent.
@@ -894,23 +853,10 @@ module OnlineMigrations
894
853
  # Skip costly operation if already validated.
895
854
  return if check_constraint.validated?
896
855
 
897
- if OnlineMigrations.config.statement_timeout
898
- # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
899
- # It only conflicts with other validations, creating/removing indexes,
900
- # and some other "ALTER TABLE"s.
901
- super
902
- else
903
- OnlineMigrations.deprecator.warn(<<~MSG)
904
- Running `validate_check_constraint` without a statement timeout is deprecated.
905
- Configure an explicit statement timeout in the initializer file via `config.statement_timeout`
906
- or the default database statement timeout will be used.
907
- Example, `config.statement_timeout = 1.hour`.
908
- MSG
909
-
910
- disable_statement_timeout do
911
- super
912
- end
913
- end
856
+ # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
857
+ # It only conflicts with other validations, creating/removing indexes,
858
+ # and some other "ALTER TABLE"s.
859
+ super
914
860
  end
915
861
 
916
862
  # Extends default method to be idempotent
@@ -965,28 +911,6 @@ module OnlineMigrations
965
911
  end
966
912
  end
967
913
 
968
- # @private
969
- def disable_statement_timeout
970
- OnlineMigrations.deprecator.warn(<<~MSG)
971
- `disable_statement_timeout` is deprecated and will be removed. Configure an explicit
972
- statement timeout in the initializer file via `config.statement_timeout` or the default
973
- database statement timeout will be used. Example, `config.statement_timeout = 1.hour`.
974
- MSG
975
-
976
- prev_value = select_value("SHOW statement_timeout")
977
- __set_statement_timeout(0)
978
- yield
979
- ensure
980
- __set_statement_timeout(prev_value)
981
- end
982
-
983
- # @private
984
- def __set_statement_timeout(timeout)
985
- # use ceil to prevent no timeout for values under 1 ms
986
- timeout = (timeout.to_f * 1000).ceil if !timeout.is_a?(String)
987
- execute("SET statement_timeout TO #{quote(timeout)}")
988
- end
989
-
990
914
  # @private
991
915
  # Executes the block with a retry mechanism that alters the `lock_timeout`
992
916
  # and sleep time between attempts.
@@ -86,26 +86,6 @@ module OnlineMigrations
86
86
  "#{short_name}#{hashed_identifier}"
87
87
  end
88
88
 
89
- def ar_partial_writes?
90
- ActiveRecord::Base.public_send(ar_partial_writes_setting)
91
- end
92
-
93
- def ar_partial_writes_setting
94
- if Utils.ar_version >= 7.0
95
- "partial_inserts"
96
- else
97
- "partial_writes"
98
- end
99
- end
100
-
101
- def ar_enumerate_columns_in_select_statements
102
- if ar_version >= 7
103
- ActiveRecord::Base.enumerate_columns_in_select_statements
104
- else
105
- false
106
- end
107
- end
108
-
109
89
  # Returns estimated rows count for a table.
110
90
  # https://www.citusdata.com/blog/2016/10/12/count-performance/
111
91
  def estimated_count(connection, table_name)
@@ -6,7 +6,7 @@ module OnlineMigrations
6
6
  class << self
7
7
  def enable
8
8
  @activerecord_logger_was = ActiveRecord::Base.logger
9
- @verbose_query_logs_was = verbose_query_logs
9
+ @verbose_query_logs_was = ActiveRecord.verbose_query_logs
10
10
  return if @activerecord_logger_was.nil?
11
11
 
12
12
  stdout_logger = ActiveSupport::Logger.new($stdout)
@@ -23,30 +23,13 @@ module OnlineMigrations
23
23
  end
24
24
 
25
25
  ActiveRecord::Base.logger = combined_logger
26
- set_verbose_query_logs(false)
26
+ ActiveRecord.verbose_query_logs = false
27
27
  end
28
28
 
29
29
  def disable
30
30
  ActiveRecord::Base.logger = @activerecord_logger_was
31
- set_verbose_query_logs(@verbose_query_logs_was)
31
+ ActiveRecord.verbose_query_logs = @verbose_query_logs_was
32
32
  end
33
-
34
- private
35
- def verbose_query_logs
36
- if Utils.ar_version >= 7.0
37
- ActiveRecord.verbose_query_logs
38
- else
39
- ActiveRecord::Base.verbose_query_logs
40
- end
41
- end
42
-
43
- def set_verbose_query_logs(value) # rubocop:disable Naming/AccessorMethodName
44
- if Utils.ar_version >= 7.0
45
- ActiveRecord.verbose_query_logs = value
46
- else
47
- ActiveRecord::Base.verbose_query_logs = value
48
- end
49
- end
50
33
  end
51
34
  end
52
35
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.22.0"
4
+ VERSION = "0.23.0"
5
5
  end
@@ -23,12 +23,12 @@ module OnlineMigrations
23
23
 
24
24
  extend ActiveSupport::Autoload
25
25
 
26
+ autoload :AdvisoryLock
26
27
  autoload :ApplicationRecord
27
28
  autoload :BatchIterator
28
29
  autoload :VerboseSqlLogs
29
30
  autoload :ForeignKeysCollector
30
31
  autoload :IndexDefinition
31
- autoload :IndexesCollector
32
32
  autoload :CommandChecker
33
33
  autoload :BackgroundMigration
34
34
 
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.22.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-01-03 00:00:00.000000000 Z
11
+ date: 2025-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '6.1'
19
+ version: '7.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '6.1'
26
+ version: '7.0'
27
27
  description:
28
28
  email:
29
29
  - fatkodima123@gmail.com
@@ -48,6 +48,7 @@ files:
48
48
  - lib/generators/online_migrations/templates/migration.rb.tt
49
49
  - lib/generators/online_migrations/upgrade_generator.rb
50
50
  - lib/online_migrations.rb
51
+ - lib/online_migrations/advisory_lock.rb
51
52
  - lib/online_migrations/application_record.rb
52
53
  - lib/online_migrations/background_migration.rb
53
54
  - lib/online_migrations/background_migrations/backfill_column.rb
@@ -82,7 +83,6 @@ files:
82
83
  - lib/online_migrations/error_messages.rb
83
84
  - lib/online_migrations/foreign_keys_collector.rb
84
85
  - lib/online_migrations/index_definition.rb
85
- - lib/online_migrations/indexes_collector.rb
86
86
  - lib/online_migrations/lock_retrier.rb
87
87
  - lib/online_migrations/migration.rb
88
88
  - lib/online_migrations/migrator.rb
@@ -107,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '2.7'
110
+ version: '3.0'
111
111
  required_rubygems_version: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - ">="
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module OnlineMigrations
4
- # @private
5
- class IndexesCollector
6
- COLUMN_TYPES = [:bigint, :binary, :boolean, :date, :datetime, :decimal,
7
- :float, :integer, :json, :string, :text, :time, :timestamp, :virtual]
8
-
9
- attr_reader :indexes
10
-
11
- def initialize
12
- @indexes = []
13
- end
14
-
15
- def collect
16
- yield self
17
- end
18
-
19
- def index(_column_name, **options)
20
- @indexes << IndexDefinition.new(using: options[:using].to_s)
21
- end
22
-
23
- def references(*_ref_names, **options)
24
- index = options.fetch(:index, true)
25
-
26
- if index
27
- using = index.is_a?(Hash) ? index[:using].to_s : nil
28
- @indexes << IndexDefinition.new(using: using)
29
- end
30
- end
31
- alias belongs_to references
32
-
33
- def method_missing(method_name, *_args, **options)
34
- # Check for type-based methods, where we can also specify an index:
35
- # t.string :email, index: true
36
- if COLUMN_TYPES.include?(method_name)
37
- index = options.fetch(:index, false)
38
-
39
- if index
40
- using = index.is_a?(Hash) ? index[:using].to_s : nil
41
- @indexes << IndexDefinition.new(using: using)
42
- end
43
- end
44
- end
45
- end
46
- end