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,590 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "openssl"
5
+ require "set"
6
+
7
+ module OnlineMigrations
8
+ # @private
9
+ class CommandChecker
10
+ attr_accessor :direction
11
+
12
+ def initialize(migration)
13
+ @migration = migration
14
+ @safe = false
15
+ @new_tables = []
16
+ @lock_timeout_checked = false
17
+ @foreign_key_tables = Set.new
18
+ end
19
+
20
+ def safety_assured
21
+ @prev_value = @safe
22
+ @safe = true
23
+ yield
24
+ ensure
25
+ @safe = @prev_value
26
+ end
27
+
28
+ def check(command, *args, &block)
29
+ check_lock_timeout
30
+
31
+ unless safe?
32
+ do_check(command, *args, &block)
33
+
34
+ run_custom_checks(command, args)
35
+
36
+ if @foreign_key_tables.count { |t| !new_table?(t) } > 1
37
+ raise_error :multiple_foreign_keys
38
+ end
39
+ end
40
+
41
+ true
42
+ end
43
+
44
+ private
45
+ def check_lock_timeout
46
+ limit = OnlineMigrations.config.lock_timeout_limit
47
+
48
+ if limit && !@lock_timeout_checked
49
+ lock_timeout = connection.select_value("SHOW lock_timeout")
50
+ lock_timeout_sec = timeout_to_sec(lock_timeout)
51
+
52
+ if lock_timeout_sec == 0
53
+ Utils.warn("DANGER: No lock timeout set")
54
+ elsif lock_timeout_sec > limit
55
+ Utils.warn("DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}")
56
+ end
57
+
58
+ @lock_timeout_checked = true
59
+ end
60
+ end
61
+
62
+ def timeout_to_sec(timeout)
63
+ units = {
64
+ "us" => 10**-6,
65
+ "ms" => 10**-3,
66
+ "s" => 1,
67
+ "min" => 60,
68
+ "h" => 60 * 60,
69
+ "d" => 60 * 60 * 24,
70
+ }
71
+
72
+ timeout_sec = timeout.to_i
73
+
74
+ units.each do |k, v|
75
+ if timeout.end_with?(k)
76
+ timeout_sec *= v
77
+ break
78
+ end
79
+ end
80
+ timeout_sec
81
+ end
82
+
83
+ def safe?
84
+ @safe ||
85
+ ENV["SAFETY_ASSURED"] ||
86
+ (direction == :down && !OnlineMigrations.config.check_down) ||
87
+ version <= OnlineMigrations.config.start_after
88
+ end
89
+
90
+ def version
91
+ @migration.version || @migration.class.version
92
+ end
93
+
94
+ def do_check(command, *args, **options, &block)
95
+ case command
96
+ when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
97
+ check_columns_removal(command, *args, **options)
98
+ else
99
+ if respond_to?(command, true)
100
+ send(command, *args, **options, &block)
101
+ else
102
+ # assume it is safe
103
+ true
104
+ end
105
+ end
106
+ end
107
+
108
+ def create_table(table_name, **options, &block)
109
+ raise_error :create_table if options[:force]
110
+
111
+ # Probably, it would be good idea to also check for foreign keys
112
+ # with short integer types, and for mismatched primary key vs foreign key types.
113
+ # But I think this check is enough for now.
114
+ raise_error :short_primary_key_type if short_primary_key_type?(options)
115
+
116
+ if block
117
+ collect_foreign_keys(&block)
118
+ check_for_hash_indexes(&block) if postgresql_version < Gem::Version.new("10")
119
+ end
120
+
121
+ @new_tables << table_name.to_s
122
+ end
123
+
124
+ def create_join_table(table1, table2, **options, &block)
125
+ raise_error :create_table if options[:force]
126
+ raise_error :short_primary_key_type if short_primary_key_type?(options)
127
+
128
+ if block
129
+ collect_foreign_keys(&block)
130
+ check_for_hash_indexes(&block) if postgresql_version < Gem::Version.new("10")
131
+ end
132
+
133
+ table_name = options[:table_name] || derive_join_table_name(table1, table2)
134
+ @new_tables << table_name.to_s
135
+ end
136
+
137
+ def change_table(*)
138
+ raise_error :change_table, header: "Possibly dangerous operation"
139
+ end
140
+
141
+ def rename_table(table_name, new_name, **)
142
+ if !new_table?(table_name)
143
+ raise_error :rename_table,
144
+ table_name: table_name,
145
+ new_name: new_name
146
+ end
147
+ end
148
+
149
+ def add_column(table_name, column_name, type, **options)
150
+ volatile_default = false
151
+ if !new_or_small_table?(table_name) && !options[:default].nil? &&
152
+ (postgresql_version < Gem::Version.new("11") || (volatile_default = Utils.volatile_default?(connection, type, options[:default])))
153
+
154
+ raise_error :add_column_with_default,
155
+ code: command_str(:add_column_with_default, table_name, column_name, type, options),
156
+ not_null: options[:null] == false,
157
+ volatile_default: volatile_default
158
+ end
159
+
160
+ if type.to_s == "json"
161
+ raise_error :add_column_json,
162
+ code: command_str(:add_column, table_name, column_name, :jsonb, options)
163
+ end
164
+ end
165
+
166
+ def rename_column(table_name, column_name, new_column, **)
167
+ if !new_table?(table_name)
168
+ raise_error :rename_column,
169
+ table_name: table_name,
170
+ column_name: column_name,
171
+ new_column: new_column,
172
+ model: table_name.to_s.classify,
173
+ partial_writes: Utils.ar_partial_writes?,
174
+ partial_writes_setting: Utils.ar_partial_writes_setting
175
+ end
176
+ end
177
+
178
+ def change_column(table_name, column_name, type, **options)
179
+ return if new_table?(table_name)
180
+
181
+ type = type.to_sym
182
+
183
+ existing_column = connection.columns(table_name).find { |c| c.name == column_name.to_s }
184
+ if existing_column
185
+ existing_type = existing_column.type.to_sym
186
+
187
+ safe =
188
+ case type
189
+ when :string
190
+ # safe to increase limit or remove it
191
+ # not safe to decrease limit or add a limit
192
+ case existing_type
193
+ when :string
194
+ !options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
195
+ when :text
196
+ !options[:limit]
197
+ end
198
+ when :text
199
+ # safe to change varchar to text (and text to text)
200
+ [:string, :text].include?(existing_type)
201
+ when :numeric, :decimal
202
+ # numeric and decimal are equivalent and can be used interchangably
203
+ [:numeric, :decimal].include?(existing_type) &&
204
+ (
205
+ (
206
+ # unconstrained
207
+ !options[:precision] && !options[:scale]
208
+ ) || (
209
+ # increased precision, same scale
210
+ options[:precision] && existing_column.precision &&
211
+ options[:precision] >= existing_column.precision &&
212
+ options[:scale] == existing_column.scale
213
+ )
214
+ )
215
+ when :datetime, :timestamp, :timestamptz
216
+ [:timestamp, :timestamptz].include?(existing_type) &&
217
+ postgresql_version >= Gem::Version.new("12") &&
218
+ connection.select_value("SHOW timezone") == "UTC"
219
+ else
220
+ type == existing_type &&
221
+ options[:limit] == existing_column.limit &&
222
+ options[:precision] == existing_column.precision &&
223
+ options[:scale] == existing_column.scale
224
+ end
225
+
226
+ # unsafe to set NOT NULL for safe types
227
+ if safe && existing_column.null && options[:null] == false
228
+ raise_error :change_column_with_not_null
229
+ end
230
+
231
+ if !safe
232
+ raise_error :change_column,
233
+ initialize_change_code: command_str(:initialize_column_type_change, table_name, column_name, type, **options),
234
+ backfill_code: command_str(:backfill_column_for_type_change, table_name, column_name, **options),
235
+ finalize_code: command_str(:finalize_column_type_change, table_name, column_name),
236
+ cleanup_code: command_str(:cleanup_change_column_type_concurrently, table_name, column_name),
237
+ cleanup_down_code: command_str(:initialize_column_type_change, table_name, column_name, existing_type)
238
+ end
239
+ end
240
+ end
241
+
242
+ def change_column_null(table_name, column_name, allow_null, default = nil, **)
243
+ if !allow_null && !new_or_small_table?(table_name)
244
+ safe = false
245
+ # In PostgreSQL 12+ you can add a check constraint to the table
246
+ # and then "promote" it to NOT NULL for the column.
247
+ if postgresql_version >= Gem::Version.new("12")
248
+ safe = check_constraints(table_name).any? do |c|
249
+ c["def"] == "CHECK ((#{column_name} IS NOT NULL))" ||
250
+ c["def"] == "CHECK ((#{connection.quote_column_name(column_name)} IS NOT NULL))"
251
+ end
252
+ end
253
+
254
+ if !safe
255
+ constraint_name = "#{table_name}_#{column_name}_null"
256
+ vars = {
257
+ add_constraint_code: command_str(:add_not_null_constraint, table_name, column_name, name: constraint_name, validate: false),
258
+ backfill_code: nil,
259
+ validate_constraint_code: command_str(:validate_not_null_constraint, table_name, column_name, name: constraint_name),
260
+ remove_constraint_code: nil,
261
+ }
262
+
263
+ if !default.nil?
264
+ vars[:backfill_code] = command_str(:update_column_in_batches, table_name, column_name, default)
265
+ end
266
+
267
+ if postgresql_version >= Gem::Version.new("12")
268
+ vars[:remove_constraint_code] = command_str(:remove_check_constraint, table_name, name: constraint_name)
269
+ vars[:change_column_null_code] = command_str(:change_column_null, table_name, column_name, true)
270
+ end
271
+
272
+ raise_error :change_column_null, **vars
273
+ end
274
+ end
275
+ end
276
+
277
+ def check_columns_removal(command, *args, **options)
278
+ case command
279
+ when :remove_column
280
+ table_name, column_name = args
281
+ columns = [column_name]
282
+ when :remove_columns
283
+ table_name, *columns = args
284
+ when :remove_timestamps
285
+ table_name = args[0]
286
+ columns = [:created_at, :updated_at]
287
+ else
288
+ table_name, reference = args
289
+ columns = [:"#{reference}_id"]
290
+ columns << :"#{reference}_type" if options[:polymorphic]
291
+ end
292
+
293
+ if !new_table?(table_name)
294
+ indexes = connection.indexes(table_name).select do |index|
295
+ (index.columns & columns.map(&:to_s)).any?
296
+ end
297
+
298
+ raise_error :remove_column,
299
+ model: table_name.to_s.classify,
300
+ columns: columns.inspect,
301
+ command: command_str(command, *args),
302
+ table_name: table_name.inspect,
303
+ indexes: indexes.map { |i| i.name.to_sym.inspect }
304
+ end
305
+ end
306
+
307
+ def add_timestamps(table_name, **options)
308
+ volatile_default = false
309
+ if !new_or_small_table?(table_name) && !options[:default].nil? &&
310
+ (postgresql_version < Gem::Version.new("11") || (volatile_default = Utils.volatile_default?(connection, :datetime, options[:default])))
311
+
312
+ raise_error :add_timestamps_with_default,
313
+ code: [command_str(:add_column_with_default, table_name, :created_at, :datetime, options),
314
+ command_str(:add_column_with_default, table_name, :updated_at, :datetime, options)].join("\n "),
315
+ not_null: options[:null] == false,
316
+ volatile_default: volatile_default
317
+ end
318
+ end
319
+
320
+ def add_reference(table_name, ref_name, **options)
321
+ # Always added by default in 5.0+
322
+ index = options.fetch(:index) { Utils.ar_version >= 5.0 }
323
+
324
+ if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
325
+ raise_error :add_hash_index
326
+ end
327
+
328
+ concurrently_set = index.is_a?(Hash) && index[:algorithm] == :concurrently
329
+ bad_index = index && !concurrently_set
330
+
331
+ foreign_key = options.fetch(:foreign_key, false)
332
+
333
+ if foreign_key
334
+ foreign_table_name = Utils.foreign_table_name(ref_name, options)
335
+ @foreign_key_tables << foreign_table_name.to_s
336
+ end
337
+
338
+ validate_foreign_key = !foreign_key.is_a?(Hash) ||
339
+ (!foreign_key.key?(:validate) || foreign_key[:validate] == true)
340
+ bad_foreign_key = foreign_key && validate_foreign_key
341
+
342
+ if !new_or_small_table?(table_name) && (bad_index || bad_foreign_key)
343
+ raise_error :add_reference,
344
+ code: command_str(:add_reference_concurrently, table_name, ref_name, **options),
345
+ bad_index: bad_index,
346
+ bad_foreign_key: bad_foreign_key
347
+ end
348
+ end
349
+ alias add_belongs_to add_reference
350
+
351
+ def add_index(table_name, column_name, **options)
352
+ if options[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
353
+ raise_error :add_hash_index
354
+ elsif options[:algorithm] != :concurrently && !new_or_small_table?(table_name)
355
+ raise_error :add_index,
356
+ command: command_str(:add_index, table_name, column_name, **options.merge(algorithm: :concurrently))
357
+ end
358
+ end
359
+
360
+ def remove_index(table_name, column_name = nil, **options)
361
+ options[:column] ||= column_name
362
+
363
+ if options[:algorithm] != :concurrently && !new_or_small_table?(table_name)
364
+ raise_error :remove_index,
365
+ command: command_str(:remove_index, table_name, **options.merge(algorithm: :concurrently))
366
+ end
367
+ end
368
+
369
+ def add_foreign_key(from_table, to_table, **options)
370
+ if !new_or_small_table?(from_table)
371
+ validate = options.fetch(:validate, true)
372
+
373
+ if validate
374
+ raise_error :add_foreign_key,
375
+ add_code: command_str(:add_foreign_key, from_table, to_table, **options.merge(validate: false)),
376
+ validate_code: command_str(:validate_foreign_key, from_table, to_table)
377
+ end
378
+ end
379
+
380
+ @foreign_key_tables << to_table.to_s
381
+ end
382
+
383
+ def validate_foreign_key(*)
384
+ if crud_blocked?
385
+ raise_error :validate_foreign_key
386
+ end
387
+ end
388
+
389
+ def add_check_constraint(table_name, expression, **options)
390
+ if !new_or_small_table?(table_name) && options[:validate] != false
391
+ name = options[:name] || check_constraint_name(table_name, expression)
392
+
393
+ raise_error :add_check_constraint,
394
+ add_code: command_str(:add_check_constraint, table_name, expression, **options.merge(validate: false)),
395
+ validate_code: command_str(:validate_check_constraint, table_name, name: name)
396
+ end
397
+ end
398
+
399
+ def validate_constraint(*)
400
+ if crud_blocked?
401
+ raise_error :validate_constraint
402
+ end
403
+ end
404
+ alias validate_check_constraint validate_constraint
405
+ alias validate_not_null_constraint validate_constraint
406
+ alias validate_text_limit_constraint validate_constraint
407
+
408
+ def add_not_null_constraint(table_name, column_name, **options)
409
+ if !new_or_small_table?(table_name) && options[:validate] != false
410
+ raise_error :add_not_null_constraint,
411
+ add_code: command_str(:add_not_null_constraint, table_name, column_name, **options.merge(validate: false)),
412
+ validate_code: command_str(:validate_not_null_constraint, table_name, column_name, **options.except(:validate))
413
+ end
414
+ end
415
+
416
+ def add_text_limit_constraint(table_name, column_name, limit, **options)
417
+ if !new_or_small_table?(table_name) && options[:validate] != false
418
+ raise_error :add_text_limit_constraint,
419
+ add_code: command_str(:add_text_limit_constraint, table_name, column_name, limit, **options.merge(validate: false)),
420
+ validate_code: command_str(:validate_text_limit_constraint, table_name, column_name, **options.except(:validate))
421
+ end
422
+ end
423
+
424
+ def execute(*)
425
+ raise_error :execute, header: "Possibly dangerous operation"
426
+ end
427
+ alias exec_query execute
428
+
429
+ def short_primary_key_type?(options)
430
+ pk_type =
431
+ case options[:id]
432
+ when false
433
+ nil
434
+ when Hash
435
+ options[:id][:type]
436
+ when nil
437
+ # default type is used
438
+ connection.native_database_types[:primary_key].split.first
439
+ else
440
+ options[:id]
441
+ end
442
+
443
+ pk_type && !["bigserial", "bigint", "uuid"].include?(pk_type.to_s)
444
+ end
445
+
446
+ def collect_foreign_keys(&block)
447
+ collector = ForeignKeysCollector.new
448
+ collector.collect(&block)
449
+ @foreign_key_tables |= collector.referenced_tables
450
+ end
451
+
452
+ def check_for_hash_indexes(&block)
453
+ indexes = collect_indexes(&block)
454
+ if indexes.any? { |index| index.using == "hash" }
455
+ raise_error :add_hash_index
456
+ end
457
+ end
458
+
459
+ def collect_indexes(&block)
460
+ collector = IndexesCollector.new
461
+ collector.collect(&block)
462
+ collector.indexes
463
+ end
464
+
465
+ def new_or_small_table?(table_name)
466
+ small_tables = OnlineMigrations.config.small_tables
467
+
468
+ new_table?(table_name) ||
469
+ small_tables.include?(table_name.to_s)
470
+ end
471
+
472
+ def new_table?(table_name)
473
+ @new_tables.include?(table_name.to_s)
474
+ end
475
+
476
+ def postgresql_version
477
+ version =
478
+ if Utils.developer_env? && (target_version = OnlineMigrations.config.target_version)
479
+ target_version.to_s
480
+ else
481
+ # For rails 6.0+ we can use connection.database_version
482
+ pg_connection = connection.instance_variable_get(:@connection)
483
+ database_version = pg_connection.server_version
484
+ patch = database_version % 100
485
+ database_version /= 100
486
+ minor = database_version % 100
487
+ database_version /= 100
488
+ major = database_version
489
+ "#{major}.#{minor}.#{patch}"
490
+ end
491
+
492
+ Gem::Version.new(version)
493
+ end
494
+
495
+ def connection
496
+ @migration.connection
497
+ end
498
+
499
+ def raise_error(message_key, header: nil, **vars)
500
+ return if !OnlineMigrations.config.check_enabled?(message_key, version: version)
501
+
502
+ template = OnlineMigrations.config.error_messages.fetch(message_key)
503
+
504
+ vars[:migration_name] = @migration.name
505
+ vars[:migration_parent] = Utils.migration_parent_string
506
+ vars[:model_parent] = Utils.model_parent_string
507
+
508
+ if RUBY_VERSION >= "2.6"
509
+ message = ERB.new(template, trim_mode: "<>").result_with_hash(vars)
510
+ else
511
+ # `result_with_hash` was added in ruby 2.5
512
+ b = TOPLEVEL_BINDING.dup
513
+ vars.each_pair do |key, value|
514
+ b.local_variable_set(key, value)
515
+ end
516
+ message = ERB.new(template, nil, "<>").result(b)
517
+ end
518
+
519
+ @migration.stop!(message, header: header || "Dangerous operation detected")
520
+ end
521
+
522
+ def command_str(command, *args)
523
+ arg_list = args[0..-2].map(&:inspect)
524
+
525
+ last_arg = args.last
526
+ if last_arg.is_a?(Hash)
527
+ if last_arg.any?
528
+ arg_list << last_arg.map do |k, v|
529
+ case v
530
+ when Hash
531
+ # pretty index: { algorithm: :concurrently }
532
+ "#{k}: { #{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(', ')} }"
533
+ when Array, Numeric, String, Symbol, TrueClass, FalseClass
534
+ "#{k}: #{v.inspect}"
535
+ else
536
+ "<paste value here>"
537
+ end
538
+ end.join(", ")
539
+ end
540
+ else
541
+ arg_list << last_arg.inspect
542
+ end
543
+
544
+ "#{command} #{arg_list.join(', ')}"
545
+ end
546
+
547
+ def crud_blocked?
548
+ locks_query = <<~SQL
549
+ SELECT relation::regclass::text
550
+ FROM pg_locks
551
+ WHERE mode IN ('ShareLock', 'ShareRowExclusiveLock', 'ExclusiveLock', 'AccessExclusiveLock')
552
+ AND pid = pg_backend_pid()
553
+ SQL
554
+
555
+ connection.select_values(locks_query).any?
556
+ end
557
+
558
+ def check_constraint_name(table_name, expression)
559
+ identifier = "#{table_name}_#{expression}_chk"
560
+ hashed_identifier = OpenSSL::Digest::SHA256.hexdigest(identifier).first(10)
561
+
562
+ "chk_rails_#{hashed_identifier}"
563
+ end
564
+
565
+ def check_constraints(table_name)
566
+ constraints_query = <<~SQL
567
+ SELECT pg_get_constraintdef(oid) AS def
568
+ FROM pg_constraint
569
+ WHERE contype = 'c'
570
+ AND convalidated
571
+ AND conrelid = #{connection.quote(table_name)}::regclass
572
+ SQL
573
+
574
+ connection.select_all(constraints_query).to_a
575
+ end
576
+
577
+ # From ActiveRecord
578
+ def derive_join_table_name(table1, table2)
579
+ [table1.to_s, table2.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
580
+ end
581
+
582
+ def run_custom_checks(method, args)
583
+ OnlineMigrations.config.checks.each do |options, check|
584
+ if !options[:start_after] || version > options[:start_after]
585
+ @migration.instance_exec(method, args, &check)
586
+ end
587
+ end
588
+ end
589
+ end
590
+ end