mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-25 16:03:48 +00:00
265 lines
9.5 KiB
Ruby
265 lines
9.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative 'helpers/file_helper'
|
|
require_relative 'helpers/milestones'
|
|
require_relative '../lib/generators/post_deployment_migration/post_deployment_migration_generator'
|
|
|
|
module Keeps
|
|
# This is an implementation of ::Gitlab::Housekeeper::Keep.
|
|
# This initializes the conversion of bigint columns for a given table.
|
|
#
|
|
# You can run it individually with:
|
|
#
|
|
# ```
|
|
# bundle exec gitlab-housekeeper -d -k Keeps::InitializeBigIntConversion
|
|
# ```
|
|
class InitializeBigIntConversion < ::Gitlab::Housekeeper::Keep
|
|
INTEGER_COLUMNS_FILE = 'db/integer_ids_not_yet_initialized_to_bigint.yml'
|
|
MIGRATION_TEMPLATE = 'generator_templates/active_record/migration/'
|
|
FALLBACK_REVIEWER_FEATURE_CATEGORY = 'database'
|
|
CLASS_WITH_NAMESPACE = /class\s+([A-Z][A-Za-z0-9]*(?:::[A-Z][A-Za-z0-9]*)+)\s*(?:<\s*[A-Z][A-Za-z0-9:]*\s*)?$/
|
|
CLASS_WITHOUT_NAMESPACE = /class\s+([A-Z][A-Za-z0-9]*)\s*(?:<\s*[A-Z][A-Za-z0-9:]*\s*)?$/
|
|
|
|
TABLE_INT_IDS_YAML_FILE_COMMENT = <<~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(...)
|
|
::PostDeploymentMigration::PostDeploymentMigrationGenerator.source_root(MIGRATION_TEMPLATE)
|
|
|
|
reset_db
|
|
migrate
|
|
|
|
super
|
|
end
|
|
|
|
def each_change
|
|
integer_columns_to_migrate.each do |table_name, columns|
|
|
change = build_change(table_name, columns)
|
|
change.changed_files = []
|
|
migration_file_1, migration_number_1 = generate_initialization_migration_file(table_name, columns)
|
|
migration_file_2, migration_number_2 = generate_backfill_migration_file(table_name, columns)
|
|
|
|
change.changed_files << migration_file_1
|
|
change.changed_files << migration_file_2
|
|
change.changed_files << Pathname.new('db').join('schema_migrations', migration_number_1).to_s
|
|
change.changed_files << Pathname.new('db').join('schema_migrations', migration_number_2).to_s
|
|
|
|
file_path = update_model(table_name, columns)
|
|
update_integer_columns_file(table_name)
|
|
|
|
migrate
|
|
change.changed_files << Pathname.new('db').join('structure.sql').to_s
|
|
change.changed_files << file_path
|
|
change.changed_files << INTEGER_COLUMNS_FILE
|
|
|
|
yield(change)
|
|
|
|
reset_db
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def integer_columns_to_migrate
|
|
YAML.safe_load_file(INTEGER_COLUMNS_FILE)
|
|
end
|
|
|
|
def build_change(table_name, columns)
|
|
change = ::Gitlab::Housekeeper::Change.new
|
|
change.title = "Prepare conversion of #{table_name} to bigint".truncate(70, omission: '')
|
|
change.identifiers = [self.class.name.demodulize, table_name]
|
|
change.changelog_type = 'added'
|
|
change.labels = labels(table_name)
|
|
change.reviewers = pick_reviewers(table_name, change.identifiers).uniq
|
|
|
|
change.description = <<~MARKDOWN
|
|
Prepares conversion of `#{table_name}` to bigint for `#{columns.join(', ')}`
|
|
|
|
You can read more about the process for preparing bigint conversion in
|
|
https://docs.gitlab.com/ee/development/database/avoiding_downtime_in_migrations.html#migrating-integer-primary-keys-to-bigint.
|
|
|
|
As part of our process we want to ensure all ID columns are bigint to avoid the risk of overflowing while we continue our growth.
|
|
|
|
See https://gitlab.com/gitlab-org/gitlab/-/issues/465805+
|
|
|
|
Verify this MR as it was automatically created by `gitlab-housekeeper`.
|
|
|
|
Ensure that those columns are not being converted yet in the production database by checking Joe Bot through https://console.postgres.ai/gitlab.
|
|
If the columns were already converted in another merge request, consider closing this merge request
|
|
MARKDOWN
|
|
|
|
change
|
|
end
|
|
|
|
def labels(table_name)
|
|
table_info = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name)
|
|
|
|
group_labels = table_info.feature_categories.flat_map do |feature_category|
|
|
groups_helper.labels_for_feature_category(feature_category)
|
|
end
|
|
|
|
group_labels << 'maintenance::scalability'
|
|
end
|
|
|
|
def pick_reviewers(table_name, identifiers)
|
|
table_info = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name)
|
|
|
|
table_info.feature_categories.map do |feature_category|
|
|
groups_helper.pick_reviewer_for_feature_category(feature_category, identifiers,
|
|
fallback_feature_category: FALLBACK_REVIEWER_FEATURE_CATEGORY)
|
|
end
|
|
end
|
|
|
|
def generate_initialization_migration_file(table_name, columns)
|
|
migration_name = "initialize_conversion_of_#{table_name}_to_bigint".truncate(100, omission: '')
|
|
generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name])
|
|
|
|
migration_content = <<~RUBY.strip
|
|
disable_ddl_transaction!
|
|
|
|
TABLE_NAME = :#{table_name}
|
|
COLUMNS = %i[#{columns.join(' ')}]
|
|
|
|
def up
|
|
initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS, primary_key: :#{primary_key(table_name)})
|
|
end
|
|
|
|
def down
|
|
revert_initialize_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS)
|
|
end
|
|
RUBY
|
|
|
|
migration_file = generator.invoke_all.first
|
|
file_helper = ::Keeps::Helpers::FileHelper.new(migration_file)
|
|
file_helper.replace_method_content(:change, migration_content, strip_comments_from_file: true)
|
|
|
|
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', migration_file)
|
|
|
|
[migration_file, generator.migration_number]
|
|
end
|
|
|
|
def generate_backfill_migration_file(table_name, columns)
|
|
migration_name = "backfill_#{table_name}_for_bigint_conversion".truncate(100, omission: '')
|
|
generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name])
|
|
gitlab_schema = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).gitlab_schema
|
|
|
|
migration_content = <<~RUBY.strip
|
|
disable_ddl_transaction!
|
|
restrict_gitlab_migration gitlab_schema: :#{gitlab_schema}
|
|
|
|
TABLE_NAME = :#{table_name}
|
|
COLUMNS = %i[#{columns.join(' ')}]
|
|
|
|
def up
|
|
backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS, primary_key: :#{primary_key(table_name)})
|
|
end
|
|
|
|
def down
|
|
revert_backfill_conversion_of_integer_to_bigint(TABLE_NAME, COLUMNS)
|
|
end
|
|
RUBY
|
|
|
|
migration_file = generator.invoke_all.first
|
|
file_helper = ::Keeps::Helpers::FileHelper.new(migration_file)
|
|
file_helper.replace_method_content(:change, migration_content, strip_comments_from_file: true)
|
|
|
|
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', migration_file)
|
|
|
|
[migration_file, generator.migration_number]
|
|
end
|
|
|
|
def model_path(table_name)
|
|
model = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first
|
|
nested_model = model.underscore
|
|
file_path = Rails.root.join('app', 'models', "#{nested_model}.rb").to_s
|
|
|
|
if File.exist?(file_path)
|
|
file_path
|
|
else
|
|
Rails.root.join('ee', 'app', 'models', "#{nested_model}.rb").to_s
|
|
end
|
|
end
|
|
|
|
def update_model(table_name, columns)
|
|
class_name = Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first.split("::").last
|
|
file_path = model_path(table_name)
|
|
|
|
ignore_columns = columns.map do |column|
|
|
"ignore_column :#{column}_convert_to_bigint, remove_with: '#{n_3_milestone.version}', " \
|
|
"remove_after: '#{n_2_milestone.date}'"
|
|
end
|
|
|
|
new_content = <<~RUBY
|
|
#{ignore_columns.join("\n")}
|
|
RUBY
|
|
|
|
insert_after_class_definition(file_path, class_name, new_content)
|
|
::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', file_path)
|
|
|
|
file_path
|
|
end
|
|
|
|
def insert_after_class_definition(file_path, class_name, new_content)
|
|
content = File.read(file_path)
|
|
pattern = class_name.include?('::') ? CLASS_WITH_NAMESPACE : CLASS_WITHOUT_NAMESPACE
|
|
|
|
matches = content.scan(pattern)
|
|
return unless matches.flatten.include?(class_name)
|
|
|
|
updated_content = content.gsub(pattern) do |match|
|
|
if ::Regexp.last_match(1) == class_name
|
|
"#{match}\n#{new_content}\n"
|
|
else
|
|
match
|
|
end
|
|
end
|
|
|
|
File.write(file_path, updated_content)
|
|
end
|
|
|
|
def update_integer_columns_file(table_name)
|
|
file_path = Rails.root.join(INTEGER_COLUMNS_FILE)
|
|
data = YAML.safe_load_file(file_path)
|
|
data.delete(table_name)
|
|
|
|
File.open(INTEGER_COLUMNS_FILE, 'w') do |f|
|
|
f.write(TABLE_INT_IDS_YAML_FILE_COMMENT)
|
|
f.write(data.to_yaml)
|
|
end
|
|
end
|
|
|
|
def migrate
|
|
::Gitlab::Housekeeper::Shell.execute('rails', 'db:migrate', env: { 'RAILS_ENV' => 'test' })
|
|
end
|
|
|
|
def reset_db
|
|
ApplicationRecord.connection_handler.clear_all_connections!
|
|
::Gitlab::Housekeeper::Shell.execute('rails', 'db:reset', env: { 'RAILS_ENV' => 'test' })
|
|
end
|
|
|
|
def n_2_milestone
|
|
milestones_helper.upcoming_milestones[2]
|
|
end
|
|
|
|
def n_3_milestone
|
|
milestones_helper.upcoming_milestones[3]
|
|
end
|
|
|
|
def groups_helper
|
|
@groups_helper ||= ::Keeps::Helpers::Groups.new
|
|
end
|
|
|
|
def milestones_helper
|
|
@milestones_helper ||= ::Keeps::Helpers::Milestones.new
|
|
end
|
|
|
|
def primary_key(table_name)
|
|
Gitlab::Database::Dictionary.entries.find_by_table_name(table_name).classes.first.constantize.primary_key
|
|
end
|
|
end
|
|
end
|