From 759fcb65ee642d791d79c82293e108f3c635c94e Mon Sep 17 00:00:00 2001 From: Andy Klimczak Date: Sun, 10 Jun 2018 13:15:42 -0400 Subject: [PATCH] Fix `SystemStackError` when paranoid models have a circular `dependent: :destroy` - Previously, paranoid models with a circular `dependent: :destroy` would recursively try to delete eachother - Add a variable to check if the destroy callback has already been called for that model - Similar fix to rails/rails#18548 --- lib/paranoia/active_record_5_2.rb | 6 ++++++ test/paranoia_test.rb | 28 ++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/paranoia/active_record_5_2.rb b/lib/paranoia/active_record_5_2.rb index c9c5463f..f28202d5 100644 --- a/lib/paranoia/active_record_5_2.rb +++ b/lib/paranoia/active_record_5_2.rb @@ -4,6 +4,9 @@ def handle_dependency case options[:dependent] when :destroy + @_destroy_callback_already_called ||= false + return if @_destroy_callback_already_called + @_destroy_callback_already_called = true target.destroy if target.respond_to?(:paranoia_destroyed?) raise ActiveRecord::Rollback unless target.paranoia_destroyed? @@ -23,7 +26,10 @@ def delete(method = options[:dependent]) when :delete target.delete when :destroy + @_destroy_callback_already_called ||= false + return if @_destroy_callback_already_called target.destroyed_by_association = reflection + @_destroy_callback_already_called = true target.destroy if target.respond_to?(:paranoia_destroyed?) throw(:abort) unless target.paranoia_destroyed? diff --git a/test/paranoia_test.rb b/test/paranoia_test.rb index e08f9a7b..66f4c54f 100644 --- a/test/paranoia_test.rb +++ b/test/paranoia_test.rb @@ -46,8 +46,10 @@ def setup! 'active_column_models' => 'deleted_at DATETIME, active BOOLEAN', 'active_column_model_with_uniqueness_validations' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 'paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN, active_column_model_with_has_many_relationship_id INTEGER', - 'active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', - 'without_default_scope_models' => 'deleted_at DATETIME' + 'active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', + 'without_default_scope_models' => 'deleted_at DATETIME', + 'paranoid_has_one_dependent_destroys' => 'deleted_at DATETIME', + 'paranoid_belongs_to_dependent_destroys' => 'deleted_at DATETIME, paranoid_has_one_dependent_destroy_id INTEGER' }.each do |table_name, columns_as_sql_string| ActiveRecord::Base.connection.execute "CREATE TABLE #{table_name} (id INTEGER NOT NULL PRIMARY KEY, #{columns_as_sql_string})" end @@ -1053,6 +1055,18 @@ def test_counter_cache_column_on_restore related_model.restore assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count end + + def test_circular_dependent_destroy_has_one_belongs_to + has_one_dependent_destroy = ParanoidHasOneDependentDestroy.create + belongs_to_dependent_destroy = ParanoidBelongsToDependentDestroy.create + belongs_to_dependent_destroy.paranoid_has_one_dependent_destroy = has_one_dependent_destroy + belongs_to_dependent_destroy.save! + + belongs_to_dependent_destroy.destroy + + assert_equal false, has_one_dependent_destroy.reload.deleted_at.nil? + assert_equal false, belongs_to_dependent_destroy.reload.deleted_at.nil? + end end private @@ -1356,6 +1370,16 @@ class PolymorphicModel < ActiveRecord::Base belongs_to :parent, polymorphic: true end +class ParanoidHasOneDependentDestroy < ActiveRecord::Base + acts_as_paranoid + has_one :paranoid_belongs_to_dependent_destroy, dependent: :destroy +end + +class ParanoidBelongsToDependentDestroy < ActiveRecord::Base + acts_as_paranoid + belongs_to :paranoid_has_one_dependent_destroy, dependent: :destroy +end + module Namespaced def self.table_name_prefix "namespaced_"