~cytrogen/masto-fe

6b896b20cc87f9665ded7ed36d1ace32438ca9e3 — Claire 2 years ago a0fad5c
Add primary key to preview_cards_statuses join table (includes deduplication migration) (#25243)

M app/services/fetch_link_card_service.rb => app/services/fetch_link_card_service.rb +7 -3
@@ 61,9 61,13 @@ class FetchLinkCardService < BaseService
  end

  def attach_card
    @status.preview_cards << @card
    Rails.cache.delete(@status)
    Trends.links.register(@status)
    with_redis_lock("attach_card:#{@status.id}") do
      return if @status.preview_cards.any?

      @status.preview_cards << @card
      Rails.cache.delete(@status)
      Trends.links.register(@status)
    end
  end

  def parse_urls

A db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb => db/post_migrate/20230803082451_add_unique_index_on_preview_cards_statuses.rb +39 -0
@@ 0,0 1,39 @@
# frozen_string_literal: true

class AddUniqueIndexOnPreviewCardsStatuses < ActiveRecord::Migration[6.1]
  disable_ddl_transaction!

  def up
    add_index :preview_cards_statuses, [:status_id, :preview_card_id], name: :preview_cards_statuses_pkey, algorithm: :concurrently, unique: true
  rescue ActiveRecord::RecordNotUnique
    deduplicate_and_reindex!
  end

  def down
    remove_index :preview_cards_statuses, name: :preview_cards_statuses_pkey
  end

  private

  def deduplicate_and_reindex!
    deduplicate_preview_cards!

    safety_assured { execute 'REINDEX INDEX preview_cards_statuses_pkey' }
  rescue ActiveRecord::RecordNotUnique
    retry
  end

  def deduplicate_preview_cards!
    # Statuses should have only one preview card at most, even if that's not the database
    # constraint we will end up with
    duplicate_ids = select_all('SELECT status_id FROM preview_cards_statuses GROUP BY status_id HAVING count(*) > 1;').rows

    duplicate_ids.each_slice(1000) do |ids|
      # This one is tricky: since we don't have primary keys to keep only one record,
      # use the physical `ctid`
      safety_assured do
        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)"
      end
    end
  end
end

A db/post_migrate/20230803112520_add_primary_key_to_preview_cards_statuses_join_table.rb => db/post_migrate/20230803112520_add_primary_key_to_preview_cards_statuses_join_table.rb +20 -0
@@ 0,0 1,20 @@
# frozen_string_literal: true

class AddPrimaryKeyToPreviewCardsStatusesJoinTable < ActiveRecord::Migration[6.1]
  disable_ddl_transaction!

  def up
    safety_assured do
      execute 'ALTER TABLE preview_cards_statuses ADD PRIMARY KEY USING INDEX preview_cards_statuses_pkey'
    end
  end

  def down
    safety_assured do
      # I have found no way to demote the primary key to an index, instead, re-create the index
      execute 'CREATE UNIQUE INDEX CONCURRENTLY preview_cards_statuses_pkey_tmp ON preview_cards_statuses (status_id, preview_card_id)'
      execute 'ALTER TABLE preview_cards_statuses DROP CONSTRAINT preview_cards_statuses_pkey'
      execute 'ALTER INDEX preview_cards_statuses_pkey_tmp RENAME TO preview_cards_statuses_pkey'
    end
  end
end

M db/schema.rb => db/schema.rb +2 -2
@@ 10,7 10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2023_07_24_160715) do
ActiveRecord::Schema[7.0].define(version: 2023_08_03_112520) do
  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"



@@ 805,7 805,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_24_160715) do
    t.index ["url"], name: "index_preview_cards_on_url", unique: true
  end

  create_table "preview_cards_statuses", id: false, force: :cascade do |t|
  create_table "preview_cards_statuses", primary_key: ["status_id", "preview_card_id"], force: :cascade do |t|
    t.bigint "preview_card_id", null: false
    t.bigint "status_id", null: false
    t.index ["status_id", "preview_card_id"], name: "index_preview_cards_statuses_on_status_id_and_preview_card_id"

M lib/tasks/tests.rake => lib/tasks/tests.rake +25 -0
@@ 63,6 63,11 @@ namespace :tests do
        puts 'Account domains not properly normalized'
        exit(1)
      end

      unless Status.find(12).preview_cards.pluck(:url) == ['https://joinmastodon.org/']
        puts 'Preview cards not deduplicated as expected'
        exit(1)
      end
    end

    desc 'Populate the database with test data for 2.4.3'


@@ 238,6 243,11 @@ namespace :tests do
          (10, 2, '@admin hey!', NULL, 1, 3, now(), now()),
          (11, 1, '@user hey!', 10, 1, 3, now(), now());

        INSERT INTO "statuses"
          (id, account_id, text, created_at, updated_at)
        VALUES
          (12, 1, 'check out https://joinmastodon.org/', now(), now());

        -- mentions (from previous statuses)

        INSERT INTO "mentions"


@@ 326,6 336,21 @@ namespace :tests do
          (1, 6, 2, 'Follow', 2, now(), now()),
          (2, 2, 1, 'Mention', 4, now(), now()),
          (3, 1, 2, 'Mention', 5, now(), now());

        -- preview cards

        INSERT INTO "preview_cards"
          (id, url, title, created_at, updated_at)
        VALUES
          (1, 'https://joinmastodon.org/', 'Mastodon - Decentralized social media', now(), now());

        -- many-to-many association between preview cards and statuses

        INSERT INTO "preview_cards_statuses"
          (status_id, preview_card_id)
        VALUES
          (12, 1),
          (12, 1);
      SQL
    end
  end