20230803082451_add_unique_index_on_preview_cards_statuses.rb 1.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
  1. # frozen_string_literal: true
  2. class AddUniqueIndexOnPreviewCardsStatuses < ActiveRecord::Migration[6.1]
  3. disable_ddl_transaction!
  4. def up
  5. add_index :preview_cards_statuses, [:status_id, :preview_card_id], name: :preview_cards_statuses_pkey, algorithm: :concurrently, unique: true
  6. rescue ActiveRecord::RecordNotUnique
  7. deduplicate_and_reindex!
  8. end
  9. def down
  10. remove_index :preview_cards_statuses, name: :preview_cards_statuses_pkey
  11. end
  12. private
  13. def supports_concurrent_reindex?
  14. @supports_concurrent_reindex ||= begin
  15. version = select_one("SELECT current_setting('server_version_num') AS v")['v'].to_i
  16. version >= 120_000
  17. end
  18. end
  19. def deduplicate_and_reindex!
  20. deduplicate_preview_cards!
  21. if supports_concurrent_reindex?
  22. safety_assured { execute 'REINDEX INDEX CONCURRENTLY preview_cards_statuses_pkey' }
  23. else
  24. remove_index :preview_cards_statuses, name: :preview_cards_statuses_pkey
  25. add_index :preview_cards_statuses, [:status_id, :preview_card_id], name: :preview_cards_statuses_pkey, algorithm: :concurrently, unique: true
  26. end
  27. rescue ActiveRecord::RecordNotUnique
  28. retry
  29. end
  30. def deduplicate_preview_cards!
  31. # Statuses should have only one preview card at most, even if that's not the database
  32. # constraint we will end up with
  33. duplicate_ids = select_all('SELECT status_id FROM preview_cards_statuses GROUP BY status_id HAVING count(*) > 1;').rows
  34. duplicate_ids.each_slice(1000) do |ids|
  35. # This one is tricky: since we don't have primary keys to keep only one record,
  36. # use the physical `ctid`
  37. safety_assured do
  38. execute "DELETE FROM preview_cards_statuses p WHERE p.status_id IN (#{ids.join(', ')}) AND p.ctid NOT IN (SELECT q.ctid FROM preview_cards_statuses q WHERE q.status_id = p.status_id LIMIT 1)"
  39. end
  40. end
  41. end
  42. end