diff --git a/db/post_migrate/20220830051704_add_temporary_index_for_orphaned_invited_members.rb b/db/post_migrate/20220830051704_add_temporary_index_for_orphaned_invited_members.rb new file mode 100644 index 0000000000000000000000000000000000000000..90254ac3d86f1b4f565528bd2f815155ac00fc5d --- /dev/null +++ b/db/post_migrate/20220830051704_add_temporary_index_for_orphaned_invited_members.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddTemporaryIndexForOrphanedInvitedMembers < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + TMP_INDEX_NAME = 'tmp_idx_orphaned_invited_members' + + def up + add_concurrent_index('members', :id, where: query_condition, name: TMP_INDEX_NAME) + end + + def down + remove_concurrent_index_by_name('members', TMP_INDEX_NAME) if index_exists_by_name?('members', TMP_INDEX_NAME) + end + + private + + def query_condition + 'invite_token IS NULL and invite_accepted_at IS NOT NULL AND user_id IS NULL' + end +end diff --git a/db/post_migrate/20220830061704_orphaned_invited_members_cleanup.rb b/db/post_migrate/20220830061704_orphaned_invited_members_cleanup.rb new file mode 100644 index 0000000000000000000000000000000000000000..c52495101648e0456aa41860f7144ee94c2d64f0 --- /dev/null +++ b/db/post_migrate/20220830061704_orphaned_invited_members_cleanup.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class OrphanedInvitedMembersCleanup < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + # rubocop:disable Style/SymbolProc + membership.where(query_condition).each_batch(of: 100) do |relation| + relation.delete_all + end + # rubocop:enable Style/SymbolProc + end + + def down + # This migration is irreversible + end + + private + + def membership + @membership ||= define_batchable_model('members') + end + + def query_condition + 'invite_token IS NULL and invite_accepted_at IS NOT NULL AND user_id IS NULL' + end +end diff --git a/db/post_migrate/20220830071704_remove_temporary_index_for_orphaned_invited_members.rb b/db/post_migrate/20220830071704_remove_temporary_index_for_orphaned_invited_members.rb new file mode 100644 index 0000000000000000000000000000000000000000..c6b712da4c0477b4bb5583da1b2af95a5b797c24 --- /dev/null +++ b/db/post_migrate/20220830071704_remove_temporary_index_for_orphaned_invited_members.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class RemoveTemporaryIndexForOrphanedInvitedMembers < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + TMP_INDEX_NAME = 'tmp_idx_orphaned_invited_members' + + def up + remove_concurrent_index_by_name('members', TMP_INDEX_NAME) if index_exists_by_name?('members', TMP_INDEX_NAME) + end + + def down + add_concurrent_index('members', :id, where: query_condition, name: TMP_INDEX_NAME) + end + + private + + def query_condition + 'invite_token IS NULL and invite_accepted_at IS NOT NULL AND user_id IS NULL' + end +end diff --git a/db/schema_migrations/20220830051704 b/db/schema_migrations/20220830051704 new file mode 100644 index 0000000000000000000000000000000000000000..5785862da4fdae9cffed9e74e6396744a56592b2 --- /dev/null +++ b/db/schema_migrations/20220830051704 @@ -0,0 +1 @@ +aa0b767ad0e38500e0eef83d5c8306054952363166f8cc2076ce48feeac1b0e1 \ No newline at end of file diff --git a/db/schema_migrations/20220830061704 b/db/schema_migrations/20220830061704 new file mode 100644 index 0000000000000000000000000000000000000000..7a0db1acc651ddb92f7673a8727aea28ede9a042 --- /dev/null +++ b/db/schema_migrations/20220830061704 @@ -0,0 +1 @@ +badc3556e1dea545bbf8b55fb33065f45598df9b3fda74bffd28e89d7485e0b4 \ No newline at end of file diff --git a/db/schema_migrations/20220830071704 b/db/schema_migrations/20220830071704 new file mode 100644 index 0000000000000000000000000000000000000000..bc9d7fd0f8b473b01382aac023f42cf5db269def --- /dev/null +++ b/db/schema_migrations/20220830071704 @@ -0,0 +1 @@ +85e401f0920c6eb13b6756f191ccdf70494ca40f8133f05bbd5f23ba295b115d \ No newline at end of file diff --git a/spec/migrations/orphaned_invited_members_cleanup_spec.rb b/spec/migrations/orphaned_invited_members_cleanup_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4427e707f56b9a79cede4510954452444dedb92e --- /dev/null +++ b/spec/migrations/orphaned_invited_members_cleanup_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe OrphanedInvitedMembersCleanup, :migration do + describe '#up', :aggregate_failures do + it 'removes accepted members with no associated user' do + user = create_user!('testuser1') + + create_member(invite_token: nil, invite_accepted_at: 1.day.ago) + record2 = create_member(invite_token: nil, invite_accepted_at: 1.day.ago, user_id: user.id) + record3 = create_member(invite_token: 'foo2', invite_accepted_at: nil) + record4 = create_member(invite_token: 'foo3', invite_accepted_at: 1.day.ago) + + migrate! + + expect(table(:members).all.pluck(:id)).to match_array([record2.id, record3.id, record4.id]) + end + end + + private + + def create_user!(name) + email = "#{name}@example.com" + + table(:users).create!( + name: name, + email: email, + username: name, + projects_limit: 0 + ) + end + + def create_member(**extra_attributes) + defaults = { + access_level: 10, + source_id: 1, + source_type: "Project", + notification_level: 0, + type: 'ProjectMember' + } + + table(:members).create!(defaults.merge(extra_attributes)) + end +end