Dynamically determine classes to update when transferring from TLG to new org

What does this MR do and why?

This MR aims to simplify the transfer of a top-level group to an organization when updating records that are associated to a user.

This is achieved by finding models that have a single association to an user and organization, and then query and update the rows via the foreign key that's linked to a user.

There are some models that we cannot transfer automatically, and those will be dealt with manually in other MR's.

Notes

database

Check if index exists on user foreign key
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative 'config/environment'

Rails.application.eager_load!

models = Organizations::Users::TransferService.migratable_models

models_with_index = []
models_without_index = []

models.sort_by(&:name).each do |model|
  user_assoc = model.reflect_on_all_associations.find { |assoc| assoc.class_name == 'User' }
  next unless user_assoc

  foreign_key = user_assoc.foreign_key
  table_name = model.table_name

  indices = model.connection.indexes(table_name).select { |idx| idx.columns.first == foreign_key }

  model_info = {
    name: model.name,
    table: table_name,
    assoc_name: user_assoc.name,
    foreign_key: foreign_key,
    indices: indices
  }

  if indices.any?
    models_with_index << model_info
  else
    models_without_index << model_info
  end
end

puts '=' * 80
puts 'SUMMARY'
puts '=' * 80
puts "Total migratable models: #{models.size}"
puts "Models WITH index on user association: #{models_with_index.size}"
puts "Models WITHOUT index on user association: #{models_without_index.size}"
puts

puts '=' * 80
puts 'MODELS WITH INDEX ON USER ASSOCIATION'
puts '=' * 80
models_with_index.each do |info|
  puts "\n#{info[:name]}"
  puts "  Table: #{info[:table]}"
  puts "  User Association: #{info[:assoc_name]} (#{info[:foreign_key]})"
  puts "  Indices:"
  info[:indices].each do |idx|
    puts "    - #{idx.name} on (#{idx.columns.join(', ')})#{idx.unique ? ' [UNIQUE]' : ''}"
  end
end

puts
puts '=' * 80
puts 'MODELS WITHOUT INDEX ON USER ASSOCIATION'
puts '=' * 80
if models_without_index.empty?
  puts 'None! All models have indices.'
else
  models_without_index.each do |info|
    puts "\n#{info[:name]}"
    puts "  Table: #{info[:table]}"
    puts "  User Association: #{info[:assoc_name]} (#{info[:foreign_key]})"
    puts "  ⚠️  NO INDEX FOUND"
  end
end

All affected models have an index on the User foreign key:

Models with index on user association
================================================================================
SUMMARY
================================================================================
Total migratable models: 24
Models WITH index on user association: 24
Models WITHOUT index on user association: 0

================================================================================
MODELS WITH INDEX ON USER ASSOCIATION
================================================================================

Ai::Catalog::ItemVersion
  Table: ai_catalog_item_versions
  User Association: created_by (created_by_id)
  Indices:
    - index_ai_catalog_item_versions_on_created_by_id on (created_by_id)

Ai::Conversation::Thread
  Table: ai_conversation_threads
  User Association: user (user_id)
  Indices:
    - index_ai_conversation_threads_on_user_id_and_last_updated_at on (user_id, last_updated_at)

Ai::EventsCount
  Table: ai_events_counts
  User Association: user (user_id)
  Indices:
    - index_ai_events_counts_on_user_id on (user_id)

Ai::UsageEvent
  Table: ai_usage_events
  User Association: user (user_id)
  Indices:
    - index_ai_usage_events_on_user_id on (user_id)

Analytics::CustomDashboards::DashboardVersion
  Table: custom_dashboard_versions
  User Association: updated_by (updated_by_id)
  Indices:
    - index_custom_dashboard_versions_on_updated_by_id on (updated_by_id)

AuthenticationEvent
  Table: authentication_events
  User Association: user (user_id)
  Indices:
    - index_authentication_events_on_user_and_ip_address_and_result on (user_id, ip_address, result)
    - index_successful_authentication_events_for_metrics on (user_id, provider, created_at)

AwardEmoji
  Table: award_emoji
  User Association: user (user_id)
  Indices:
    - idx_award_emoji_on_user_emoji_name_awardable_type_awardable_id on (user_id, name, awardable_type, awardable_id)

BulkImport
  Table: bulk_imports
  User Association: user (user_id)
  Indices:
    - index_bulk_imports_on_user_id on (user_id)

Dependencies::DependencyListExport
  Table: dependency_list_exports
  User Association: author (user_id)
  Indices:
    - index_dependency_list_exports_on_user_id on (user_id)

DeployKey
  Table: keys
  User Association: user (user_id)
  Indices:
    - index_keys_on_user_id on (user_id)

Doorkeeper::AccessToken
  Table: oauth_access_tokens
  User Association: resource_owner (resource_owner_id)
  Indices:
    - partial_index_user_id_app_id_created_at_token_not_revoked on (resource_owner_id, application_id, created_at)

GitlabSubscriptions::SeatAssignment
  Table: subscription_seat_assignments
  User Association: user (user_id)
  Indices:
    - index_subscription_seat_assignments_on_user_id on (user_id)

Import::Offline::Export
  Table: import_offline_exports
  User Association: user (user_id)
  Indices:
    - index_import_offline_exports_on_user_id on (user_id)

Import::PlaceholderUserDetail
  Table: import_placeholder_user_details
  User Association: placeholder_user (placeholder_user_id)
  Indices:
    - index_import_placeholder_user_details_on_placeholder_user_id on (placeholder_user_id)

Key
  Table: keys
  User Association: user (user_id)
  Indices:
    - index_keys_on_user_id on (user_id)

LDAPKey
  Table: keys
  User Association: user (user_id)
  Indices:
    - index_keys_on_user_id on (user_id)

OauthAccessGrant
  Table: oauth_access_grants
  User Association: resource_owner (resource_owner_id)
  Indices:
    - index_oauth_access_grants_on_resource_owner_id on (resource_owner_id, application_id, created_at)

OauthAccessToken
  Table: oauth_access_tokens
  User Association: resource_owner (resource_owner_id)
  Indices:
    - partial_index_user_id_app_id_created_at_token_not_revoked on (resource_owner_id, application_id, created_at)

PersonalAccessToken
  Table: personal_access_tokens
  User Association: user (user_id)
  Indices:
    - index_pat_on_user_id_and_expires_at on (user_id, expires_at)
    - index_personal_access_tokens_on_user_id_and_id on (user_id, id)

PersonalSnippet
  Table: snippets
  User Association: author (author_id)
  Indices:
    - index_snippets_on_author_id on (author_id)

RemoteDevelopment::OrganizationClusterAgentMapping
  Table: organization_cluster_agent_mappings
  User Association: user (creator_id)
  Indices:
    - i_organization_cluster_agent_mappings_on_creator_id on (creator_id)

ResourceEvents::AbuseReportEvent
  Table: abuse_report_events
  User Association: user (user_id)
  Indices:
    - index_abuse_report_events_on_user_id on (user_id)

SpamLog
  Table: spam_logs
  User Association: user (user_id)
  Indices:
    - index_spam_logs_on_user_id on (user_id)

Vulnerabilities::Export
  Table: vulnerability_exports
  User Association: author (author_id)
  Indices:
    - index_vulnerability_exports_on_author_id on (author_id)

================================================================================
MODELS WITHOUT INDEX ON USER ASSOCIATION
================================================================================
None! All models have indices.

References

Relates to gitlab-org#19841 (closed)

Screenshots or screen recordings

Before After

How to set up and validate locally

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by tim mccarthy

Merge request reports

Loading
Loading