online_migrations 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +34 -31
  4. data/docs/background_migrations.md +36 -4
  5. data/docs/configuring.md +3 -2
  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 +5 -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 +6 -20
  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 +68 -83
  23. data/lib/online_migrations/command_checker.rb +11 -29
  24. data/lib/online_migrations/config.rb +7 -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 +1 -1
  32. data/lib/online_migrations/schema_statements.rb +64 -256
  33. data/lib/online_migrations/utils.rb +18 -56
  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 +7 -6
  37. metadata +8 -7
  38. data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
  39. data/lib/online_migrations/foreign_key_definition.rb +0 -17
@@ -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)
@@ -419,8 +420,6 @@ module OnlineMigrations
419
420
  name = index.name.gsub(from_column, to_column)
420
421
  end
421
422
 
422
- # Generate a shorter name if needed.
423
- max_identifier_length = 63 # could use just `max_identifier_length` method for ActiveRecord >= 5.0.
424
423
  name = index_name(table_name, new_columns) if !name || name.length > max_identifier_length
425
424
 
426
425
  options = {
@@ -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
@@ -154,13 +154,7 @@ module OnlineMigrations
154
154
  check_columns_removal(command, *args, **options)
155
155
  else
156
156
  if respond_to?(command, true)
157
- if options.any?
158
- # Workaround for Active Record < 5 change_column_default
159
- # not accepting options.
160
- send(command, *args, **options, &block)
161
- else
162
- send(command, *args, &block)
163
- end
157
+ send(command, *args, **options, &block)
164
158
  else
165
159
  # assume it is safe
166
160
  true
@@ -362,9 +356,7 @@ module OnlineMigrations
362
356
  precision = options[:precision] || options[:limit] || 6
363
357
  existing_precision = existing_column.precision || existing_column.limit || 6
364
358
 
365
- # PostgreSQL interval data type was added in https://github.com/rails/rails/pull/16919
366
- (existing_type == :interval || (Utils.ar_version < 6.1 && existing_column.sql_type.start_with?("interval"))) &&
367
- precision >= existing_precision
359
+ existing_type == :interval && precision >= existing_precision
368
360
  when :inet
369
361
  existing_type == :cidr
370
362
  else
@@ -487,7 +479,7 @@ module OnlineMigrations
487
479
 
488
480
  def add_reference(table_name, ref_name, **options)
489
481
  # Always added by default in 5.0+
490
- index = options.fetch(:index) { Utils.ar_version >= 5.0 }
482
+ index = options.fetch(:index, true)
491
483
 
492
484
  if index.is_a?(Hash) && index[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
493
485
  raise_error :add_hash_index
@@ -515,7 +507,7 @@ module OnlineMigrations
515
507
  end
516
508
 
517
509
  if !options[:polymorphic]
518
- type = (options[:type] || (Utils.ar_version >= 5.1 ? :bigint : :integer)).to_sym
510
+ type = (options[:type] || :bigint).to_sym
519
511
  column_name = "#{ref_name}_id"
520
512
 
521
513
  foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
@@ -573,9 +565,9 @@ module OnlineMigrations
573
565
  end
574
566
 
575
567
  if index_def
576
- existing_options = [:name, :columns, :unique, :where, :type, :using, :opclasses].map do |option|
568
+ existing_options = [:name, :columns, :unique, :where, :type, :using, :opclasses].filter_map do |option|
577
569
  [option, index_def.public_send(option)] if index_def.respond_to?(option)
578
- end.compact.to_h
570
+ end.to_h
579
571
 
580
572
  @removed_indexes << IndexDefinition.new(table: table_name, **existing_options)
581
573
  end
@@ -738,20 +730,10 @@ module OnlineMigrations
738
730
  template = OnlineMigrations.config.error_messages.fetch(message_key)
739
731
 
740
732
  vars[:migration_name] = @migration.name
741
- vars[:migration_parent] = Utils.migration_parent_string
742
- vars[:model_parent] = Utils.model_parent_string
733
+ vars[:migration_parent] = "ActiveRecord::Migration[#{Utils.ar_version}]"
743
734
  vars[:ar_version] = Utils.ar_version
744
735
 
745
- if RUBY_VERSION >= "2.6"
746
- message = ERB.new(template, trim_mode: "<>").result_with_hash(vars)
747
- else
748
- # `result_with_hash` was added in ruby 2.5
749
- b = TOPLEVEL_BINDING.dup
750
- vars.each_pair do |key, value|
751
- b.local_variable_set(key, value)
752
- end
753
- message = ERB.new(template, nil, "<>").result(b)
754
- end
736
+ message = ERB.new(template, trim_mode: "<>").result_with_hash(vars)
755
737
 
756
738
  if (link = ERROR_MESSAGE_TO_LINK[message_key])
757
739
  message += "\nFor more details, see https://github.com/fatkodima/online_migrations##{link}"
@@ -790,7 +772,7 @@ module OnlineMigrations
790
772
  end
791
773
 
792
774
  def crud_blocked?
793
- locks_query = <<-SQL.strip_heredoc
775
+ locks_query = <<~SQL
794
776
  SELECT relation::regclass::text
795
777
  FROM pg_locks
796
778
  WHERE mode IN ('ShareLock', 'ShareRowExclusiveLock', 'ExclusiveLock', 'AccessExclusiveLock')
@@ -808,7 +790,7 @@ module OnlineMigrations
808
790
  end
809
791
 
810
792
  def check_constraints(table_name)
811
- constraints_query = <<-SQL.strip_heredoc
793
+ constraints_query = <<~SQL
812
794
  SELECT pg_get_constraintdef(oid) AS def
813
795
  FROM pg_constraint
814
796
  WHERE contype = 'c'
@@ -829,7 +811,7 @@ module OnlineMigrations
829
811
 
830
812
  def check_mismatched_foreign_key_type(table_name, column_name, type, **options)
831
813
  column_name = column_name.to_s
832
- ref_name = column_name.sub(/_id\z/, "")
814
+ ref_name = column_name.delete_suffix("_id")
833
815
 
834
816
  if like_foreign_key?(column_name, type)
835
817
  foreign_table_name = Utils.foreign_table_name(ref_name, options)
@@ -16,7 +16,6 @@ module OnlineMigrations
16
16
  #
17
17
  def start_after=(value)
18
18
  if value.is_a?(Hash)
19
- ensure_supports_multiple_dbs
20
19
  @start_after = value.stringify_keys
21
20
  else
22
21
  @start_after = value
@@ -49,7 +48,6 @@ module OnlineMigrations
49
48
  #
50
49
  def target_version=(value)
51
50
  if value.is_a?(Hash)
52
- ensure_supports_multiple_dbs
53
51
  @target_version = value.stringify_keys
54
52
  else
55
53
  @target_version = value
@@ -148,7 +146,7 @@ module OnlineMigrations
148
146
  # Returns a list of enabled checks
149
147
  #
150
148
  # All checks are enabled by default. To disable/enable a check use `disable_check`/`enable_check`.
151
- # For the list of available checks look at `lib/error_messages` folder.
149
+ # For the list of available checks look at the `error_messages.rb` file.
152
150
  #
153
151
  # @return [Array]
154
152
  #
@@ -184,7 +182,7 @@ module OnlineMigrations
184
182
  attempts: 30,
185
183
  base_delay: 0.01.seconds,
186
184
  max_delay: 1.minute,
187
- lock_timeout: 0.05.seconds
185
+ lock_timeout: 0.2.seconds
188
186
  )
189
187
 
190
188
  @background_migrations = BackgroundMigrations::Config.new
@@ -196,8 +194,8 @@ module OnlineMigrations
196
194
  @check_down = false
197
195
  @auto_analyze = false
198
196
  @alphabetize_schema = false
199
- @enabled_checks = @error_messages.keys.map { |k| [k, {}] }.to_h
200
- @verbose_sql_logs = defined?(Rails.env) && Rails.env.production?
197
+ @enabled_checks = @error_messages.keys.index_with({})
198
+ @verbose_sql_logs = defined?(Rails.env) && (Rails.env.production? || Rails.env.staging?)
201
199
  end
202
200
 
203
201
  def lock_retrier=(value)
@@ -210,7 +208,7 @@ module OnlineMigrations
210
208
 
211
209
  # Enables specific check
212
210
  #
213
- # For the list of available checks look at `lib/error_messages` module.
211
+ # For the list of available checks look at the `error_messages.rb` file.
214
212
  #
215
213
  # @param name [Symbol] check name
216
214
  # @param start_after [Integer] migration version from which this check will be performed
@@ -222,7 +220,7 @@ module OnlineMigrations
222
220
 
223
221
  # Disables specific check
224
222
  #
225
- # For the list of available checks look at `lib/error_messages` module.
223
+ # For the list of available checks look at the `error_messages.rb` file.
226
224
  #
227
225
  # @param name [Symbol] check name
228
226
  # @return [void]
@@ -233,7 +231,7 @@ module OnlineMigrations
233
231
 
234
232
  # Test whether specific check is enabled
235
233
  #
236
- # For the list of available checks look at `lib/error_messages` module.
234
+ # For the list of available checks look at the `error_messages.rb` file.
237
235
  #
238
236
  # @param name [Symbol] check name
239
237
  # @param version [Integer] migration version
@@ -272,12 +270,6 @@ module OnlineMigrations
272
270
  end
273
271
 
274
272
  private
275
- def ensure_supports_multiple_dbs
276
- if !Utils.supports_multiple_dbs?
277
- raise "OnlineMigrations does not support multiple databases for Active Record < 6.1"
278
- end
279
- end
280
-
281
273
  def db_config_name
282
274
  connection = OnlineMigrations.current_migration.connection
283
275
  connection.pool.db_config.name
@@ -18,12 +18,12 @@ module OnlineMigrations
18
18
  "trigger_#{hashed_identifier}"
19
19
  end
20
20
 
21
- def create(from_columns, to_columns)
21
+ def create(from_columns, to_columns, type_cast_functions: {})
22
22
  from_columns, to_columns = normalize_column_names(from_columns, to_columns)
23
23
  trigger_name = name(from_columns, to_columns)
24
- assignment_clauses = assignment_clauses_for_columns(from_columns, to_columns)
24
+ assignment_clauses = assignment_clauses_for_columns(from_columns, to_columns, type_cast_functions)
25
25
 
26
- connection.execute(<<-SQL.strip_heredoc)
26
+ connection.execute(<<~SQL)
27
27
  CREATE OR REPLACE FUNCTION #{trigger_name}() RETURNS TRIGGER AS $$
28
28
  BEGIN
29
29
  #{assignment_clauses};
@@ -32,11 +32,11 @@ module OnlineMigrations
32
32
  $$ LANGUAGE plpgsql;
33
33
  SQL
34
34
 
35
- connection.execute(<<-SQL.strip_heredoc)
35
+ connection.execute(<<~SQL)
36
36
  DROP TRIGGER IF EXISTS #{trigger_name} ON #{quoted_table_name}
37
37
  SQL
38
38
 
39
- connection.execute(<<-SQL.strip_heredoc)
39
+ connection.execute(<<~SQL)
40
40
  CREATE TRIGGER #{trigger_name}
41
41
  BEFORE INSERT OR UPDATE
42
42
  ON #{quoted_table_name}
@@ -75,14 +75,19 @@ module OnlineMigrations
75
75
  [from_columns, to_columns]
76
76
  end
77
77
 
78
- def assignment_clauses_for_columns(from_columns, to_columns)
78
+ def assignment_clauses_for_columns(from_columns, to_columns, type_cast_functions)
79
79
  combined_column_names = to_columns.zip(from_columns)
80
80
 
81
81
  assignment_clauses = combined_column_names.map do |(new_name, old_name)|
82
- new_name = connection.quote_column_name(new_name)
83
- old_name = connection.quote_column_name(old_name)
84
-
85
- "NEW.#{new_name} := NEW.#{old_name}"
82
+ quoted_new_name = connection.quote_column_name(new_name)
83
+ quoted_old_name = connection.quote_column_name(old_name)
84
+ type_cast_function = type_cast_functions[old_name]
85
+
86
+ if type_cast_function
87
+ "NEW.#{quoted_new_name} := #{type_cast_function.gsub(old_name.to_s, "NEW.#{quoted_old_name}")}"
88
+ else
89
+ "NEW.#{quoted_new_name} := NEW.#{quoted_old_name}"
90
+ end
86
91
  end
87
92
 
88
93
  assignment_clauses.join(";\n ")
@@ -109,14 +109,8 @@ A safer approach is to:
109
109
 
110
110
  1. ignore the column:
111
111
 
112
- class <%= model %> < <%= model_parent %>
113
- <% if ar_version >= 5 %>
112
+ class <%= model %> < ApplicationRecord
114
113
  self.ignored_columns = [\"<%= column_name %>\"]
115
- <% else %>
116
- def self.columns
117
- super.reject { |c| c.name == \"<%= column_name %>\" }
118
- end
119
- <% end %>
120
114
  end
121
115
 
122
116
  2. deploy
@@ -157,13 +151,7 @@ It will use a combination of a VIEW and column aliasing to work with both column
157
151
  <% if enumerate_columns_in_select_statements %>
158
152
  5. Ignore old column
159
153
 
160
- <% if ar_version >= 5 %>
161
154
  self.ignored_columns = [:<%= column_name %>]
162
- <% else %>
163
- def self.columns
164
- super.reject { |c| c.name == \"<%= column_name %>\" }
165
- end
166
- <% end %>
167
155
 
168
156
  6. Deploy
169
157
  7. Remove the column rename config from step 1
@@ -216,6 +204,8 @@ which will be passed to `add_column` when creating a new column, so you can over
216
204
 
217
205
  def up
218
206
  <%= backfill_code %>
207
+ # You can use `backfill_column_for_type_change_in_background` if want to
208
+ # backfill using background migrations.
219
209
  end
220
210
 
221
211
  def down
@@ -223,7 +213,10 @@ which will be passed to `add_column` when creating a new column, so you can over
223
213
  end
224
214
  end
225
215
 
226
- 3. Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place:
216
+ 3. Make sure your application works with values in both formats (when read from the database, converting
217
+ during writes works automatically). For most column type changes, this does not need any updates in the app.
218
+ 4. Deploy
219
+ 5. Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place:
227
220
 
228
221
  class Finalize<%= migration_name %> < <%= migration_parent %>
229
222
  disable_ddl_transaction!
@@ -233,8 +226,8 @@ which will be passed to `add_column` when creating a new column, so you can over
233
226
  end
234
227
  end
235
228
 
236
- 4. Deploy
237
- 5. Finally, if everything is working as expected, remove copy trigger and old column:
229
+ 6. Deploy
230
+ 7. Finally, if everything works as expected, remove copy trigger and old column:
238
231
 
239
232
  class Cleanup<%= migration_name %> < <%= migration_parent %>
240
233
  def up
@@ -246,7 +239,8 @@ which will be passed to `add_column` when creating a new column, so you can over
246
239
  end
247
240
  end
248
241
 
249
- 6. Deploy",
242
+ 8. Remove changes from step 3, if any
243
+ 9. Deploy",
250
244
 
251
245
  change_column_default:
252
246
  "Partial writes are enabled, which can cause incorrect values
@@ -303,14 +297,8 @@ A safer approach is to:
303
297
 
304
298
  1. Ignore the column(s):
305
299
 
306
- class <%= model %> < <%= model_parent %>
307
- <% if ar_version >= 5 %>
300
+ class <%= model %> < ApplicationRecord
308
301
  self.ignored_columns = <%= columns %>
309
- <% else %>
310
- def self.columns
311
- super.reject { |c| <%= columns %>.include?(c.name) }
312
- end
313
- <% end %>
314
302
  end
315
303
 
316
304
  2. Deploy
@@ -504,7 +492,7 @@ execute call, so cannot help you here. Make really sure that what
504
492
  you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
505
493
 
506
494
  multiple_foreign_keys:
507
- "Adding multiple foreign keys in a single migration blocks reads and writes on all involved tables until migration is completed.
495
+ "Adding multiple foreign keys in a single migration blocks writes on all involved tables until migration is completed.
508
496
  Avoid adding foreign key more than once per migration file, unless the source and target tables are identical.",
509
497
 
510
498
  drop_table_multiple_foreign_keys:
@@ -9,8 +9,8 @@ module OnlineMigrations
9
9
  @referenced_tables = Set.new
10
10
  end
11
11
 
12
- def collect(&table_definition)
13
- table_definition.call(self)
12
+ def collect
13
+ yield self
14
14
  end
15
15
 
16
16
  def foreign_key(to_table, **_options)
@@ -12,8 +12,8 @@ module OnlineMigrations
12
12
  @indexes = []
13
13
  end
14
14
 
15
- def collect(&table_definition)
16
- table_definition.call(self)
15
+ def collect
16
+ yield self
17
17
  end
18
18
 
19
19
  def index(_column_name, **options)
@@ -21,7 +21,7 @@ module OnlineMigrations
21
21
  end
22
22
 
23
23
  def references(*_ref_names, **options)
24
- index = options.fetch(:index) { Utils.ar_version >= 5.0 }
24
+ index = options.fetch(:index, true)
25
25
 
26
26
  if index
27
27
  using = index.is_a?(Hash) ? index[:using].to_s : nil