From 9a27ca8b421c3803c837e908abc786931a89bd0d Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Thu, 16 Apr 2026 15:43:55 -0400 Subject: [PATCH] Scope clear_search_records to the account being destroyed (#2847) Account::Searchable#clear_search_records called Search::Record.for(id).destroy_all which deleted every row in the shard's table, wiping search records belonging to every other account that happened to hash to the same shard. ref: https://3.basecamp.com/2914079/buckets/27/card_tables/cards/9782824728 --- app/models/account/searchable.rb | 2 +- test/models/account/incineratable_test.rb | 41 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/app/models/account/searchable.rb b/app/models/account/searchable.rb index b06a44ed3..5e5cc3c59 100644 --- a/app/models/account/searchable.rb +++ b/app/models/account/searchable.rb @@ -9,6 +9,6 @@ module Account::Searchable private def clear_search_records - Search::Record.for(id).destroy_all + Search::Record.for(id).where(account_id: id).destroy_all end end diff --git a/test/models/account/incineratable_test.rb b/test/models/account/incineratable_test.rb index 7f69bed0b..cb6e6560b 100644 --- a/test/models/account/incineratable_test.rb +++ b/test/models/account/incineratable_test.rb @@ -176,4 +176,45 @@ class Account::IncineratableTest < ActiveSupport::TestCase # Search records (sharded) assert_empty Search::Record.for(account_id).where(account_id: account_id) end + + test "incinerating an account clears its own search records but preserves others on the same shard" do + doomed = accounts(:initech) + shard = Search::Record.for(doomed.id) + + doomed_card = cards(:radio) + doomed_record = shard.create!( + account_id: doomed_card.account_id, + searchable_type: "Card", + searchable_id: doomed_card.id, + card_id: doomed_card.id, + board_id: doomed_card.board_id, + title: "doomed", + content: "should be destroyed", + created_at: Time.current + ) + + # Simulate a shard collision: plant a record belonging to a different + # account directly on doomed's shard, as would happen in production when + # two accounts' UUIDs hash to the same shard. + foreign_card = cards(:logo) + foreign_record = shard.create!( + account_id: foreign_card.account_id, + searchable_type: "Card", + searchable_id: foreign_card.id, + card_id: foreign_card.id, + board_id: foreign_card.board_id, + title: "foreign", + content: "should survive", + created_at: Time.current + ) + + Current.account = doomed + Current.session = Session.new(identity: identities(:mike)) + doomed.incinerate + + assert_not shard.exists?(id: doomed_record.id), + "the incinerated account's search record should be destroyed" + assert shard.exists?(id: foreign_record.id), + "another account's search record on the same shard should survive" + end end