online_migrations 0.1.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +112 -0
  3. data/.gitignore +10 -0
  4. data/.rubocop.yml +113 -0
  5. data/.yardopts +1 -0
  6. data/BACKGROUND_MIGRATIONS.md +288 -0
  7. data/CHANGELOG.md +5 -0
  8. data/Gemfile +27 -0
  9. data/Gemfile.lock +108 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +1067 -0
  12. data/Rakefile +23 -0
  13. data/gemfiles/activerecord_42.gemfile +6 -0
  14. data/gemfiles/activerecord_50.gemfile +5 -0
  15. data/gemfiles/activerecord_51.gemfile +5 -0
  16. data/gemfiles/activerecord_52.gemfile +5 -0
  17. data/gemfiles/activerecord_60.gemfile +5 -0
  18. data/gemfiles/activerecord_61.gemfile +5 -0
  19. data/gemfiles/activerecord_70.gemfile +5 -0
  20. data/gemfiles/activerecord_head.gemfile +5 -0
  21. data/lib/generators/online_migrations/background_migration_generator.rb +29 -0
  22. data/lib/generators/online_migrations/install_generator.rb +34 -0
  23. data/lib/generators/online_migrations/templates/background_migration.rb.tt +22 -0
  24. data/lib/generators/online_migrations/templates/initializer.rb.tt +94 -0
  25. data/lib/generators/online_migrations/templates/migration.rb.tt +46 -0
  26. data/lib/online_migrations/background_migration.rb +64 -0
  27. data/lib/online_migrations/background_migrations/advisory_lock.rb +62 -0
  28. data/lib/online_migrations/background_migrations/backfill_column.rb +52 -0
  29. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +36 -0
  30. data/lib/online_migrations/background_migrations/config.rb +98 -0
  31. data/lib/online_migrations/background_migrations/copy_column.rb +90 -0
  32. data/lib/online_migrations/background_migrations/migration.rb +210 -0
  33. data/lib/online_migrations/background_migrations/migration_helpers.rb +238 -0
  34. data/lib/online_migrations/background_migrations/migration_job.rb +92 -0
  35. data/lib/online_migrations/background_migrations/migration_job_runner.rb +63 -0
  36. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +27 -0
  37. data/lib/online_migrations/background_migrations/migration_runner.rb +97 -0
  38. data/lib/online_migrations/background_migrations/migration_status_validator.rb +45 -0
  39. data/lib/online_migrations/background_migrations/scheduler.rb +49 -0
  40. data/lib/online_migrations/batch_iterator.rb +87 -0
  41. data/lib/online_migrations/change_column_type_helpers.rb +587 -0
  42. data/lib/online_migrations/command_checker.rb +590 -0
  43. data/lib/online_migrations/command_recorder.rb +137 -0
  44. data/lib/online_migrations/config.rb +198 -0
  45. data/lib/online_migrations/copy_trigger.rb +91 -0
  46. data/lib/online_migrations/database_tasks.rb +19 -0
  47. data/lib/online_migrations/error_messages.rb +388 -0
  48. data/lib/online_migrations/foreign_key_definition.rb +17 -0
  49. data/lib/online_migrations/foreign_keys_collector.rb +33 -0
  50. data/lib/online_migrations/indexes_collector.rb +48 -0
  51. data/lib/online_migrations/lock_retrier.rb +250 -0
  52. data/lib/online_migrations/migration.rb +63 -0
  53. data/lib/online_migrations/migrator.rb +23 -0
  54. data/lib/online_migrations/schema_cache.rb +96 -0
  55. data/lib/online_migrations/schema_statements.rb +1042 -0
  56. data/lib/online_migrations/utils.rb +140 -0
  57. data/lib/online_migrations/version.rb +5 -0
  58. data/lib/online_migrations.rb +74 -0
  59. data/online_migrations.gemspec +28 -0
  60. metadata +119 -0
@@ -0,0 +1,587 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OnlineMigrations
4
+ # To safely change the type of the column, we need to perform the following steps:
5
+ # 1. create a new column based on the old one (covered by `initialize_column_type_change`)
6
+ # 2. ensure data stays in sync (via triggers) (covered by `initialize_column_type_change`)
7
+ # 3. backfill data from the old column (`backfill_column_for_type_change`)
8
+ # 4. copy indexes, foreign keys, check constraints, NOT NULL constraint,
9
+ # make new column a Primary Key if we change type of the primary key column,
10
+ # swap new column in place (`finalize_column_type_change`)
11
+ # 5. remove copy trigger and old column (`cleanup_column_type_change`)
12
+ #
13
+ # For example, suppose we need to change `files`.`size` column's type from `integer` to `bigint`:
14
+ #
15
+ # 1. Create a new column and keep data in sync
16
+ # ```
17
+ # class InitializeFilesSizeTypeChangeToBigint < ActiveRecord::Migration
18
+ # def change
19
+ # initialize_column_type_change(:files, :size, :bigint)
20
+ # end
21
+ # end
22
+ # ```
23
+ #
24
+ # 2. Backfill data
25
+ # ```
26
+ # class BackfillFilesSizeTypeChangeToBigint < ActiveRecord::Migration
27
+ # def up
28
+ # backfill_column_for_type_change(:files, :size, progress: true)
29
+ # end
30
+ #
31
+ # def down
32
+ # # no op
33
+ # end
34
+ # end
35
+ # ```
36
+ #
37
+ # 3. Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place
38
+ # ```
39
+ # class FinalizeFilesSizeTypeChangeToBigint < ActiveRecord::Migration
40
+ # def change
41
+ # finalize_column_type_change(:files, :size)
42
+ # end
43
+ # end
44
+ # ```
45
+ #
46
+ # 4. Finally, if everything is working as expected, remove copy trigger and old column
47
+ # ```
48
+ # class CleanupFilesSizeTypeChangeToBigint < ActiveRecord::Migration
49
+ # def up
50
+ # cleanup_column_type_change(:files, :size)
51
+ # end
52
+ #
53
+ # def down
54
+ # initialize_column_type_change(:files, :size, :integer)
55
+ # end
56
+ # end
57
+ # ```
58
+ #
59
+ module ChangeColumnTypeHelpers
60
+ # Initialize the process of changing column type. Creates a new column from
61
+ # the old one and ensures that data stays in sync.
62
+ #
63
+ # @param table_name [String, Symbol]
64
+ # @param column_name [String, Symbol]
65
+ # @param new_type [String, Symbol]
66
+ # @param options [Hash] additional options that apply to a new type, `:limit` for example
67
+ #
68
+ # @return [void]
69
+ #
70
+ # @example
71
+ # initialize_column_type_change(:files, :size, :bigint)
72
+ #
73
+ # @example With additional column options
74
+ # initialize_column_type_change(:users, :name, :string, limit: 64)
75
+ #
76
+ def initialize_column_type_change(table_name, column_name, new_type, **options)
77
+ initialize_columns_type_change(table_name, [[column_name, new_type]], column_name => options)
78
+ end
79
+
80
+ # Same as `initialize_column_type_change` but for multiple columns at once
81
+ #
82
+ # This is useful to avoid multiple costly disk rewrites of large tables
83
+ # when changing type of each column separately.
84
+ #
85
+ # @param table_name [String, Symbol]
86
+ # @param columns_and_types [Array<Array<(Symbol, Symbol)>>] columns and new types,
87
+ # represented as nested arrays. Example: `[[:id, :bigint], [:name, :string]]`
88
+ # @param options [Hash] keys - column names, values -
89
+ # options for specific columns (additional options that apply to a new type, `:limit` for example)
90
+ #
91
+ # @see #initialize_column_type_change
92
+ #
93
+ 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) }
95
+ raise ArgumentError, "columns_and_types must be an array of arrays"
96
+ end
97
+
98
+ conversions = columns_and_types.map do |(column_name, _new_type)|
99
+ [column_name, __change_type_column(column_name)]
100
+ end.to_h
101
+
102
+ if (extra_keys = (options.keys - conversions.keys)).any?
103
+ raise ArgumentError, "Options has unknown keys: #{extra_keys.map(&:inspect).join(', ')}. "\
104
+ "Can contain only column names: #{conversions.keys.map(&:inspect).join(', ')}."
105
+ end
106
+
107
+ transaction do
108
+ columns_and_types.each do |(column_name, new_type)|
109
+ old_col = __column_for(table_name, column_name)
110
+ column_options = options[column_name] || {}
111
+ tmp_column_name = conversions[column_name]
112
+
113
+ if @connection.server_version >= 11_00_00 &&
114
+ primary_key(table_name) == column_name.to_s && old_col.type == :integer
115
+ # If the column to be converted is a Primary Key, set it to
116
+ # `NOT NULL DEFAULT 0` and we'll copy the correct values when backfilling.
117
+ # That way, we skip the expensive validation step required to add
118
+ # a `NOT NULL` constraint at the end of the process.
119
+ add_column(table_name, tmp_column_name, new_type,
120
+ **column_options.merge(default: old_col.default || 0, null: false))
121
+ else
122
+ add_column(table_name, tmp_column_name, new_type, **column_options)
123
+ change_column_default(table_name, tmp_column_name, old_col.default) unless old_col.default.nil?
124
+ end
125
+ end
126
+
127
+ __create_copy_triggers(table_name, conversions.keys, conversions.values)
128
+ end
129
+ end
130
+
131
+ # Reverts operations performed by initialize_column_type_change
132
+ #
133
+ # @param table_name [String, Symbol]
134
+ # @param column_name [String, Symbol]
135
+ # @param _new_type [String, Symbol] Passing this argument will make this change reversible in migration
136
+ # @param _options [Hash] additional options that apply to a new type.
137
+ # Passing this argument will make this change reversible in migration
138
+ #
139
+ # @return [void]
140
+ #
141
+ # @example
142
+ # revert_initialize_column_type_change(:files, :size)
143
+ #
144
+ def revert_initialize_column_type_change(table_name, column_name, _new_type = nil, **_options)
145
+ cleanup_column_type_change(table_name, column_name)
146
+ end
147
+
148
+ # Same as `revert_initialize_column_type_change` but for multiple columns.
149
+ # @see #revert_initialize_column_type_change
150
+ #
151
+ def revert_initialize_columns_type_change(table_name, columns_and_types, **_options)
152
+ column_names = columns_and_types.map(&:first)
153
+ cleanup_columns_type_change(table_name, *column_names)
154
+ end
155
+
156
+ # Backfills data from the old column to the new column.
157
+ #
158
+ # @param table_name [String, Symbol]
159
+ # @param column_name [String, Symbol]
160
+ # @param type_cast_function [String, Symbol] Some type changes require casting data to a new type.
161
+ # For example when changing from `text` to `jsonb`. In this case, use the `type_cast_function` option.
162
+ # You need to make sure there is no bad data and the cast will always succeed
163
+ # @param options [Hash] used to control the behavior of `update_column_in_batches`
164
+ # @return [void]
165
+ #
166
+ # @example
167
+ # backfill_column_for_type_change(:files, :size)
168
+ #
169
+ # @example With type casting
170
+ # backfill_column_for_type_change(:users, :settings, type_cast_function: "jsonb")
171
+ #
172
+ # @example Additional batch options
173
+ # backfill_column_for_type_change(:files, :size, batch_size: 10_000)
174
+ #
175
+ # @note This method should not be run within a transaction
176
+ # @note For extra large tables (100s of millions of records)
177
+ # it is recommended to use `backfill_column_for_type_change_in_background`.
178
+ #
179
+ def backfill_column_for_type_change(table_name, column_name, type_cast_function: nil, **options)
180
+ backfill_columns_for_type_change(table_name, column_name,
181
+ type_cast_functions: { column_name => type_cast_function }, **options)
182
+ end
183
+
184
+ # Same as `backfill_column_for_type_change` but for multiple columns.
185
+ #
186
+ # @param type_cast_functions [Hash] if not empty, keys - column names,
187
+ # values - corresponding type cast functions
188
+ #
189
+ # @see #backfill_column_for_type_change
190
+ #
191
+ def backfill_columns_for_type_change(table_name, *column_names, type_cast_functions: {}, **options)
192
+ conversions = column_names.map do |column_name|
193
+ tmp_column = __change_type_column(column_name)
194
+
195
+ old_value = Arel::Table.new(table_name)[column_name]
196
+ if (type_cast_function = type_cast_functions.with_indifferent_access[column_name])
197
+ old_value = Arel::Nodes::NamedFunction.new(type_cast_function.to_s, [old_value])
198
+ end
199
+
200
+ [tmp_column, old_value]
201
+ end
202
+
203
+ update_columns_in_batches(table_name, conversions, **options)
204
+ end
205
+
206
+ # Copies `NOT NULL` constraint, indexes, foreign key, and check constraints
207
+ # from the old column to the new column
208
+ #
209
+ # Note: If a column contains one or more indexes that don't contain the name of the original column,
210
+ # this procedure will fail. In that case, you'll first need to rename these indexes.
211
+ #
212
+ # @example
213
+ # finalize_column_type_change(:files, :size)
214
+ #
215
+ # @note This method should not be run within a transaction
216
+ #
217
+ def finalize_column_type_change(table_name, column_name)
218
+ finalize_columns_type_change(table_name, column_name)
219
+ end
220
+
221
+ # Same as `finalize_column_type_change` but for multiple columns
222
+ # @see #finalize_column_type_change
223
+ #
224
+ def finalize_columns_type_change(table_name, *column_names)
225
+ __ensure_not_in_transaction!
226
+
227
+ conversions = column_names.map do |column_name|
228
+ [column_name.to_s, __change_type_column(column_name)]
229
+ end.to_h
230
+
231
+ conversions.each do |column_name, tmp_column_name|
232
+ old_column = __column_for(table_name, column_name)
233
+ column = __column_for(table_name, tmp_column_name)
234
+
235
+ # We already set default and NOT NULL for to-be-PK columns
236
+ # for PG >= 11, so can skip this case
237
+ if !old_column.null && column.null
238
+ add_not_null_constraint(table_name, tmp_column_name, validate: false)
239
+ validate_not_null_constraint(table_name, tmp_column_name)
240
+
241
+ # At this point we are sure there are no NULLs in this column
242
+ transaction do
243
+ # For PG < 11 and Primary Key conversions, setting a column as the PK
244
+ # converts even check constraints to NOT NULL column constraints
245
+ # and forces an inline re-verification of the whole table.
246
+ #
247
+ # For PG >= 12 we can "promote" CHECK constraint to NOT NULL constraint,
248
+ # but for older versions we can set attribute as NOT NULL directly
249
+ # through PG internal tables.
250
+ # In-depth analysis of implications of this was made, so this approach
251
+ # is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
252
+ execute(<<~SQL)
253
+ UPDATE pg_catalog.pg_attribute
254
+ SET attnotnull = true
255
+ WHERE attrelid = #{quote(table_name)}::regclass
256
+ AND attname = #{quote(tmp_column_name)}
257
+ SQL
258
+
259
+ remove_not_null_constraint(table_name, tmp_column_name)
260
+ end
261
+ end
262
+
263
+ __copy_indexes(table_name, column_name, tmp_column_name)
264
+ __copy_foreign_keys(table_name, column_name, tmp_column_name)
265
+ __copy_check_constraints(table_name, column_name, tmp_column_name)
266
+
267
+ if primary_key(table_name) == column_name
268
+ __finalize_primary_key_type_change(table_name, column_name, column_names)
269
+ end
270
+ end
271
+
272
+ # Swap all non-PK columns at once, because otherwise when this helper possibly
273
+ # will have a need to be rerun, it will be impossible to know which columns
274
+ # already were swapped and which were not.
275
+ transaction do
276
+ conversions
277
+ .reject { |column_name, _tmp_column_name| column_name == primary_key(table_name) }
278
+ .each do |column_name, tmp_column_name|
279
+ swap_column_names(table_name, column_name, tmp_column_name)
280
+ end
281
+
282
+ __reset_trigger_function(table_name, column_names)
283
+ end
284
+ end
285
+
286
+ # Reverts operations performed by `finalize_column_type_change`
287
+ #
288
+ # @param table_name [String, Symbol]
289
+ # @param column_name [String, Symbol]
290
+ # @return [void]
291
+ #
292
+ # @example
293
+ # revert_finalize_column_type_change(:files, :size)
294
+ #
295
+ def revert_finalize_column_type_change(table_name, column_name)
296
+ revert_finalize_columns_type_change(table_name, column_name)
297
+ end
298
+
299
+ # Same as `revert_finalize_column_type_change` but for multiple columns
300
+ # @see #revert_finalize_column_type_change
301
+ #
302
+ def revert_finalize_columns_type_change(table_name, *column_names)
303
+ __ensure_not_in_transaction!
304
+
305
+ conversions = column_names.map do |column_name|
306
+ [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
+ end
318
+
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
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)
329
+ end
330
+ end
331
+
332
+ __check_constraints_for(table_name, tmp_column_name).each do |constraint|
333
+ remove_check_constraint(table_name, name: constraint.constraint_name)
334
+ end
335
+
336
+ if primary_key(table_name) == column_name
337
+ __finalize_primary_key_type_change(table_name, column_name, column_names)
338
+ end
339
+ end
340
+ end
341
+
342
+ # Finishes the process of column type change
343
+ #
344
+ # This helper removes copy triggers and old column.
345
+ #
346
+ # @param table_name [String, Symbol]
347
+ # @param column_name [String, Symbol]
348
+ # @return [void]
349
+ #
350
+ # @example
351
+ # cleanup_column_type_change(:files, :size)
352
+ #
353
+ # @note This method is not reversible by default in migrations.
354
+ # You need to use `initialize_column_type_change` in `down` method with
355
+ # the original column type to be able to revert.
356
+ #
357
+ def cleanup_column_type_change(table_name, column_name)
358
+ cleanup_columns_type_change(table_name, column_name)
359
+ end
360
+
361
+ # Same as `cleanup_column_type_change` but for multiple columns
362
+ # @see #cleanup_column_type_change
363
+ #
364
+ 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
368
+
369
+ transaction do
370
+ __remove_copy_triggers(table_name, conversions.keys, conversions.values)
371
+ remove_columns(table_name, *conversions.values)
372
+ end
373
+ end
374
+
375
+ private
376
+ def __change_type_column(column_name)
377
+ "#{column_name}_for_type_change"
378
+ end
379
+
380
+ def __copy_triggers_name(table_name, from_column, to_column)
381
+ CopyTrigger.on_table(table_name, connection: self).name(from_column, to_column)
382
+ end
383
+
384
+ def __create_copy_triggers(table_name, from_column, to_column)
385
+ CopyTrigger.on_table(table_name, connection: self).create(from_column, to_column)
386
+ end
387
+
388
+ def __remove_copy_triggers(table_name, from_column, to_column)
389
+ CopyTrigger.on_table(table_name, connection: self).remove(from_column, to_column)
390
+ end
391
+
392
+ def __copy_indexes(table_name, from_column, to_column)
393
+ from_column = from_column.to_s
394
+ to_column = to_column.to_s
395
+
396
+ __indexes_for(table_name, from_column).each do |index|
397
+ new_columns = index.columns.map do |column|
398
+ column == from_column ? to_column : column
399
+ end
400
+
401
+ # This is necessary as we can't properly rename indexes such as "taggings_idx".
402
+ unless index.name.include?(from_column)
403
+ raise "The index #{index.name} can not be copied as it does not "\
404
+ "mention the old column. You have to rename this index manually first."
405
+ end
406
+
407
+ name = index.name.gsub(from_column, to_column)
408
+
409
+ options = {
410
+ unique: index.unique,
411
+ name: name,
412
+ length: index.lengths,
413
+ order: index.orders,
414
+ }
415
+
416
+ options[:using] = index.using if index.using
417
+ options[:where] = index.where if index.where
418
+
419
+ # Opclasses were added in 5.2
420
+ if Utils.ar_version >= 5.2 && !index.opclasses.blank?
421
+ opclasses = index.opclasses.dup
422
+
423
+ # Copy the operator classes for the old column (if any) to the new column.
424
+ opclasses[to_column] = opclasses.delete(from_column) if opclasses[from_column]
425
+
426
+ options[:opclass] = opclasses
427
+ end
428
+
429
+ add_index(table_name, new_columns, **options.merge(algorithm: :concurrently))
430
+ end
431
+ end
432
+
433
+ def __indexes_for(table_name, column_name)
434
+ column_name = column_name.to_s
435
+
436
+ indexes(table_name).select { |index| index.columns.include?(column_name) }
437
+ end
438
+
439
+ # While its rare for a column to have multiple foreign keys, PostgreSQL supports this.
440
+ #
441
+ # One of the examples is when changing type of the referenced column
442
+ # with zero-downtime, we can have a column referencing both old column
443
+ # and new column, until the full migration is done.
444
+ def __copy_foreign_keys(table_name, from_column, to_column)
445
+ __foreign_keys_for(table_name, from_column).each do |fk|
446
+ __copy_foreign_key(fk, to_column)
447
+ end
448
+ end
449
+
450
+ def __foreign_keys_for(table_name, column_name)
451
+ foreign_keys(table_name).select { |fk| fk.column == column_name.to_s }
452
+ end
453
+
454
+ def __copy_check_constraints(table_name, from_column, to_column)
455
+ __check_constraints_for(table_name, from_column).each do |check|
456
+ expression = check["constraint_def"][/CHECK \({2}(.+)\){2}/, 1]
457
+ new_expression = expression.gsub(from_column.to_s, to_column.to_s)
458
+
459
+ add_check_constraint(table_name, new_expression, validate: false)
460
+
461
+ if check["valid"]
462
+ validate_check_constraint(table_name, expression: new_expression)
463
+ end
464
+ end
465
+ end
466
+
467
+ def __check_constraints_for(table_name, column_name)
468
+ __check_constraints(table_name).select { |c| c["column_name"] == column_name }
469
+ end
470
+
471
+ def __check_constraints(table_name)
472
+ schema = __schema_for_table(table_name)
473
+
474
+ check_sql = <<~SQL
475
+ SELECT
476
+ ccu.column_name as column_name,
477
+ con.conname as constraint_name,
478
+ pg_get_constraintdef(con.oid) as constraint_def,
479
+ con.convalidated AS valid
480
+ FROM pg_catalog.pg_constraint con
481
+ INNER JOIN pg_catalog.pg_class rel
482
+ ON rel.oid = con.conrelid
483
+ INNER JOIN pg_catalog.pg_namespace nsp
484
+ ON nsp.oid = con.connamespace
485
+ INNER JOIN information_schema.constraint_column_usage ccu
486
+ ON con.conname = ccu.constraint_name
487
+ AND rel.relname = ccu.table_name
488
+ WHERE rel.relname = #{quote(table_name)}
489
+ AND con.contype = 'c'
490
+ AND nsp.nspname = #{schema}
491
+ SQL
492
+
493
+ select_all(check_sql)
494
+ end
495
+
496
+ def __rename_constraint(table_name, old_name, new_name)
497
+ execute(<<~SQL)
498
+ ALTER TABLE #{quote_table_name(table_name)}
499
+ RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}
500
+ SQL
501
+ end
502
+
503
+ def __finalize_primary_key_type_change(table_name, column_name, column_names)
504
+ quoted_table_name = quote_table_name(table_name)
505
+ quoted_column_name = quote_column_name(column_name)
506
+ tmp_column_name = __change_type_column(column_name)
507
+
508
+ # This is to replace the existing "<table_name>_pkey" index
509
+ pkey_index_name = "index_#{table_name}_for_pkey"
510
+ add_index(table_name, tmp_column_name, unique: true, algorithm: :concurrently, name: pkey_index_name)
511
+
512
+ __replace_referencing_foreign_keys(table_name, column_name, tmp_column_name)
513
+
514
+ transaction do
515
+ # Lock the table explicitly to prevent new rows being inserted
516
+ execute("LOCK TABLE #{quoted_table_name} IN ACCESS EXCLUSIVE MODE")
517
+
518
+ swap_column_names(table_name, column_name, tmp_column_name)
519
+
520
+ # We need to update the trigger function in order to make PostgreSQL to
521
+ # regenerate the execution plan for it. This is to avoid type mismatch errors like
522
+ # "type of parameter 15 (bigint) does not match that when preparing the plan (integer)"
523
+ __reset_trigger_function(table_name, column_names)
524
+
525
+ # Transfer ownership of the primary key sequence
526
+ sequence_name = "#{table_name}_#{column_name}_seq"
527
+ execute("ALTER SEQUENCE #{quote_table_name(sequence_name)} OWNED BY #{quoted_table_name}.#{quoted_column_name}")
528
+ execute("ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} SET DEFAULT nextval(#{quote(sequence_name)}::regclass)")
529
+ change_column_default(table_name, tmp_column_name, nil)
530
+
531
+ # Replace the primary key constraint
532
+ pkey_constraint_name = "#{table_name}_pkey"
533
+ # CASCADE is not used here because the old FKs should be removed already
534
+ execute("ALTER TABLE #{quoted_table_name} DROP CONSTRAINT #{quote_table_name(pkey_constraint_name)}")
535
+ rename_index(table_name, pkey_index_name, pkey_constraint_name)
536
+ execute("ALTER TABLE #{quoted_table_name} ADD CONSTRAINT #{quote_table_name(pkey_constraint_name)} PRIMARY KEY USING INDEX #{quote_table_name(pkey_constraint_name)}")
537
+ end
538
+ end
539
+
540
+ # Replaces existing FKs in other tables referencing this table's old column
541
+ # with new ones referencing a new column.
542
+ def __replace_referencing_foreign_keys(table_name, from_column, to_column)
543
+ referencing_table_names = __referencing_table_names(table_name)
544
+
545
+ referencing_table_names.each do |referencing_table_name|
546
+ foreign_keys(referencing_table_name).each do |fk|
547
+ if fk.to_table == table_name.to_s && fk.primary_key == from_column
548
+ existing_name = fk.name
549
+ tmp_name = "#{existing_name}_tmp"
550
+ __copy_foreign_key(fk, fk.column, primary_key: to_column, name: tmp_name)
551
+
552
+ transaction do
553
+ # We'll need ACCESS EXCLUSIVE lock on the related tables,
554
+ # lets make sure it can be acquired from the start.
555
+ execute("LOCK TABLE #{table_name}, #{referencing_table_name} IN ACCESS EXCLUSIVE MODE")
556
+
557
+ remove_foreign_key(referencing_table_name, name: existing_name)
558
+ __rename_constraint(referencing_table_name, tmp_name, existing_name)
559
+ end
560
+ end
561
+ end
562
+ end
563
+ end
564
+
565
+ # Returns tables that have a FK to the given table
566
+ def __referencing_table_names(table_name)
567
+ schema = __schema_for_table(table_name)
568
+
569
+ select_values(<<~SQL)
570
+ SELECT DISTINCT con.conrelid::regclass::text AS conrelname
571
+ FROM pg_catalog.pg_constraint con
572
+ INNER JOIN pg_catalog.pg_namespace nsp
573
+ ON nsp.oid = con.connamespace
574
+ WHERE con.confrelid = #{quote(table_name)}::regclass
575
+ AND con.contype = 'f'
576
+ AND nsp.nspname = #{schema}
577
+ ORDER BY 1
578
+ SQL
579
+ end
580
+
581
+ def __reset_trigger_function(table_name, column_names)
582
+ tmp_column_names = column_names.map { |c| __change_type_column(c) }
583
+ function_name = __copy_triggers_name(table_name, column_names, tmp_column_names)
584
+ execute("ALTER FUNCTION #{quote_table_name(function_name)}() RESET ALL")
585
+ end
586
+ end
587
+ end