20180528141303_fix_accounts_unique_index.rb 4.3 KB

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