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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module OnlineMigrations
4
4
  module BackgroundMigrations
5
- class MigrationJob < ActiveRecord::Base
5
+ class MigrationJob < ApplicationRecord
6
6
  STATUSES = [
7
7
  :enqueued,
8
8
  :running,
@@ -12,12 +12,11 @@ module OnlineMigrations
12
12
 
13
13
  self.table_name = :background_migration_jobs
14
14
 
15
- # For Active Record <= 4.2 needs to fully specify enum values
16
- scope :active, -> { where(status: [statuses[:enqueued], statuses[:running]]) }
17
- scope :completed, -> { where(status: [statuses[:failed], statuses[:succeeded]]) }
15
+ scope :active, -> { where(status: [:enqueued, :running]) }
16
+ scope :completed, -> { where(status: [:failed, :succeeded]) }
18
17
  scope :stuck, -> do
19
- timeout = ::OnlineMigrations.config.background_migrations.stuck_jobs_timeout
20
- active.where("updated_at <= ?", timeout.ago)
18
+ timeout = OnlineMigrations.config.background_migrations.stuck_jobs_timeout
19
+ active.where("updated_at <= ?", timeout.seconds.ago)
21
20
  end
22
21
 
23
22
  scope :retriable, -> do
@@ -26,7 +25,7 @@ module OnlineMigrations
26
25
  stuck_sql = connection.unprepared_statement { stuck.to_sql }
27
26
  failed_retriable_sql = connection.unprepared_statement { failed_retriable.to_sql }
28
27
 
29
- from(Arel.sql(<<-SQL.strip_heredoc))
28
+ from(Arel.sql(<<~SQL))
30
29
  (
31
30
  (#{failed_retriable_sql})
32
31
  UNION
@@ -35,19 +34,16 @@ module OnlineMigrations
35
34
  SQL
36
35
  end
37
36
 
38
- scope :except_succeeded, -> { where("status != ?", statuses[:succeeded]) }
37
+ scope :except_succeeded, -> { where.not(status: :succeeded) }
39
38
  scope :attempts_exceeded, -> { where("attempts >= max_attempts") }
40
39
 
41
- enum status: STATUSES.map { |status| [status, status.to_s] }.to_h
40
+ enum status: STATUSES.index_with(&:to_s)
42
41
 
43
42
  delegate :migration_class, :migration_object, :migration_relation, :batch_column_name,
44
43
  :arguments, :batch_pause, to: :migration
45
44
 
46
45
  belongs_to :migration
47
46
 
48
- # For Active Record 5.0+ this is validated by default from belongs_to
49
- validates :migration, presence: true
50
-
51
47
  validates :min_value, :max_value, presence: true, numericality: { greater_than: 0 }
52
48
  validate :values_in_migration_range, if: :min_value?
53
49
  validate :validate_values_order, if: :min_value?
@@ -56,6 +52,13 @@ module OnlineMigrations
56
52
 
57
53
  before_create :copy_settings_from_migration
58
54
 
55
+ # Whether the job is considered stuck (is running for some configured time).
56
+ #
57
+ def stuck?
58
+ timeout = OnlineMigrations.config.background_migrations.stuck_jobs_timeout
59
+ running? && updated_at <= timeout.seconds.ago
60
+ end
61
+
59
62
  # Mark this job as ready to be processed again.
60
63
  #
61
64
  # This is used when retrying failed jobs.
@@ -6,7 +6,7 @@ module OnlineMigrations
6
6
  class MigrationJobRunner
7
7
  attr_reader :migration_job
8
8
 
9
- delegate :attempts, :migration_relation, :migration_object, :sub_batch_size,
9
+ delegate :migration, :attempts, :migration_relation, :migration_object, :sub_batch_size,
10
10
  :batch_column_name, :min_value, :max_value, :pause_ms, to: :migration_job
11
11
 
12
12
  def initialize(migration_job)
@@ -30,7 +30,7 @@ module OnlineMigrations
30
30
  )
31
31
 
32
32
  ActiveSupport::Notifications.instrument("process_batch.background_migrations", job_payload) do
33
- run_batch
33
+ migration.on_shard { run_batch }
34
34
  end
35
35
 
36
36
  migration_job.update!(status: :succeeded, finished_at: Time.current)
@@ -12,8 +12,13 @@ module OnlineMigrations
12
12
 
13
13
  # Runs one background migration job.
14
14
  def run_migration_job
15
- migration.running! if migration.enqueued?
16
- migration_payload = { background_migration: migration }
15
+ raise "Should not be called on a composite (with sharding) migration" if migration.composite?
16
+
17
+ if migration.enqueued?
18
+ migration.running!
19
+ migration.parent.running! if migration.parent && migration.parent.enqueued?
20
+ end
21
+ migration_payload = notifications_payload(migration)
17
22
 
18
23
  if !migration.migration_jobs.exists?
19
24
  ActiveSupport::Notifications.instrument("started.background_migrations", migration_payload)
@@ -37,6 +42,8 @@ module OnlineMigrations
37
42
  end
38
43
 
39
44
  ActiveSupport::Notifications.instrument("completed.background_migrations", migration_payload)
45
+
46
+ complete_parent_if_needed(migration) if migration.parent.present?
40
47
  end
41
48
 
42
49
  next_migration_job
@@ -52,8 +59,15 @@ module OnlineMigrations
52
59
 
53
60
  migration.running!
54
61
 
55
- while migration.running?
56
- run_migration_job
62
+ if migration.composite?
63
+ migration.children.each do |child_migration|
64
+ runner = self.class.new(child_migration)
65
+ runner.run_all_migration_jobs
66
+ end
67
+ else
68
+ while migration.running?
69
+ run_migration_job
70
+ end
57
71
  end
58
72
  end
59
73
 
@@ -64,13 +78,20 @@ module OnlineMigrations
64
78
  def finish
65
79
  return if migration.completed?
66
80
 
67
- # Mark is as finishing to avoid being picked up
68
- # by the background migrations scheduler.
69
- migration.finishing!
70
- migration.reset_failed_jobs_attempts
71
-
72
- while migration.finishing?
73
- run_migration_job
81
+ if migration.composite?
82
+ migration.children.each do |child_migration|
83
+ runner = self.class.new(child_migration)
84
+ runner.finish
85
+ end
86
+ else
87
+ # Mark is as finishing to avoid being picked up
88
+ # by the background migrations scheduler.
89
+ migration.finishing!
90
+ migration.reset_failed_jobs_attempts
91
+
92
+ while migration.finishing?
93
+ run_migration_job
94
+ end
74
95
  end
75
96
  end
76
97
 
@@ -95,6 +116,30 @@ module OnlineMigrations
95
116
  max_value: max_value
96
117
  )
97
118
  end
119
+
120
+ def complete_parent_if_needed(migration)
121
+ parent = migration.parent
122
+ completed = false
123
+
124
+ parent.with_lock do
125
+ children = parent.children.select(:status)
126
+ if children.all?(&:succeeded?)
127
+ parent.succeeded!
128
+ completed = true
129
+ elsif children.any?(&:failed?)
130
+ parent.failed!
131
+ completed = true
132
+ end
133
+ end
134
+
135
+ if completed
136
+ ActiveSupport::Notifications.instrument("completed.background_migrations", notifications_payload(migration))
137
+ end
138
+ end
139
+
140
+ def notifications_payload(migration)
141
+ { background_migration: migration }
142
+ end
98
143
  end
99
144
  end
100
145
  end
@@ -26,7 +26,7 @@ module OnlineMigrations
26
26
  counter_name = reflection.counter_cache_column
27
27
 
28
28
  quoted_association_table = connection.quote_table_name(has_many_association.table_name)
29
- count_subquery = <<-SQL.strip_heredoc
29
+ count_subquery = <<~SQL
30
30
  SELECT COUNT(*)
31
31
  FROM #{quoted_association_table}
32
32
  WHERE #{quoted_association_table}.#{connection.quote_column_name(foreign_key)} =
@@ -41,8 +41,7 @@ module OnlineMigrations
41
41
  names = Array.wrap(names)
42
42
  options = names.extract_options!
43
43
  touch_updates = touch_attributes_with_time(*names, **options)
44
- # In Active Record 4.2 sanitize_sql_for_assignment is protected
45
- updates << model.send(:sanitize_sql_for_assignment, touch_updates)
44
+ updates << model.sanitize_sql_for_assignment(touch_updates)
46
45
  end
47
46
 
48
47
  relation.update_all(updates.join(", "))
@@ -64,11 +63,6 @@ module OnlineMigrations
64
63
 
65
64
  has_many_association = has_many.find do |association|
66
65
  counter_cache_column = association.counter_cache_column
67
-
68
- # Active Record <= 4.2 is able to return only explicitly provided `counter_cache` column.
69
- if !counter_cache_column && Utils.ar_version <= 4.2
70
- counter_cache_column = "#{association.name}_count"
71
- end
72
66
  counter_cache_column && counter_cache_column.to_sym == counter_association.to_sym
73
67
  end
74
68
 
@@ -86,7 +80,7 @@ module OnlineMigrations
86
80
  def touch_attributes_with_time(*names, time: nil)
87
81
  attribute_names = timestamp_attributes_for_update & model.column_names
88
82
  attribute_names |= names.map(&:to_s)
89
- attribute_names.map { |attribute_name| [attribute_name, time || Time.current] }.to_h
83
+ attribute_names.index_with(time || Time.current)
90
84
  end
91
85
 
92
86
  def timestamp_attributes_for_update
@@ -4,15 +4,14 @@ module OnlineMigrations
4
4
  module BackgroundMigrations
5
5
  # Class responsible for scheduling background migrations.
6
6
  # It selects runnable background migrations and runs them one step (one batch) at a time.
7
- # A migration is considered runnable if it is not completed and time the interval between
7
+ # A migration is considered runnable if it is not completed and the time interval between
8
8
  # successive runs has passed.
9
- # Scheduler ensures (via advisory locks) that at most one background migration at a time is running per database.
10
9
  #
11
- # Scheduler should be run via some kind of periodical means, for example, cron.
10
+ # Scheduler should be configured to run periodically, for example, via cron.
12
11
  # @example Run via whenever
13
12
  # # add this to schedule.rb
14
13
  # every 1.minute do
15
- # runner "OnlineMigrations::BackgroundMigrations::Scheduler.run"
14
+ # runner "OnlineMigrations.run_background_migrations"
16
15
  # end
17
16
  #
18
17
  class Scheduler
@@ -22,24 +21,15 @@ module OnlineMigrations
22
21
 
23
22
  # Runs Scheduler
24
23
  def run
25
- active_migrations = Migration.active.queue_order
24
+ active_migrations = Migration.runnable.active.queue_order
26
25
  runnable_migrations = active_migrations.select(&:interval_elapsed?)
27
26
 
28
27
  runnable_migrations.each do |migration|
29
- connection = migration.migration_relation.connection
30
-
31
- with_exclusive_lock(connection) do
32
- run_migration_job(migration)
33
- end
28
+ run_migration_job(migration)
34
29
  end
35
30
  end
36
31
 
37
32
  private
38
- def with_exclusive_lock(connection, &block)
39
- lock = AdvisoryLock.new(name: "online_migrations_scheduler", connection: connection)
40
- lock.with_lock(&block)
41
- end
42
-
43
33
  def run_migration_job(migration)
44
34
  runner = MigrationRunner.new(migration)
45
35
  runner.run_migration_job
@@ -72,6 +72,9 @@ module OnlineMigrations
72
72
  #
73
73
  # @example With additional column options
74
74
  # initialize_column_type_change(:users, :name, :string, limit: 64)
75
+ # @example With type casting
76
+ # initialize_column_type_change(:users, :settings, :jsonb, type_cast_function: "jsonb")
77
+ # initialize_column_type_change(:users, :company_id, :integer, type_cast_function: Arel.sql("company_id::integer"))
75
78
  #
76
79
  def initialize_column_type_change(table_name, column_name, new_type, **options)
77
80
  initialize_columns_type_change(table_name, [[column_name, new_type]], column_name => options)
@@ -91,13 +94,13 @@ module OnlineMigrations
91
94
  # @see #initialize_column_type_change
92
95
  #
93
96
  def initialize_columns_type_change(table_name, columns_and_types, **options)
94
- if !columns_and_types.is_a?(Array) || !columns_and_types.all? { |e| e.is_a?(Array) }
97
+ if !columns_and_types.is_a?(Array) || !columns_and_types.all?(Array)
95
98
  raise ArgumentError, "columns_and_types must be an array of arrays"
96
99
  end
97
100
 
98
- conversions = columns_and_types.map do |(column_name, _new_type)|
101
+ conversions = columns_and_types.to_h do |(column_name, _new_type)|
99
102
  [column_name, __change_type_column(column_name)]
100
- end.to_h
103
+ end
101
104
 
102
105
  if (extra_keys = (options.keys - conversions.keys)).any?
103
106
  raise ArgumentError, "Options has unknown keys: #{extra_keys.map(&:inspect).join(', ')}. " \
@@ -105,10 +108,14 @@ module OnlineMigrations
105
108
  end
106
109
 
107
110
  transaction do
111
+ type_cast_functions = {}.with_indifferent_access
112
+
108
113
  columns_and_types.each do |(column_name, new_type)|
109
- old_col = __column_for(table_name, column_name)
114
+ old_col = column_for(table_name, column_name)
110
115
  old_col_options = __options_from_column(old_col, [:collation, :comment])
111
116
  column_options = options[column_name] || {}
117
+ type_cast_function = column_options.delete(:type_cast_function)
118
+ type_cast_functions[column_name] = type_cast_function if type_cast_function
112
119
  tmp_column_name = conversions[column_name]
113
120
 
114
121
  if raw_connection.server_version >= 11_00_00
@@ -132,7 +139,7 @@ module OnlineMigrations
132
139
  end
133
140
  end
134
141
 
135
- __create_copy_triggers(table_name, conversions.keys, conversions.values)
142
+ __create_copy_triggers(table_name, conversions.keys, conversions.values, type_cast_functions: type_cast_functions)
136
143
  end
137
144
  end
138
145
 
@@ -239,13 +246,15 @@ module OnlineMigrations
239
246
  def finalize_columns_type_change(table_name, *column_names)
240
247
  __ensure_not_in_transaction!
241
248
 
242
- conversions = column_names.map do |column_name|
249
+ conversions = column_names.to_h do |column_name|
243
250
  [column_name.to_s, __change_type_column(column_name)]
244
- end.to_h
251
+ end
252
+
253
+ primary_key = primary_key(table_name)
245
254
 
246
255
  conversions.each do |column_name, tmp_column_name|
247
- old_column = __column_for(table_name, column_name)
248
- column = __column_for(table_name, tmp_column_name)
256
+ old_column = column_for(table_name, column_name)
257
+ column = column_for(table_name, tmp_column_name)
249
258
 
250
259
  # We already set default and NOT NULL for to-be-PK columns
251
260
  # for PG >= 11, so can skip this case
@@ -264,7 +273,12 @@ module OnlineMigrations
264
273
  __copy_foreign_keys(table_name, column_name, tmp_column_name)
265
274
  __copy_check_constraints(table_name, column_name, tmp_column_name)
266
275
 
267
- if primary_key(table_name) == column_name
276
+ # Exclusion constraints were added in https://github.com/rails/rails/pull/40224.
277
+ if Utils.ar_version >= 7.1
278
+ __copy_exclusion_constraints(table_name, column_name, tmp_column_name)
279
+ end
280
+
281
+ if column_name == primary_key
268
282
  __finalize_primary_key_type_change(table_name, column_name, column_names)
269
283
  end
270
284
  end
@@ -274,7 +288,7 @@ module OnlineMigrations
274
288
  # already were swapped and which were not.
275
289
  transaction do
276
290
  conversions
277
- .reject { |column_name, _tmp_column_name| column_name == primary_key(table_name) }
291
+ .reject { |column_name, _tmp_column_name| column_name == primary_key }
278
292
  .each do |column_name, tmp_column_name|
279
293
  swap_column_names(table_name, column_name, tmp_column_name)
280
294
  end
@@ -302,40 +316,27 @@ module OnlineMigrations
302
316
  def revert_finalize_columns_type_change(table_name, *column_names)
303
317
  __ensure_not_in_transaction!
304
318
 
305
- conversions = column_names.map do |column_name|
319
+ conversions = column_names.to_h do |column_name|
306
320
  [column_name.to_s, __change_type_column(column_name)]
307
- end.to_h
308
-
309
- transaction do
310
- conversions
311
- .reject { |column_name, _tmp_column_name| column_name == primary_key(table_name) }
312
- .each do |column_name, tmp_column_name|
313
- swap_column_names(table_name, column_name, tmp_column_name)
314
- end
315
-
316
- __reset_trigger_function(table_name, column_names)
317
321
  end
318
322
 
319
- conversions.each do |column_name, tmp_column_name|
320
- indexes(table_name).each do |index|
321
- if index.columns.include?(tmp_column_name)
322
- remove_index(table_name, tmp_column_name, algorithm: :concurrently)
323
- end
324
- end
323
+ primary_key = primary_key(table_name)
324
+ primary_key_conversion = conversions.delete(primary_key)
325
325
 
326
- foreign_keys(table_name).each do |fk|
327
- if fk.column == tmp_column_name
328
- remove_foreign_key(table_name, column: tmp_column_name)
326
+ # No need to remove indexes, foreign keys etc, because it can take a significant amount
327
+ # of time and will be automatically removed if decided to remove the column itself.
328
+ if conversions.any?
329
+ transaction do
330
+ conversions.each do |column_name, tmp_column_name|
331
+ swap_column_names(table_name, column_name, tmp_column_name)
329
332
  end
330
- end
331
333
 
332
- __check_constraints_for(table_name, tmp_column_name).each do |constraint|
333
- remove_check_constraint(table_name, name: constraint.constraint_name)
334
+ __reset_trigger_function(table_name, column_names)
334
335
  end
336
+ end
335
337
 
336
- if primary_key(table_name) == column_name
337
- __finalize_primary_key_type_change(table_name, column_name, column_names)
338
- end
338
+ if primary_key_conversion
339
+ __finalize_primary_key_type_change(table_name, primary_key, column_names)
339
340
  end
340
341
  end
341
342
 
@@ -362,9 +363,9 @@ module OnlineMigrations
362
363
  # @see #cleanup_column_type_change
363
364
  #
364
365
  def cleanup_columns_type_change(table_name, *column_names)
365
- conversions = column_names.map do |column_name|
366
- [column_name, __change_type_column(column_name)]
367
- end.to_h
366
+ conversions = column_names.index_with do |column_name|
367
+ __change_type_column(column_name)
368
+ end
368
369
 
369
370
  transaction do
370
371
  __remove_copy_triggers(table_name, conversions.keys, conversions.values)
@@ -392,8 +393,8 @@ module OnlineMigrations
392
393
  CopyTrigger.on_table(table_name, connection: self).name(from_column, to_column)
393
394
  end
394
395
 
395
- def __create_copy_triggers(table_name, from_column, to_column)
396
- CopyTrigger.on_table(table_name, connection: self).create(from_column, to_column)
396
+ def __create_copy_triggers(table_name, from_columns, to_columns, type_cast_functions: nil)
397
+ CopyTrigger.on_table(table_name, connection: self).create(from_columns, to_columns, type_cast_functions: type_cast_functions)
397
398
  end
398
399
 
399
400
  def __remove_copy_triggers(table_name, from_column, to_column)
@@ -415,13 +416,11 @@ module OnlineMigrations
415
416
  end
416
417
  end
417
418
 
418
- # This is necessary as we can't properly rename indexes such as "taggings_idx".
419
- if !index.name.include?(from_column)
420
- raise "The index #{index.name} can not be copied as it does not " \
421
- "mention the old column. You have to rename this index manually first."
419
+ if index.name.include?(from_column)
420
+ name = index.name.gsub(from_column, to_column)
422
421
  end
423
422
 
424
- name = index.name.gsub(from_column, to_column)
423
+ name = index_name(table_name, new_columns) if !name || name.length > max_identifier_length
425
424
 
426
425
  options = {
427
426
  unique: index.unique,
@@ -433,8 +432,7 @@ module OnlineMigrations
433
432
  options[:using] = index.using if index.using
434
433
  options[:where] = index.where if index.where
435
434
 
436
- # Opclasses were added in 5.2
437
- if Utils.ar_version >= 5.2 && !index.opclasses.blank?
435
+ if index.opclasses.present?
438
436
  opclasses = index.opclasses.dup
439
437
 
440
438
  # Copy the operator classes for the old column (if any) to the new column.
@@ -475,23 +473,39 @@ module OnlineMigrations
475
473
  end
476
474
 
477
475
  def __copy_check_constraints(table_name, from_column, to_column)
478
- __check_constraints_for(table_name, from_column).each do |check|
479
- expression = check["constraint_def"][/CHECK \({2}(.+)\){2}/, 1]
480
- new_expression = expression.gsub(from_column.to_s, to_column.to_s)
476
+ check_constraints = check_constraints(table_name).select { |c| c.expression.include?(from_column) }
477
+
478
+ check_constraints.each do |check|
479
+ new_expression = check.expression.gsub(from_column, to_column)
481
480
 
482
481
  add_check_constraint(table_name, new_expression, validate: false)
483
482
 
484
- if check["valid"]
483
+ if check.validated?
485
484
  validate_check_constraint(table_name, expression: new_expression)
486
485
  end
487
486
  end
488
487
  end
489
488
 
489
+ def __copy_exclusion_constraints(table_name, from_column, to_column)
490
+ exclusion_constraints = exclusion_constraints(table_name).select { |c| c.expression.include?(from_column) }
491
+
492
+ exclusion_constraints.each do |constraint|
493
+ new_expression = constraint.expression.gsub(from_column, to_column)
494
+ add_exclusion_constraint(
495
+ table_name,
496
+ new_expression,
497
+ using: constraint.using,
498
+ where: constraint.where,
499
+ deferrable: constraint.deferrable
500
+ )
501
+ end
502
+ end
503
+
490
504
  def __set_not_null(table_name, column_name)
491
505
  # For PG >= 12 we can "promote" CHECK constraint to NOT NULL constraint:
492
506
  # https://github.com/postgres/postgres/commit/bbb96c3704c041d139181c6601e5bc770e045d26
493
507
  if raw_connection.server_version >= 12_00_00
494
- execute(<<-SQL.strip_heredoc)
508
+ execute(<<~SQL)
495
509
  ALTER TABLE #{quote_table_name(table_name)}
496
510
  ALTER #{quote_column_name(column_name)}
497
511
  SET NOT NULL
@@ -501,7 +515,7 @@ module OnlineMigrations
501
515
  # through PG internal tables.
502
516
  # In-depth analysis of implications of this was made, so this approach
503
517
  # is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
504
- execute(<<-SQL.strip_heredoc)
518
+ execute(<<~SQL)
505
519
  UPDATE pg_catalog.pg_attribute
506
520
  SET attnotnull = true
507
521
  WHERE attrelid = #{quote(table_name)}::regclass
@@ -510,37 +524,8 @@ module OnlineMigrations
510
524
  end
511
525
  end
512
526
 
513
- def __check_constraints_for(table_name, column_name)
514
- __check_constraints(table_name).select { |c| c["column_name"] == column_name }
515
- end
516
-
517
- def __check_constraints(table_name)
518
- schema = __schema_for_table(table_name)
519
-
520
- check_sql = <<-SQL.strip_heredoc
521
- SELECT
522
- ccu.column_name as column_name,
523
- con.conname as constraint_name,
524
- pg_get_constraintdef(con.oid) as constraint_def,
525
- con.convalidated AS valid
526
- FROM pg_catalog.pg_constraint con
527
- INNER JOIN pg_catalog.pg_class rel
528
- ON rel.oid = con.conrelid
529
- INNER JOIN pg_catalog.pg_namespace nsp
530
- ON nsp.oid = con.connamespace
531
- INNER JOIN information_schema.constraint_column_usage ccu
532
- ON con.conname = ccu.constraint_name
533
- AND rel.relname = ccu.table_name
534
- WHERE rel.relname = #{quote(table_name)}
535
- AND con.contype = 'c'
536
- AND nsp.nspname = #{schema}
537
- SQL
538
-
539
- select_all(check_sql)
540
- end
541
-
542
527
  def __rename_constraint(table_name, old_name, new_name)
543
- execute(<<-SQL.strip_heredoc)
528
+ execute(<<~SQL)
544
529
  ALTER TABLE #{quote_table_name(table_name)}
545
530
  RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}
546
531
  SQL
@@ -612,7 +597,7 @@ module OnlineMigrations
612
597
  def __referencing_table_names(table_name)
613
598
  schema = __schema_for_table(table_name)
614
599
 
615
- select_values(<<-SQL.strip_heredoc)
600
+ select_values(<<~SQL)
616
601
  SELECT DISTINCT con.conrelid::regclass::text AS conrelname
617
602
  FROM pg_catalog.pg_constraint con
618
603
  INNER JOIN pg_catalog.pg_namespace nsp