diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 85aae3f1d..d598a00e5 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -145,6 +145,29 @@ class ApplicationRecord < ActiveRecord::Base def update!(*args) all.each { |record| record.update!(*args) } end + + def each_duplicate(*columns) + return enum_for(:each_duplicate, *columns) unless block_given? + + group(columns).having("count(*) > 1").count.each do |values, count| + hash = columns.zip(Array.wrap(values)).to_h + yield count: count, **hash + end + end + + def destroy_duplicates!(*columns, log: true) + each_duplicate(*columns) do |count:, **columns_with_values| + records = where(columns_with_values).order(:id) + dupes = records.drop(1) + + if log + data = { keep: records.first.id, destroy: dupes.map(&:id), count: count, **columns_with_values } + DanbooruLogger.info("Destroying duplicate #{self.name} #{dupes.map(&:id).join(", ")}", data) + end + + dupes.each(&:destroy!) + end + end end end diff --git a/db/migrate/20211013011619_add_unique_user_id_and_post_id_index_to_post_disapprovals.rb b/db/migrate/20211013011619_add_unique_user_id_and_post_id_index_to_post_disapprovals.rb new file mode 100644 index 000000000..fa3c2bc39 --- /dev/null +++ b/db/migrate/20211013011619_add_unique_user_id_and_post_id_index_to_post_disapprovals.rb @@ -0,0 +1,5 @@ +class AddUniqueUserIdAndPostIdIndexToPostDisapprovals < ActiveRecord::Migration[6.1] + def change + add_index :post_disapprovals, [:user_id, :post_id], unique: true + end +end diff --git a/db/structure.sql b/db/structure.sql index e6204333a..5b3c81d0d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -4096,6 +4096,13 @@ CREATE INDEX index_post_disapprovals_on_post_id ON public.post_disapprovals USIN CREATE INDEX index_post_disapprovals_on_user_id ON public.post_disapprovals USING btree (user_id); +-- +-- Name: index_post_disapprovals_on_user_id_and_post_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_post_disapprovals_on_user_id_and_post_id ON public.post_disapprovals USING btree (user_id, post_id); + + -- -- Name: index_post_flags_on_creator_id; Type: INDEX; Schema: public; Owner: - -- @@ -5060,6 +5067,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20210926125826'), ('20211008091234'), ('20211010181657'), -('20211011044400'); +('20211011044400'), +('20211013011619'); diff --git a/script/fixes/081_delete_duplicate_post_disapprovals.rb b/script/fixes/081_delete_duplicate_post_disapprovals.rb new file mode 100755 index 000000000..93231ed6c --- /dev/null +++ b/script/fixes/081_delete_duplicate_post_disapprovals.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +require_relative "../../config/environment" + +PostDisapproval.transaction do + PostDisapproval.destroy_duplicates!(:user_id, :post_id) +end diff --git a/test/unit/application_record.rb b/test/unit/application_record.rb index 60355d482..5f36f6cc8 100644 --- a/test/unit/application_record.rb +++ b/test/unit/application_record.rb @@ -45,4 +45,20 @@ class ApplicationRecordTest < ActiveSupport::TestCase end end end + + context "ApplicationRecord#destroy_duplicates!" do + should "destroy all duplicates" do + @post1 = create(:post, score: 42) + @post2 = create(:post, score: 42) + @post3 = create(:post, score: 42) + @post4 = create(:post, score: 23) + + Post.destroy_duplicates!(:score) + + assert_equal(true, Post.exists?(@post1.id)) + assert_equal(false, Post.exists?(@post2.id)) + assert_equal(false, Post.exists?(@post3.id)) + assert_equal(true, Post.exists?(@post4.id)) + end + end end