Module: OnlineMigrations::ChangeColumnTypeHelpers
- Included in:
- SchemaStatements
- Defined in:
- lib/online_migrations/change_column_type_helpers.rb
Overview
To safely change the type of the column, we need to perform the following steps:
1. create a new column based on the old one (covered by `initialize_column_type_change`)
2. ensure data stays in sync (via triggers) (covered by `initialize_column_type_change`)
3. backfill data from the old column (`backfill_column_for_type_change`)
4. copy indexes, foreign keys, check constraints, NOT NULL constraint,
make new column a Primary Key if we change type of the primary key column,
swap new column in place (`finalize_column_type_change`)
5. remove copy trigger and old column (`cleanup_column_type_change`)
For example, suppose we need to change files.size column's type from integer to bigint:
- Create a new column and keep data in sync
class InitializeFilesSizeTypeChangeToBigint < ActiveRecord::Migration
def change
initialize_column_type_change(:files, :size, :bigint)
end
end
- Backfill data
class BackfillFilesSizeTypeChangeToBigint < ActiveRecord::Migration
def up
backfill_column_for_type_change(:files, :size, progress: true)
end
def down
# no op
end
end
- Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place
class FinalizeFilesSizeTypeChangeToBigint < ActiveRecord::Migration
def change
finalize_column_type_change(:files, :size)
end
end
- Finally, if everything is working as expected, remove copy trigger and old column
class CleanupFilesSizeTypeChangeToBigint < ActiveRecord::Migration
def up
cleanup_column_type_change(:files, :size)
end
def down
initialize_column_type_change(:files, :size, :integer)
end
end
Instance Method Summary collapse
-
#backfill_column_for_type_change(table_name, column_name, type_cast_function: nil, **options) ⇒ void
Backfills data from the old column to the new column.
-
#backfill_columns_for_type_change(table_name, *column_names, type_cast_functions: {}, **options) ⇒ Object
Same as
backfill_column_for_type_changebut for multiple columns. -
#cleanup_column_type_change(table_name, column_name) ⇒ void
Finishes the process of column type change.
-
#cleanup_columns_type_change(table_name, *column_names) ⇒ Object
Same as
cleanup_column_type_changebut for multiple columns. -
#finalize_column_type_change(table_name, column_name) ⇒ Object
Copies
NOT NULLconstraint, indexes, foreign key, and check constraints from the old column to the new column. -
#finalize_columns_type_change(table_name, *column_names) ⇒ Object
Same as
finalize_column_type_changebut for multiple columns. -
#initialize_column_type_change(table_name, column_name, new_type, **options) ⇒ void
Initialize the process of changing column type.
-
#initialize_columns_type_change(table_name, columns_and_types, **options) ⇒ Object
Same as
initialize_column_type_changebut for multiple columns at once. -
#revert_finalize_column_type_change(table_name, column_name) ⇒ void
Reverts operations performed by
finalize_column_type_change. -
#revert_finalize_columns_type_change(table_name, *column_names) ⇒ Object
Same as
revert_finalize_column_type_changebut for multiple columns. -
#revert_initialize_column_type_change(table_name, column_name, _new_type = nil, **_options) ⇒ void
Reverts operations performed by initialize_column_type_change.
-
#revert_initialize_columns_type_change(table_name, columns_and_types, **_options) ⇒ Object
Same as
revert_initialize_column_type_changebut for multiple columns.
Instance Method Details
#backfill_column_for_type_change(table_name, column_name, type_cast_function: nil, **options) ⇒ void
This method should not be run within a transaction
For large tables (10/100s of millions of records)
it is recommended to use backfill_column_for_type_change_in_background.
This method returns an undefined value.
Backfills data from the old column to the new column.
199 200 201 202 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 199 def backfill_column_for_type_change(table_name, column_name, type_cast_function: nil, **) backfill_columns_for_type_change(table_name, column_name, type_cast_functions: { column_name => type_cast_function }, **) end |
#backfill_columns_for_type_change(table_name, *column_names, type_cast_functions: {}, **options) ⇒ Object
Same as backfill_column_for_type_change but for multiple columns.
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 211 def backfill_columns_for_type_change(table_name, *column_names, type_cast_functions: {}, **) conversions = column_names.map do |column_name| tmp_column = __change_type_column(column_name) old_value = begin # Delete after supporting only ActiveRecord >= 8.2 Arel::Table.new(table_name)[column_name] rescue ArgumentError # https://github.com/rails/rails/commit/b1650993b02497ae7d0d8b984d40bc036e62c681 Arel::Table.new(name: table_name)[column_name] end if (type_cast_function = type_cast_functions.with_indifferent_access[column_name]) old_value = case type_cast_function when Arel::Nodes::SqlLiteral type_cast_function else Arel::Nodes::NamedFunction.new(type_cast_function.to_s, [old_value]) end end [tmp_column, old_value] end update_columns_in_batches(table_name, conversions, **) end |
#cleanup_column_type_change(table_name, column_name) ⇒ void
This method is not reversible by default in migrations.
You need to use initialize_column_type_change in down method with
the original column type to be able to revert.
This method returns an undefined value.
Finishes the process of column type change
This helper removes copy triggers and old column.
365 366 367 368 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 365 def cleanup_column_type_change(table_name, column_name) __ensure_not_in_transaction! cleanup_columns_type_change(table_name, column_name) end |
#cleanup_columns_type_change(table_name, *column_names) ⇒ Object
Same as cleanup_column_type_change but for multiple columns
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 373 def cleanup_columns_type_change(table_name, *column_names) __ensure_not_in_transaction! tmp_column_names = column_names.map { |column_name| __change_type_column(column_name) } # Safely remove existing indexes and foreign keys first, if any. tmp_column_names.each do |column_name| __indexes_for(table_name, column_name).each do |index| remove_index(table_name, name: index.name, algorithm: :concurrently) end __foreign_keys_for(table_name, column_name).each do |fk| remove_foreign_key(table_name, name: fk.name) end end transaction do __remove_copy_triggers(table_name, column_names, tmp_column_names) remove_columns(table_name, *tmp_column_names) end end |
#finalize_column_type_change(table_name, column_name) ⇒ Object
This method should not be run within a transaction
Copies NOT NULL constraint, indexes, foreign key, and check constraints
from the old column to the new column
Note: If a column contains one or more indexes that don't contain the name of the original column, this procedure will fail. In that case, you'll first need to rename these indexes.
250 251 252 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 250 def finalize_column_type_change(table_name, column_name) finalize_columns_type_change(table_name, column_name) end |
#finalize_columns_type_change(table_name, *column_names) ⇒ Object
Same as finalize_column_type_change but for multiple columns
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 257 def finalize_columns_type_change(table_name, *column_names) __ensure_not_in_transaction! conversions = column_names.to_h do |column_name| [column_name.to_s, __change_type_column(column_name)] end primary_key = primary_key(table_name) conversions.each do |column_name, tmp_column_name| old_column = column_for(table_name, column_name) column = column_for(table_name, tmp_column_name) # We already set default and NOT NULL for to-be-PK columns # for PG >= 11, so can skip this case if !old_column.null && column.null add_not_null_constraint(table_name, tmp_column_name, validate: false) validate_not_null_constraint(table_name, tmp_column_name) # At this point we are sure there are no NULLs in this column transaction do change_column_null(table_name, tmp_column_name, false) remove_not_null_constraint(table_name, tmp_column_name) end end __copy_indexes(table_name, column_name, tmp_column_name) __copy_foreign_keys(table_name, column_name, tmp_column_name) __copy_check_constraints(table_name, column_name, tmp_column_name) __copy_exclusion_constraints(table_name, column_name, tmp_column_name) if column_name == primary_key __finalize_primary_key_type_change(table_name, column_name, column_names) end end # Swap all non-PK columns at once, because otherwise when this helper possibly # will have a need to be rerun, it will be impossible to know which columns # already were swapped and which were not. transaction do conversions .reject { |column_name, _tmp_column_name| column_name == primary_key } .each do |column_name, tmp_column_name| swap_column_names(table_name, column_name, tmp_column_name) end __reset_trigger_function(table_name, column_names) end end |
#initialize_column_type_change(table_name, column_name, new_type, **options) ⇒ void
This method returns an undefined value.
Initialize the process of changing column type. Creates a new column from the old one and ensures that data stays in sync.
79 80 81 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 79 def initialize_column_type_change(table_name, column_name, new_type, **) initialize_columns_type_change(table_name, [[column_name, new_type]], column_name => ) end |
#initialize_columns_type_change(table_name, columns_and_types, **options) ⇒ Object
Same as initialize_column_type_change but for multiple columns at once
This is useful to avoid multiple costly disk rewrites of large tables when changing type of each column separately.
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 96 def initialize_columns_type_change(table_name, columns_and_types, **) if !columns_and_types.is_a?(Array) || !columns_and_types.all?(Array) raise ArgumentError, "columns_and_types must be an array of arrays" end conversions = columns_and_types.to_h do |(column_name, _new_type)| [column_name, __change_type_column(column_name)] end if (extra_keys = (.keys - conversions.keys)).any? raise ArgumentError, "Options has unknown keys: #{extra_keys.map(&:inspect).join(', ')}. " \ "Can contain only column names: #{conversions.keys.map(&:inspect).join(', ')}." end transaction do type_cast_functions = {}.with_indifferent_access columns_and_types.each do |(column_name, new_type)| old_col = column_for(table_name, column_name) = (old_col, [:collation, :comment]) = [column_name] || {} type_cast_function = .delete(:type_cast_function) type_cast_functions[column_name] = type_cast_function if type_cast_function tmp_column_name = conversions[column_name] if primary_key(table_name) == column_name.to_s && old_col.type == :integer # For PG < 11 and Primary Key conversions, setting a column as the PK # converts even check constraints to NOT NULL column constraints # and forces an inline re-verification of the whole table. # To avoid this, we instead set it to `NOT NULL DEFAULT 0` and we'll # copy the correct values when backfilling. add_column(table_name, tmp_column_name, new_type, **, **, default: old_col.default || 0, null: false) else if !old_col.default.nil? = .merge(default: old_col.default, null: old_col.null) end add_column(table_name, tmp_column_name, new_type, **, **) end end __create_copy_triggers(table_name, conversions.keys, conversions.values, type_cast_functions: type_cast_functions) end end |
#revert_finalize_column_type_change(table_name, column_name) ⇒ void
This method returns an undefined value.
Reverts operations performed by finalize_column_type_change
316 317 318 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 316 def revert_finalize_column_type_change(table_name, column_name) revert_finalize_columns_type_change(table_name, column_name) end |
#revert_finalize_columns_type_change(table_name, *column_names) ⇒ Object
Same as revert_finalize_column_type_change but for multiple columns
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 323 def revert_finalize_columns_type_change(table_name, *column_names) __ensure_not_in_transaction! conversions = column_names.to_h do |column_name| [column_name.to_s, __change_type_column(column_name)] end primary_key = primary_key(table_name) primary_key_conversion = conversions.delete(primary_key) # No need to remove indexes, foreign keys etc, because it can take a significant amount # of time and will be automatically removed if decided to remove the column itself. if conversions.any? transaction do conversions.each do |column_name, tmp_column_name| swap_column_names(table_name, column_name, tmp_column_name) end __reset_trigger_function(table_name, column_names) end end if primary_key_conversion __finalize_primary_key_type_change(table_name, primary_key, column_names) end end |
#revert_initialize_column_type_change(table_name, column_name, _new_type = nil, **_options) ⇒ void
This method returns an undefined value.
Reverts operations performed by initialize_column_type_change
154 155 156 157 158 159 160 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 154 def revert_initialize_column_type_change(table_name, column_name, _new_type = nil, **) tmp_column_name = __change_type_column(column_name) transaction do __remove_copy_triggers(table_name, column_name, tmp_column_name) remove_column(table_name, tmp_column_name) end end |
#revert_initialize_columns_type_change(table_name, columns_and_types, **_options) ⇒ Object
Same as revert_initialize_column_type_change but for multiple columns.
165 166 167 168 169 170 171 172 173 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 165 def revert_initialize_columns_type_change(table_name, columns_and_types, **) column_names = columns_and_types.map(&:first) tmp_column_names = column_names.map { |column_name| __change_type_column(column_name) } transaction do __remove_copy_triggers(table_name, column_names, tmp_column_names) remove_columns(table_name, *tmp_column_names) end end |