diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3131be4..dd920c3f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,40 +15,30 @@ jobs: strategy: fail-fast: false matrix: - ruby: - - 3.1 - - '3.0' - - 2.7 - - 2.6 - - 2.5 - # - jruby-9.2.19.0 - # - jruby-9.3.1.0 - rails: - - '~> 5.1.0' - - '~> 5.2.0' - - '~> 6.0.0' - - '~> 6.1.0' - - '~> 7.0.0' - - 'edge' - exclude: - # Rails edge is now 7.x and requires ruby 2.7 - - rails: 'edge' - ruby: 2.6 - - rails: 'edge' - ruby: 2.5 - - rails: '~> 7.0.0' - ruby: 2.6 - - rails: '~> 7.0.0' - ruby: 2.5 - # Legacy Rails with newer rubies - - rails: '~> 5.1.0' - ruby: '3.0' - - rails: '~> 5.2.0' - ruby: '3.0' - - rails: '~> 5.1.0' - ruby: 3.1 - - rails: '~> 5.2.0' - ruby: 3.1 + rails: ["~> 7.0.0", "~> 6.1.0", "~> 6.0.0"] + ruby: ["3.2.2", "3.1.4", "3.0.6", "2.7.8"] + include: + - ruby: 3.2 + rails: 'edge' + # single test failure with jruby + #- ruby: jruby-9.4 + # rails: '~> 7.0.0' + - ruby: 2.6 + rails: '~> 6.1.0' + - ruby: 2.6 + rails: '~> 6.0.0' + - ruby: 2.6 + rails: '~> 5.2.0' + - ruby: 2.6 + rails: '~> 5.1.0' + - ruby: 2.5 + rails: '~> 6.0.0' + - ruby: 2.5 + rails: '~> 5.2.0' + - ruby: 2.5 + rails: '~> 5.1.0' + #os: ubuntu-latest + #arch: x64 env: RAILS: ${{ matrix.rails }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d169db..5e93e080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # paranoia Changelog +## 2.6.2 + +* [#441](https://github.com/rubysherpas/paranoia/pull/441) Recursive restore with has_many/one through assocs (#441) + [Emil Ong](https://github.com/emilong) + +## 2.6.1 + +* [#535](https://github.com/rubysherpas/paranoia/pull/535) Allow to skip updating paranoia_destroy_attributes for records while really_destroy! + [Anton Bogdanov](https://github.com/kortirso) + +## 2.6.0 + +* [#512](https://github.com/rubysherpas/paranoia/pull/512) Quote table names; Mysql 8 has keywords that might match table names which cause an exception. +* [#476](https://github.com/rubysherpas/paranoia/pull/476) Fix syntax error in documentation. +* [#485](https://github.com/rubysherpas/paranoia/pull/485) Rollback transaction if destroy aborted. +* [#522](https://github.com/rubysherpas/paranoia/pull/522) Add failing tests for association with abort on destroy. +* [#513](https://github.com/rubysherpas/paranoia/pull/513) Fix create callback called on destroy. + ## 2.5.3 * [#532](https://github.com/rubysherpas/paranoia/pull/532) Fix: correct bug when sentinel_value is not a timestamp diff --git a/README.md b/README.md index cc6746e8..cf898b8f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Gem Version](https://badge.fury.io/rb/paranoia.svg)](https://badge.fury.io/rb/paranoia) [![build](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml/badge.svg)](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml) -**Notice:** +**Notice:** `paranoia` has some surprising behaviour (like overriding ActiveRecord's `delete` and `destroy`) and is not recommended for new projects. See [`discard`'s README](https://github.com/jhawthorn/discard#why-not-paranoia-or-acts_as_paranoid) for more details. @@ -103,6 +103,14 @@ If you really want it gone *gone*, call `really_destroy!`: # => client ``` +If you need skip updating timestamps for deleting records, call `really_destroy!(update_destroy_attributes: false)`. +When we call `really_destroy!(update_destroy_attributes: false)` on the parent `client`, then each child `email` will also have `really_destroy!(update_destroy_attributes: false)` called. + +``` ruby +>> client.really_destroy!(update_destroy_attributes: false) +# => client +``` + If you want to use a column other than `deleted_at`, you can pass it as an option: ``` ruby diff --git a/lib/paranoia.rb b/lib/paranoia.rb index 959e5534..6fc464a5 100644 --- a/lib/paranoia.rb +++ b/lib/paranoia.rb @@ -40,7 +40,7 @@ def only_deleted # these will not match != sentinel value because "NULL != value" is # NULL under the sql standard # Scoping with the table_name is mandatory to avoid ambiguous errors when joining tables. - scoped_quoted_paranoia_column = "#{self.table_name}.#{connection.quote_column_name(paranoia_column)}" + scoped_quoted_paranoia_column = "#{connection.quote_table_name(self.table_name)}.#{connection.quote_column_name(paranoia_column)}" with_deleted.where("#{scoped_quoted_paranoia_column} IS NULL OR #{scoped_quoted_paranoia_column} != ?", paranoia_sentinel_value) end alias_method :deleted, :only_deleted @@ -144,7 +144,7 @@ def paranoia_destroyed? end alias :deleted? :paranoia_destroyed? - def really_destroy! + def really_destroy!(update_destroy_attributes: true) with_transaction_returning_status do run_callbacks(:real_destroy) do @_disable_counter_cache = paranoia_destroyed? @@ -158,12 +158,14 @@ def really_destroy! # .paranoid? will work for both instances and classes next unless association_data && association_data.paranoid? if reflection.collection? - next association_data.with_deleted.each(&:really_destroy!) + next association_data.with_deleted.find_each { |record| + record.really_destroy!(update_destroy_attributes: update_destroy_attributes) + } end - association_data.really_destroy! + association_data.really_destroy!(update_destroy_attributes: update_destroy_attributes) end end - update_columns(paranoia_destroy_attributes) + update_columns(paranoia_destroy_attributes) if update_destroy_attributes destroy_without_paranoia end end @@ -215,7 +217,12 @@ def restore_associated_records(recovery_window_range = nil) if association_data.nil? && association.macro.to_s == "has_one" association_class_name = association.klass.name - association_foreign_key = association.foreign_key + + association_foreign_key = if association.options[:through].present? + association.klass.primary_key + else + association.foreign_key + end if association.type association_polymorphic_type = association.type @@ -224,7 +231,7 @@ def restore_associated_records(recovery_window_range = nil) association_find_conditions = { association_foreign_key => self.id } end - association_class = association_class_name.constantize + association_class = association.klass if association_class.paranoid? association_class.only_deleted.where(association_find_conditions).first .try!(:restore, recursive: true, :recovery_window_range => recovery_window_range) diff --git a/lib/paranoia/version.rb b/lib/paranoia/version.rb index 946e0abb..afe1017d 100644 --- a/lib/paranoia/version.rb +++ b/lib/paranoia/version.rb @@ -1,3 +1,3 @@ module Paranoia - VERSION = '2.5.3'.freeze + VERSION = '2.6.2'.freeze end diff --git a/test/paranoia_test.rb b/test/paranoia_test.rb index c6c064ca..e302f6b8 100644 --- a/test/paranoia_test.rb +++ b/test/paranoia_test.rb @@ -49,7 +49,11 @@ def setup! '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' + 'without_default_scope_models' => 'deleted_at DATETIME', + 'paranoid_has_through_restore_parents' => 'deleted_at DATETIME', + 'empty_paranoid_models' => 'deleted_at DATETIME', + 'paranoid_has_one_throughs' => 'paranoid_has_through_restore_parent_id INTEGER NOT NULL, empty_paranoid_model_id INTEGER NOT NULL, deleted_at DATETIME', + 'paranoid_has_many_throughs' => 'paranoid_has_through_restore_parent_id INTEGER NOT NULL, empty_paranoid_model_id INTEGER NOT NULL, deleted_at DATETIME', }.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 @@ -387,14 +391,22 @@ def test_active_column_model_with_uniqueness_validation_still_works_on_non_delet end def test_sentinel_value_for_custom_sentinel_models + time_zero = if ActiveRecord::VERSION::MAJOR < 6 + Time.new(0) + elsif ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR < 1 + Time.new(0) + else + DateTime.new(0) + end + model = CustomSentinelModel.new assert_equal 0, model.class.count model.save! - assert_equal DateTime.new(0), model.deleted_at + assert_equal time_zero, model.deleted_at assert_equal 1, model.class.count model.destroy - assert DateTime.new(0) != model.deleted_at + assert time_zero != model.deleted_at assert model.paranoia_destroyed? assert_equal 0, model.class.count @@ -403,7 +415,7 @@ def test_sentinel_value_for_custom_sentinel_models assert_equal 1, model.class.deleted.count model.restore - assert_equal DateTime.new(0), model.deleted_at + assert_equal time_zero, model.deleted_at assert !model.destroyed? assert_equal 1, model.class.count @@ -1055,6 +1067,40 @@ def test_restore_recursive_on_polymorphic_has_one_association assert_equal 1, polymorphic.class.count end + def test_recursive_restore_with_has_through_associations + parent = ParanoidHasThroughRestoreParent.create + one = EmptyParanoidModel.create + ParanoidHasOneThrough.create( + :paranoid_has_through_restore_parent => parent, + :empty_paranoid_model => one, + ) + many = Array.new(3) do + many = EmptyParanoidModel.create + ParanoidHasManyThrough.create( + :paranoid_has_through_restore_parent => parent, + :empty_paranoid_model => many, + ) + + many + end + + assert_equal true, parent.empty_paranoid_model.present? + assert_equal 3, parent.empty_paranoid_models.count + + parent.destroy + + assert_equal true, parent.empty_paranoid_model.reload.deleted? + assert_equal 0, parent.empty_paranoid_models.count + + parent = ParanoidHasThroughRestoreParent.with_deleted.first + parent.restore(recursive: true) + + assert_equal false, parent.empty_paranoid_model.deleted? + assert_equal one, parent.empty_paranoid_model + assert_equal 3, parent.empty_paranoid_models.count + assert_equal many, parent.empty_paranoid_models + end + # Ensure that we're checking parent_type when restoring def test_missing_restore_recursive_on_polymorphic_has_one_association parent = ParentModel.create @@ -1555,3 +1601,29 @@ class ParanoidBelongsTo < ActiveRecord::Base belongs_to :paranoid_has_one end end + +class ParanoidHasThroughRestoreParent < ActiveRecord::Base + acts_as_paranoid + + has_one :paranoid_has_one_through, dependent: :destroy + has_one :empty_paranoid_model, through: :paranoid_has_one_through, dependent: :destroy + + has_many :paranoid_has_many_throughs, dependent: :destroy + has_many :empty_paranoid_models, through: :paranoid_has_many_throughs, dependent: :destroy +end + +class EmptyParanoidModel < ActiveRecord::Base + acts_as_paranoid +end + +class ParanoidHasOneThrough < ActiveRecord::Base + acts_as_paranoid + belongs_to :paranoid_has_through_restore_parent + belongs_to :empty_paranoid_model, dependent: :destroy +end + +class ParanoidHasManyThrough < ActiveRecord::Base + acts_as_paranoid + belongs_to :paranoid_has_through_restore_parent + belongs_to :empty_paranoid_model, dependent: :destroy +end