283 lines
10 KiB
Ruby
283 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module Database
|
|
module Migrations
|
|
module Conversions
|
|
class BigintConverter
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
MIN_MILESTONE = '17.4'
|
|
DEFAULT_VALUE_MAP = {
|
|
integer: 0,
|
|
bigint: 0,
|
|
uuid: '00000000-0000-0000-0000-000000000000'
|
|
}.freeze
|
|
|
|
YAML_FILE_PATH = 'db/integer_ids_not_yet_initialized_to_bigint.yml'
|
|
YAML_HEADER = <<~MESSAGE
|
|
# -- DON'T MANUALLY EDIT --
|
|
# Contains the list of integer IDs which were converted to bigint for new installations in
|
|
# https://gitlab.com/gitlab-org/gitlab/-/issues/438124, but they are still integers for existing instances.
|
|
# On initialize_conversion_of_integer_to_bigint those integer IDs will be removed automatically from here.
|
|
MESSAGE
|
|
|
|
def initialize(migration, table, columns)
|
|
@migration = migration
|
|
@table = table.to_s
|
|
@columns = Array.wrap(columns).map(&:to_s)
|
|
end
|
|
|
|
def init
|
|
verify_table_and_columns_exist!
|
|
return if all_converted?
|
|
|
|
verify_all_integer_ids_are_included!
|
|
check_trigger_permissions!
|
|
# The reason why `disable_ddl_transaction!` is required
|
|
# for `initialize_conversion_of_integer_to_bigint`
|
|
migration.with_lock_retries do
|
|
# Only integer columns should be considered
|
|
# because in new instance, every ID will be bigint and should be skipped for conversion
|
|
create_bigint_columns(source: integer_columns, type: 'bigint')
|
|
create_trigger
|
|
end
|
|
end
|
|
|
|
def revert_init
|
|
verify_table_and_columns_exist!
|
|
check_trigger_permissions!
|
|
# Same as what `initialize_conversion_of_integer_to_bigint` does
|
|
migration.with_lock_retries do
|
|
remove_bigint_columns
|
|
remove_trigger
|
|
end
|
|
end
|
|
|
|
def restore_cleanup
|
|
verify_table_and_columns_exist!
|
|
check_trigger_permissions!
|
|
# All given columns should be bigint for now
|
|
# If source is integer_columns, there will be no columns to restore
|
|
create_bigint_columns(source: selected_columns, type: 'integer')
|
|
create_trigger
|
|
end
|
|
|
|
def cleanup
|
|
revert_init
|
|
update_file_for_integer_ids_not_yet_initialized_to_bigint
|
|
end
|
|
|
|
# We will queue the job and let the job to check if columns exist and continue with the copy
|
|
def backfill(primary_key:, batch_size:, sub_batch_size:, pause_ms:, job_interval:)
|
|
verify_table_and_columns_exist!
|
|
migration.queue_batched_background_migration(
|
|
'CopyColumnUsingBackgroundMigrationJob',
|
|
table,
|
|
primary_key,
|
|
*column_pair,
|
|
job_interval: job_interval,
|
|
pause_ms: pause_ms,
|
|
batch_size: batch_size,
|
|
sub_batch_size: sub_batch_size
|
|
)
|
|
end
|
|
|
|
def ensure_backfill(primary_key:)
|
|
verify_table_and_columns_exist!
|
|
migration.ensure_batched_background_migration_is_finished(
|
|
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
|
|
table_name: table,
|
|
column_name: primary_key,
|
|
job_arguments: column_pair
|
|
)
|
|
end
|
|
|
|
def revert_backfill(primary_key:)
|
|
verify_table_and_columns_exist!
|
|
migration.delete_batched_background_migration(
|
|
'CopyColumnUsingBackgroundMigrationJob',
|
|
table,
|
|
primary_key,
|
|
column_pair
|
|
)
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :migration, :table, :columns
|
|
|
|
def verify_table_and_columns_exist!
|
|
raise "Table #{table} does not exist" unless migration.table_exists?(table)
|
|
|
|
unknown_column_names = columns - all_columns.map(&:name)
|
|
return if unknown_column_names.blank?
|
|
|
|
raise "Columns #{unknown_column_names.join(', ')} does not exist on #{table}"
|
|
end
|
|
|
|
def all_converted?
|
|
integer_columns.blank?.tap do
|
|
Gitlab::AppLogger.warn(
|
|
message: 'The columns are already of type bigint.',
|
|
table: table, columns: columns
|
|
)
|
|
end
|
|
end
|
|
|
|
def verify_all_integer_ids_are_included!
|
|
return if initialization_for_all_integer_ids_not_required?
|
|
|
|
missing_integer_ids = all_table_integer_ids.fetch(table, []) - columns
|
|
return if missing_integer_ids.blank?
|
|
|
|
raise <<~ERROR
|
|
Table #{table} still has integer ID columns (#{missing_integer_ids.join(', ')}).
|
|
Please include them in the 'columns' param and in your backfill migration.
|
|
For more info: https://gitlab.com/gitlab-org/gitlab/-/issues/482470
|
|
ERROR
|
|
end
|
|
|
|
# This check can be removed once we convert all integer IDs to bigint
|
|
# in https://gitlab.com/gitlab-org/gitlab/-/issues/465805
|
|
def initialization_for_all_integer_ids_not_required?
|
|
!migration.respond_to?(:milestone) ||
|
|
Gitlab::VersionInfo.parse_from_milestone(migration.milestone) <
|
|
Gitlab::VersionInfo.parse_from_milestone(MIN_MILESTONE)
|
|
end
|
|
|
|
def all_table_integer_ids
|
|
YAML.safe_load_file(File.join(YAML_FILE_PATH))
|
|
rescue Errno::ENOENT
|
|
{}
|
|
end
|
|
strong_memoize_attr :all_table_integer_ids
|
|
|
|
def check_trigger_permissions!
|
|
migration.check_trigger_permissions!(table)
|
|
end
|
|
|
|
def create_bigint_columns(source:, type:)
|
|
bigint_columns_for_conversion(source: source).each do |options|
|
|
migration.add_column(
|
|
table, options[:name], type,
|
|
if_not_exists: true, **options.except(:name)
|
|
)
|
|
end
|
|
|
|
clear_memoization(:all_columns)
|
|
end
|
|
|
|
def remove_bigint_columns
|
|
# We don't need to worry about the `columns` are type integer or bigint
|
|
# For reverting the initialization, the `columns` are type integer
|
|
# For cleaning up the conversion, the `columns` will be type bigint
|
|
# We just need to remove the columns with `_convert_to_bigint` suffix
|
|
bigint_columns_for_conversion(source: selected_columns).each do |options|
|
|
migration.remove_column(table, options[:name], if_exists: true)
|
|
end
|
|
|
|
clear_memoization(:all_columns)
|
|
end
|
|
|
|
# Since trigger checks if bigint column for conversion exists or not,
|
|
# the full list of columns can be used to ensure same trigger name can be used
|
|
# across all different instances.
|
|
def create_trigger
|
|
migration.install_rename_triggers(table, *column_pair)
|
|
end
|
|
|
|
def remove_trigger
|
|
trigger_name = migration.rename_trigger_name(table, *column_pair)
|
|
migration.remove_rename_triggers(table, trigger_name)
|
|
end
|
|
|
|
def all_columns
|
|
migration.columns(table)
|
|
end
|
|
strong_memoize_attr :all_columns
|
|
|
|
def selected_columns
|
|
all_columns.select { |column| columns.include?(column.name) }
|
|
end
|
|
|
|
def integer_columns
|
|
selected_columns.select { |column| column.sql_type == 'integer' }
|
|
end
|
|
|
|
def bigint_columns_for_conversion(source:)
|
|
source.map do |column|
|
|
{
|
|
name: migration.convert_to_bigint_column(column.name),
|
|
default: default_for(column),
|
|
null: nullable?(column),
|
|
array: column.array
|
|
}
|
|
end
|
|
end
|
|
|
|
def all_bigint_column_names
|
|
all_columns.filter_map { |column| column.name if column.sql_type == 'bigint' }
|
|
end
|
|
|
|
def column_pair
|
|
[
|
|
columns,
|
|
columns.map { |name| migration.convert_to_bigint_column(name) }
|
|
]
|
|
end
|
|
|
|
def default_for(column)
|
|
return column.default if nullable?(column)
|
|
|
|
# If the column to be converted is either a PK or is defined as NOT NULL,
|
|
# set it to `NOT NULL DEFAULT 0` and we'll copy paste the correct values bellow
|
|
# That way, we skip the expensive validation step required to add
|
|
# a NOT NULL constraint at the end of the process
|
|
column.default || DEFAULT_VALUE_MAP.fetch(column.sql_type.to_sym)
|
|
end
|
|
|
|
def nullable?(column)
|
|
!primary_key?(column) && column.null
|
|
end
|
|
|
|
def primary_key?(column)
|
|
primary_keys.include?(column.name)
|
|
end
|
|
|
|
def primary_keys
|
|
Array.wrap(
|
|
# For single primary key, it's a string, e.g. `'id'`
|
|
# For composite primary key, it's an array, e.g. `['id', 'partition_id']`
|
|
migration.primary_key(table)
|
|
)
|
|
end
|
|
strong_memoize_attr :primary_keys
|
|
|
|
def update_file_for_integer_ids_not_yet_initialized_to_bigint
|
|
return unless Rails.env.development? || Rails.env.test?
|
|
return if initialization_for_all_integer_ids_not_required?
|
|
|
|
remaining_integer_ids = all_table_integer_ids.fetch(table, []) - all_bigint_column_names
|
|
new_table_integer_ids =
|
|
if remaining_integer_ids.blank?
|
|
all_table_integer_ids.except(table)
|
|
else
|
|
all_table_integer_ids.merge(table => remaining_integer_ids)
|
|
end
|
|
|
|
if new_table_integer_ids.blank?
|
|
FileUtils.rm_f(YAML_FILE_PATH)
|
|
else
|
|
File.open(YAML_FILE_PATH, 'w') do |file|
|
|
file.write(YAML_HEADER)
|
|
file.write(new_table_integer_ids.stringify_keys.to_yaml)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|