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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +155 -150
- data/docs/background_migrations.md +43 -10
- data/docs/configuring.md +23 -18
- data/lib/generators/online_migrations/install_generator.rb +3 -7
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +18 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +12 -3
- data/lib/generators/online_migrations/templates/migration.rb.tt +8 -3
- data/lib/generators/online_migrations/upgrade_generator.rb +33 -0
- data/lib/online_migrations/background_migrations/application_record.rb +13 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +1 -1
- data/lib/online_migrations/background_migrations/copy_column.rb +11 -19
- data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +2 -20
- data/lib/online_migrations/background_migrations/migration.rb +123 -34
- data/lib/online_migrations/background_migrations/migration_helpers.rb +0 -4
- data/lib/online_migrations/background_migrations/migration_job.rb +15 -12
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -2
- data/lib/online_migrations/background_migrations/migration_runner.rb +56 -11
- data/lib/online_migrations/background_migrations/reset_counters.rb +3 -9
- data/lib/online_migrations/background_migrations/scheduler.rb +5 -15
- data/lib/online_migrations/change_column_type_helpers.rb +71 -86
- data/lib/online_migrations/command_checker.rb +50 -46
- data/lib/online_migrations/config.rb +19 -15
- data/lib/online_migrations/copy_trigger.rb +15 -10
- data/lib/online_migrations/error_messages.rb +13 -25
- data/lib/online_migrations/foreign_keys_collector.rb +2 -2
- data/lib/online_migrations/indexes_collector.rb +3 -3
- data/lib/online_migrations/lock_retrier.rb +4 -9
- data/lib/online_migrations/schema_cache.rb +0 -6
- data/lib/online_migrations/schema_dumper.rb +21 -0
- data/lib/online_migrations/schema_statements.rb +80 -256
- data/lib/online_migrations/utils.rb +36 -55
- data/lib/online_migrations/verbose_sql_logs.rb +3 -2
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +9 -6
- metadata +9 -7
- data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
- 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 <
|
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
|
-
|
16
|
-
scope :
|
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 =
|
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(
|
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(
|
37
|
+
scope :except_succeeded, -> { where.not(status: :succeeded) }
|
39
38
|
scope :attempts_exceeded, -> { where("attempts >= max_attempts") }
|
40
39
|
|
41
|
-
enum status: STATUSES.
|
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
|
16
|
-
|
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
|
-
|
56
|
-
|
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
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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 =
|
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
|
-
|
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.
|
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
|
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
|
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
|
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
|
-
|
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?
|
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.
|
101
|
+
conversions = columns_and_types.to_h do |(column_name, _new_type)|
|
99
102
|
[column_name, __change_type_column(column_name)]
|
100
|
-
end
|
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 =
|
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.
|
249
|
+
conversions = column_names.to_h do |column_name|
|
243
250
|
[column_name.to_s, __change_type_column(column_name)]
|
244
|
-
end
|
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 =
|
248
|
-
column =
|
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
|
-
|
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
|
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.
|
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
|
-
|
320
|
-
|
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
|
-
|
327
|
-
|
328
|
-
|
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
|
-
|
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
|
-
|
337
|
-
|
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.
|
366
|
-
|
367
|
-
end
|
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,
|
396
|
-
CopyTrigger.on_table(table_name, connection: self).create(
|
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
|
-
|
419
|
-
|
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 =
|
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
|
-
|
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
|
-
|
479
|
-
|
480
|
-
|
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
|
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(
|
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(
|
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(
|
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(
|
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
|