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:
- create a new column based on the old one (covered by
initialize_column_type_change
) - ensure data stays in sync (via triggers) (covered by
initialize_column_type_change
) - backfill data from the old column (
backfill_column_for_type_change
) - 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
) - 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_change
but 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_change
but for multiple columns. -
#finalize_column_type_change(table_name, column_name) ⇒ Object
Copies
NOT NULL
constraint, 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_change
but 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_change
but 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_change
but 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_change
but 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.
190 191 192 193 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 190 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.
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 202 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 = Arel::Table.new(table_name)[column_name] 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.
353 354 355 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 353 def cleanup_column_type_change(table_name, column_name) 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
360 361 362 363 364 365 366 367 368 369 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 360 def cleanup_columns_type_change(table_name, *column_names) conversions = column_names.index_with do |column_name| __change_type_column(column_name) end transaction do __remove_copy_triggers(table_name, conversions.keys, conversions.values) remove_columns(table_name, *conversions.values) 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.
234 235 236 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 234 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
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 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 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 241 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) # Exclusion constraints were added in https://github.com/rails/rails/pull/40224. if Utils.ar_version >= 7.1 __copy_exclusion_constraints(table_name, column_name, tmp_column_name) end 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
304 305 306 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 304 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
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 311 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 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 154 def revert_initialize_column_type_change(table_name, column_name, _new_type = nil, **) cleanup_column_type_change(table_name, column_name) end |
#revert_initialize_columns_type_change(table_name, columns_and_types, **_options) ⇒ Object
Same as revert_initialize_column_type_change
but for multiple columns.
161 162 163 164 |
# File 'lib/online_migrations/change_column_type_helpers.rb', line 161 def revert_initialize_columns_type_change(table_name, columns_and_types, **) column_names = columns_and_types.map(&:first) cleanup_columns_type_change(table_name, *column_names) end |