Skip to content

Commit

Permalink
Fix SystemStackError when paranoid models have a circular `dependen…
Browse files Browse the repository at this point in the history
…t: :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
  • Loading branch information
andyklimczak committed Aug 23, 2019
1 parent 1434cc6 commit a1613ae
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 9 deletions.
6 changes: 2 additions & 4 deletions lib/paranoia.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
require 'active_record' unless defined? ActiveRecord

if [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR] == [5, 2]
require 'paranoia/active_record_5_2'
end
require 'paranoia/active_record_5_2'

module Paranoia
@@default_sentinel_value = nil
Expand Down Expand Up @@ -305,7 +303,7 @@ def build_relation(klass, *args)
class UniquenessValidator < ActiveModel::EachValidator
prepend UniquenessParanoiaValidator
end

class AssociationNotSoftDestroyedValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# if association is soft destroyed, add an error
Expand Down
6 changes: 6 additions & 0 deletions lib/paranoia/active_record_5_2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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?
Expand Down
34 changes: 29 additions & 5 deletions test/paranoia_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -189,11 +191,11 @@ def test_scoping_behavior_for_paranoid_models
p2 = ParanoidModel.create(:parent_model => parent2)
p1.destroy
p2.destroy

assert_equal 0, parent1.paranoid_models.count
assert_equal 1, parent1.paranoid_models.only_deleted.count

assert_equal 2, ParanoidModel.only_deleted.joins(:parent_model).count
assert_equal 2, ParanoidModel.only_deleted.joins(:parent_model).count
assert_equal 1, parent1.paranoid_models.deleted.count
assert_equal 0, parent1.paranoid_models.without_deleted.count
p3 = ParanoidModel.create(:parent_model => parent1)
Expand All @@ -206,7 +208,7 @@ def test_only_deleted_with_joins
c1 = ActiveColumnModelWithHasManyRelationship.create(name: 'Jacky')
c2 = ActiveColumnModelWithHasManyRelationship.create(name: 'Thomas')
p1 = ParanoidModelWithBelongsToActiveColumnModelWithHasManyRelationship.create(name: 'Hello', active_column_model_with_has_many_relationship: c1)

c1.destroy
assert_equal 1, ActiveColumnModelWithHasManyRelationship.count
assert_equal 1, ActiveColumnModelWithHasManyRelationship.only_deleted.count
Expand Down Expand Up @@ -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

refute has_one_dependent_destroy.reload.deleted_at.nil?
refute belongs_to_dependent_destroy.reload.deleted_at.nil?
end
end

private
Expand Down Expand Up @@ -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_"
Expand Down

0 comments on commit a1613ae

Please sign in to comment.