20180528141303_fix_accounts_unique_index.rb 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2]
  2. class Account < ApplicationRecord
  3. # Dummy class, to make migration possible across version changes
  4. has_one :user, inverse_of: :account
  5. def local?
  6. domain.nil?
  7. end
  8. def acct
  9. local? ? username : "#{username}@#{domain}"
  10. end
  11. end
  12. class StreamEntry < ApplicationRecord
  13. # Dummy class, to make migration possible across version changes
  14. belongs_to :account, inverse_of: :stream_entries
  15. end
  16. class Status < ApplicationRecord
  17. # Dummy class, to make migration possible across version changes
  18. belongs_to :account
  19. end
  20. class Mention < ApplicationRecord
  21. # Dummy class, to make migration possible across version changes
  22. belongs_to :account
  23. end
  24. class StatusPin < ApplicationRecord
  25. # Dummy class, to make migration possible across version changes
  26. belongs_to :account
  27. end
  28. disable_ddl_transaction!
  29. def up
  30. if $stdout.isatty
  31. say ''
  32. say 'WARNING: This migration may take a *long* time for large instances'
  33. say 'It will *not* lock tables for any significant time, but it may run'
  34. say 'for a very long time. We will pause for 10 seconds to allow you to'
  35. say 'interrupt this migration if you are not ready.'
  36. say ''
  37. say 'This migration will irreversibly delete user accounts with duplicate'
  38. say 'usernames. You may use the `rake mastodon:maintenance:find_duplicate_usernames`'
  39. say 'task to manually deal with such accounts before running this migration.'
  40. 10.downto(1) do |i|
  41. say "Continuing in #{i} second#{i == 1 ? '' : 's'}...", true
  42. sleep 1
  43. end
  44. end
  45. duplicates = Account.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1').to_ary
  46. duplicates.each do |row|
  47. deduplicate_account!(row['ids'].split(','))
  48. end
  49. remove_index :accounts, name: 'index_accounts_on_username_and_domain_lower' if index_name_exists?(:accounts, 'index_accounts_on_username_and_domain_lower')
  50. safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_accounts_on_username_and_domain_lower ON accounts (lower(username), lower(domain))' }
  51. remove_index :accounts, name: 'index_accounts_on_username_and_domain' if index_name_exists?(:accounts, 'index_accounts_on_username_and_domain')
  52. end
  53. def down
  54. raise ActiveRecord::IrreversibleMigration
  55. end
  56. private
  57. def deduplicate_account!(account_ids)
  58. accounts = Account.where(id: account_ids).to_a
  59. accounts = accounts.first.local? ? accounts.sort_by(&:created_at) : accounts.sort_by(&:updated_at).reverse
  60. reference_account = accounts.shift
  61. say_with_time "Deduplicating @#{reference_account.acct} (#{accounts.size} duplicates)..." do
  62. accounts.each do |other_account|
  63. if other_account.public_key == reference_account.public_key
  64. # The accounts definitely point to the same resource, so
  65. # it's safe to re-attribute content and relationships
  66. merge_accounts!(reference_account, other_account)
  67. elsif other_account.local?
  68. # Since domain is in the GROUP BY clause, both accounts
  69. # are always either going to be local or not local, so only
  70. # one check is needed. Since we cannot support two users with
  71. # the same username locally, one has to go. 😢
  72. other_account.user&.destroy
  73. end
  74. other_account.destroy
  75. end
  76. end
  77. end
  78. def merge_accounts!(main_account, duplicate_account)
  79. [Status, Mention, StatusPin, StreamEntry].each do |klass|
  80. klass.where(account_id: duplicate_account.id).in_batches.update_all(account_id: main_account.id)
  81. end
  82. # Since it's the same remote resource, the remote resource likely
  83. # already believes we are following/blocking, so it's safe to
  84. # re-attribute the relationships too. However, during the presence
  85. # of the index bug users could have *also* followed the reference
  86. # account already, therefore mass update will not work and we need
  87. # to check for (and skip past) uniqueness errors
  88. [Favourite, Follow, FollowRequest, Block, Mute].each do |klass|
  89. klass.where(account_id: duplicate_account.id).find_each do |record|
  90. begin
  91. record.update_attribute(:account_id, main_account.id)
  92. rescue ActiveRecord::RecordNotUnique
  93. next
  94. end
  95. end
  96. end
  97. [Follow, FollowRequest, Block, Mute].each do |klass|
  98. klass.where(target_account_id: duplicate_account.id).find_each do |record|
  99. begin
  100. record.update_attribute(:target_account_id, main_account.id)
  101. rescue ActiveRecord::RecordNotUnique
  102. next
  103. end
  104. end
  105. end
  106. end
  107. end