├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── paranoia.rb └── paranoia │ ├── active_record_5_2.rb │ ├── rspec.rb │ └── version.rb ├── paranoia.gemspec └── test └── paranoia_test.rb /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: build 9 | 10 | on: [push, pull_request] 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-20.04 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | rails: ["~> 7.2.0", "~> 7.1.0", "~> 7.0.0", "~> 6.1.0"] 19 | ruby: ["3.3","3.2", "3.1", "3.0", "2.7"] 20 | exclude: 21 | - rails: "~> 7.2.0" 22 | ruby: "3.0" 23 | - rails: "~> 7.2.0" 24 | ruby: "2.7" 25 | - rails: "edge" 26 | ruby: "3.0" 27 | - rails: "edge" 28 | ruby: "2.7" 29 | 30 | 31 | 32 | env: 33 | RAILS: ${{ matrix.rails }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: ruby/setup-ruby@v1 37 | with: 38 | ruby-version: ${{ matrix.ruby }} 39 | bundler-cache: true 40 | - run: bundle exec rake 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | tmp 5 | .rvmrc 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # paranoia Changelog 2 | 3 | ## 3.0.1 - January 19, 2025 4 | 5 | - [#566](https://github.com/rubysherpas/paranoia/pull/566) Handle #delete_all 6 | - [#559](https://github.com/rubysherpas/paranoia/pull/559) Trigger an after_commit callback when restoring a record 7 | - [#567](https://github.com/rubysherpas/paranoia/pull/567) Fix typo in newly added readme 8 | 9 | ## 3.0.0 - August 13, 2024 10 | 11 | _Tagged as 3.0 as Ruby + Rails version constraints have been modernised._ 12 | 13 | - [#564](https://github.com/rubysherpas/paranoia/pull/564) Support Rails edge 14 | - [#563](https://github.com/rubysherpas/paranoia/pull/563) Support Rails 7.2 15 | 16 | ## 2.6.4 - July 20, 2024 17 | 18 | * [#554](https://github.com/rubysherpas/paranoia/pull/554) Support prebuilt counter cache association list (#554) 19 | [Joé Dupuis](https://github.com/JoeDupuis) 20 | * [#551](https://github.com/rubysherpas/paranoia/pull/551) Fix: restore has_one with scope (#551) 21 | [Paweł Charyło](https://github.com/zygzagZ) 22 | * [#555](https://github.com/rubysherpas/paranoia/pull/555) 📝 Add Yard documentation for Paranoia::Query (#555) 23 | [Clément Prod'homme](https://github.com/cprodhomme) 24 | 25 | ## 2.6.3 - Oct 12, 2023 26 | 27 | * [#548](https://github.com/rubysherpas/paranoia/pull/548) Add support for [Rails 7.1](https://github.com/rails/rails/releases/tag/v7.1.0) (#548) 28 | [Indyarocks](https://github.com/indyarocks) 29 | 30 | ## 2.6.2 - Jun 6, 2023 31 | 32 | * [#441](https://github.com/rubysherpas/paranoia/pull/441) Recursive restore with has_many/one through assocs (#441) 33 | [Emil Ong](https://github.com/emilong) 34 | 35 | ## 2.6.1 - Nov 16, 2022 36 | 37 | * [#535](https://github.com/rubysherpas/paranoia/pull/535) Allow to skip updating paranoia_destroy_attributes for records while really_destroy! 38 | [Anton Bogdanov](https://github.com/kortirso) 39 | 40 | ## 2.6.0 - Mar 23, 2022 41 | 42 | * [#512](https://github.com/rubysherpas/paranoia/pull/512) Quote table names; Mysql 8 has keywords that might match table names which cause an exception. 43 | * [#476](https://github.com/rubysherpas/paranoia/pull/476) Fix syntax error in documentation. 44 | * [#485](https://github.com/rubysherpas/paranoia/pull/485) Rollback transaction if destroy aborted. 45 | * [#522](https://github.com/rubysherpas/paranoia/pull/522) Add failing tests for association with abort on destroy. 46 | * [#513](https://github.com/rubysherpas/paranoia/pull/513) Fix create callback called on destroy. 47 | 48 | ## 2.5.3 49 | 50 | * [#532](https://github.com/rubysherpas/paranoia/pull/532) Fix: correct bug when sentinel_value is not a timestamp 51 | [Hassanin Ahmed](https://github.com/sas1ni69) 52 | * [#531](https://github.com/rubysherpas/paranoia/pull/531) Added test case to reproduce bug introduce in v2.5.1 53 | [Sherif Elkassaby](https://github.com/sherif-nedap) 54 | * [#529](https://github.com/rubysherpas/paranoia/pull/529) Fix: Do not define a RSpec matcher when RSpec isn't present 55 | [Sebastian Welther](https://github.com/swelther) 56 | 57 | ## 2.5.2 58 | 59 | * [#526](https://github.com/rubysherpas/paranoia/pull/526) Do not include tests files in packaged gem 60 | 61 | [Jason Fleetwood-Boldt](https://github.com/jasonfb) 62 | * [#492](https://github.com/rubysherpas/paranoia/pull/492) Warn if acts_as_paranoid is called more than once on the same model 63 | 64 | [Ignatius Reza](https://github.com/ignatiusreza) 65 | 66 | ## 2.5.1 67 | 68 | * [#481](https://github.com/rubysherpas/paranoia/pull/481) Replaces hard coded `deleted_at` with `paranoia_column`. 69 | 70 | [Hassanin Ahmed](https://github.com/sas1ni69) 71 | 72 | ## 2.5.0 73 | 74 | * [#516](https://github.com/rubysherpas/paranoia/pull/516) Add support for ActiveRecord 7.0, drop support for EOL Ruby < 2.5 and Rails < 5.1 75 | adding support for Rails 7 76 | 77 | [Mathieu Jobin](https://github.com/mathieujobin) 78 | * [#515](https://github.com/rubysherpas/paranoia/pull/515) Switch from Travis CI to GitHub Actions 79 | 80 | [Shinichi Maeshima](https://github.com/willnet) 81 | 82 | ## 2.4.3 83 | 84 | * [#503](https://github.com/rubysherpas/paranoia/pull/503) Bump activerecord dependency for Rails 6.1 85 | 86 | [Jörg Schiller](https://github.com/joergschiller) 87 | 88 | * [#483](https://github.com/rubysherpas/paranoia/pull/483) Update JRuby version to 9.2.8.0 + remove EOL Ruby 2.2 89 | 90 | [Uwe Kubosch](https://github.com/donv) 91 | 92 | * [#482](https://github.com/rubysherpas/paranoia/pull/482) Fix after_commit for Rails 6 93 | 94 | [Ashwin Hegde](https://github.com/hashwin) 95 | 96 | ## 2.4.2 97 | 98 | * [#470](https://github.com/rubysherpas/paranoia/pull/470) Add support for ActiveRecord 6.0 99 | 100 | [Anton Kolodii](https://github.com/iggant), [Jared Norman](https://github.com/jarednorman) 101 | 102 | ## 2.4.1 103 | 104 | * [#435](https://github.com/rubysherpas/paranoia/pull/435) Monkeypatch activerecord relations to work with rails 5.2.0 105 | 106 | [Bartosz Bonisławski (@bbonislawski)](https://github.com/bbonislawski) 107 | 108 | ## 2.4.0 109 | 110 | * [#423](https://github.com/rubysherpas/paranoia/pull/423) Add `paranoia_destroy` and `paranoia_delete` aliases 111 | 112 | [John Hawthorn (@jhawthorn)](https://github.com/jhawthorn) 113 | 114 | * [#408](https://github.com/rubysherpas/paranoia/pull/408) Fix instance variable `@_disable_counter_cache` not initialized warning. 115 | 116 | [Akira Matsuda (@amatsuda)](https://github.com/amatsuda) 117 | 118 | * [#412](https://github.com/rubysherpas/paranoia/pull/412) Fix `really_destroy!` behavior with `sentinel_value` 119 | 120 | [Steve Rice (@steverice)](https://github.com/steverice) 121 | 122 | ## 2.3.1 123 | 124 | * [#397](https://github.com/rubysherpas/paranoia/pull/397) Bump active record max version to support 5.1 final 125 | 126 | ## 2.3.0 (2017-04-14) 127 | 128 | * [#393](https://github.com/rubysherpas/paranoia/pull/393) Drop support for Rails 4.1 and begin supporting Rails 5.1. 129 | 130 | [Miklós Fazekas (@mfazekas)](https://github.com/mfazekas) 131 | 132 | * [#391](https://github.com/rubysherpas/paranoia/pull/391) Use Contributor Covenant Version 1.4 133 | 134 | [Ben A. Morgan (@BenMorganIO)](https://github.com/BenMorganIO) 135 | 136 | * [#390](https://github.com/rubysherpas/paranoia/pull/390) Fix counter cache with double destroy, really_destroy, and restore 137 | 138 | [Chris Oliver (@excid3)](https://github.com/excid3) 139 | 140 | * [#389](https://github.com/rubysherpas/paranoia/pull/389) Added association not soft destroyed validator 141 | 142 | _Fixes [#380](https://github.com/rubysherpas/paranoia/issues/380)_ 143 | 144 | [Edward Poot (@edwardmp)](https://github.com/edwardmp) 145 | 146 | * [#383](https://github.com/rubysherpas/paranoia/pull/383) Add recovery window feature 147 | 148 | _Fixes [#359](https://github.com/rubysherpas/paranoia/issues/359)_ 149 | 150 | [Andrzej Piątyszek (@konto-andrzeja)](https://github.com/konto-andrzeja) 151 | 152 | 153 | ## 2.2.1 (2017-02-15) 154 | 155 | * [#371](https://github.com/rubysherpas/paranoia/pull/371) Use ActiveSupport.on_load to correctly re-open ActiveRecord::Base 156 | 157 | _Fixes [#335](https://github.com/rubysherpas/paranoia/issues/335) and [#381](https://github.com/rubysherpas/paranoia/issues/381)._ 158 | 159 | [Iaan Krynauw (@iaankrynauw)](https://github.com/iaankrynauw) 160 | 161 | * [#377](https://github.com/rubysherpas/paranoia/pull/377) Touch record on paranoia-destroy. 162 | 163 | _Fixes [#296](https://github.com/rubysherpas/paranoia/issues/296)._ 164 | 165 | [René (@rbr)](https://github.com/rbr) 166 | 167 | * [#379](https://github.com/rubysherpas/paranoia/pull/379) Fixes a problem of ambiguous table names when using only_deleted method. 168 | 169 | _Fixes [#26](https://github.com/rubysherpas/paranoia/issues/26) and [#27](https://github.com/rubysherpas/paranoia/pull/27)._ 170 | 171 | [Thomas Romera (@Erowlin)](https://github.com/Erowlin) 172 | 173 | ## 2.2.0 (2016-10-21) 174 | 175 | * Ruby 2.0 or greater is required 176 | * Rails 5.0.0.beta1.1 support [@pigeonworks](https://github.com/pigeonworks) [@halostatue](https://github.com/halostatue) and [@gagalago](https://github.com/gagalago) 177 | * Previously `#really_destroyed?` may have been defined on non-paranoid models, it is now only available on paranoid models, use regular `#destroyed?` instead. 178 | 179 | ## 2.1.5 (2016-01-06) 180 | 181 | * Ruby 2.3 support 182 | 183 | ## 2.1.4 184 | 185 | ## 2.1.3 186 | 187 | ## 2.1.2 188 | 189 | ## 2.1.1 190 | 191 | ## 2.1.0 (2015-01-23) 192 | 193 | ### Major changes 194 | 195 | * `#destroyed?` is no longer overridden. Use `#paranoia_destroyed?` for the existing behaviour. [Washington Luiz](https://github.com/huoxito) 196 | * `#persisted?` is no longer overridden. 197 | * ActiveRecord 4.0 no longer has `#destroy!` as an alias for `#really_destroy!`. 198 | * `#destroy` will now raise an exception if called on a readonly record. 199 | * `#destroy` on a hard deleted record is now a successful noop. 200 | * `#destroy` on a new record will set deleted_at (previously this raised an error) 201 | * `#destroy` and `#delete` always return self when successful. 202 | 203 | ### Bug Fixes 204 | 205 | * Calling `#destroy` twice will not hard-delete records. Use `#really_destroy!` if this is desired. 206 | * Fix errors on non-paranoid has_one dependent associations 207 | 208 | ## 2.0.5 (2015-01-22) 209 | 210 | ### Bug fixes 211 | 212 | * Fix restoring polymorphic has_one relationships [#189](https://github.com/radar/paranoia/pull/189) [#174](https://github.com/radar/paranoia/issues/174) [Patrick Koperwas](https://github.com/PatKoperwas) 213 | * Fix errors when restoring a model with a has_one against a non-paranoid model. [#168](https://github.com/radar/paranoia/pull/168) [Shreyas Agarwal](https://github.com/shreyas123) 214 | * Fix rspec 2 compatibility [#197](https://github.com/radar/paranoia/pull/197) [Emil Sågfors](https://github.com/lime) 215 | * Fix some deprecation warnings on rails 4.2 [Sergey Alekseev](https://github.com/sergey-alekseev) 216 | 217 | ## 2.0.4 (2014-12-02) 218 | 219 | ### Features 220 | * Add paranoia_scope as named version of default_scope [#184](https://github.com/radar/paranoia/pull/184) [Jozsef Nyitrai](https://github.com/nyjt) 221 | 222 | 223 | ### Bug Fixes 224 | * Fix initialization problems when missing table or no database connection [#186](https://github.com/radar/paranoia/issues/186) 225 | * Fix broken restore of has_one associations [#185](https://github.com/radar/paranoia/issues/185) [#171](https://github.com/radar/paranoia/pull/171) [Martin Sereinig](https://github.com/srecnig) 226 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ben@benmorgan.io. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Paranoia is an open source project and we encourage contributions. 2 | 3 | ## Filing an issue 4 | 5 | When filing an issue on the Paranoia project, please provide these details: 6 | 7 | * A comprehensive list of steps to reproduce the issue. 8 | * What you're *expecting* to happen compared with what's *actually* happening. 9 | * Your application's complete `Gemfile.lock`, and `Gemfile.lock` as text in a [Gist](https://gist.github.com) (*not as an image*) 10 | * Any relevant stack traces ("Full trace" preferred) 11 | 12 | In 99% of cases, this information is enough to determine the cause and solution 13 | to the problem that is being described. 14 | 15 | Please remember to format code using triple backticks (\`) so that it is neatly 16 | formatted when the issue is posted. 17 | 18 | ## Pull requests 19 | 20 | We gladly accept pull requests to add documentation, fix bugs and, in some circumstances, 21 | add new features to Paranoia. 22 | 23 | Here's a quick guide: 24 | 25 | 1. Fork the repo. 26 | 27 | 2. Run the tests. We only take pull requests with passing tests, and it's great 28 | to know that you have a clean slate. 29 | 30 | 3. Create new branch then make changes and add tests for your changes. Only 31 | refactoring and documentation changes require no new tests. If you are adding 32 | functionality or fixing a bug, we need tests! 33 | 34 | 4. Push to your fork and submit a pull request. 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | sqlite = ENV['SQLITE_VERSION'] 4 | 5 | if sqlite 6 | gem 'sqlite3', sqlite, platforms: [:ruby] 7 | else 8 | gem 'sqlite3', '~> 1.4', platforms: [:ruby] 9 | end 10 | 11 | platforms :jruby do 12 | gem 'activerecord-jdbcsqlite3-adapter' 13 | end 14 | 15 | if RUBY_ENGINE == 'rbx' 16 | platforms :rbx do 17 | gem 'rubinius-developer_tools' 18 | gem 'rubysl', '~> 2.0' 19 | gem 'rubysl-test-unit' 20 | end 21 | end 22 | 23 | rails = ENV['RAILS'] || '~> 6.0.4' 24 | 25 | if rails == 'edge' 26 | gem 'rails', github: 'rails/rails' 27 | else 28 | gem 'rails', rails 29 | end 30 | 31 | # Specify your gem's dependencies in paranoia.gemspec 32 | gemspec 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, without written agreement and without 2 | license or royalty fees, to use, copy, modify, and distribute this 3 | software and its documentation for any purpose, provided that the 4 | above copyright notice and the following two paragraphs appear in 5 | all copies of this software. 6 | 7 | IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR 8 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 9 | ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN 10 | IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 11 | DAMAGE. 12 | 13 | THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, 14 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 16 | ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO 17 | PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/paranoia.svg)](https://badge.fury.io/rb/paranoia) 2 | [![build](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml/badge.svg)](https://github.com/rubysherpas/paranoia/actions/workflows/build.yml) 3 | 4 | **Notice:** 5 | 6 | `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. 7 | 8 | Paranoia will continue to accept bug fixes and support new versions of Rails but isn't accepting new features. 9 | 10 | # Paranoia 11 | 12 | Paranoia is a re-implementation of [acts\_as\_paranoid](http://github.com/ActsAsParanoid/acts_as_paranoid) for Rails 3/4/5, using much, much, much less code. 13 | 14 | When your app is using Paranoia, calling `destroy` on an ActiveRecord object doesn't actually destroy the database record, but just *hides* it. Paranoia does this by setting a `deleted_at` field to the current time when you `destroy` a record, and hides it by scoping all queries on your model to only include records which do not have a `deleted_at` field. 15 | 16 | If you wish to actually destroy an object you may call `really_destroy!`. **WARNING**: This will also *really destroy* all `dependent: :destroy` records, so please aim this method away from face when using. 17 | 18 | If a record has `has_many` associations defined AND those associations have `dependent: :destroy` set on them, then they will also be soft-deleted if `acts_as_paranoid` is set, otherwise the normal destroy will be called. ***See [Destroying through association callbacks](#destroying-through-association-callbacks) for clarifying examples.*** 19 | 20 | ## Getting Started Video 21 | Setup and basic usage of the paranoia gem 22 | [GoRails #41](https://gorails.com/episodes/soft-delete-with-paranoia) 23 | 24 | ## Installation & Usage 25 | 26 | For Rails 3, please use version 1 of Paranoia: 27 | 28 | ``` ruby 29 | gem "paranoia", "~> 1.0" 30 | ``` 31 | 32 | For Rails 4 and 5, please use version 2 of Paranoia (2.2 or greater required for rails 5): 33 | 34 | ``` ruby 35 | gem "paranoia", "~> 2.2" 36 | ``` 37 | 38 | Of course you can install this from GitHub as well from one of these examples: 39 | 40 | ``` ruby 41 | gem "paranoia", github: "rubysherpas/paranoia", branch: "rails3" 42 | gem "paranoia", github: "rubysherpas/paranoia", branch: "rails4" 43 | gem "paranoia", github: "rubysherpas/paranoia", branch: "rails5" 44 | ``` 45 | 46 | Then run: 47 | 48 | ``` shell 49 | bundle install 50 | ``` 51 | 52 | Updating is as simple as `bundle update paranoia`. 53 | 54 | #### Run your migrations for the desired models 55 | 56 | Run: 57 | 58 | ``` shell 59 | bin/rails generate migration AddDeletedAtToClients deleted_at:datetime:index 60 | ``` 61 | 62 | and now you have a migration 63 | 64 | ``` ruby 65 | class AddDeletedAtToClients < ActiveRecord::Migration 66 | def change 67 | add_column :clients, :deleted_at, :datetime 68 | add_index :clients, :deleted_at 69 | end 70 | end 71 | ``` 72 | 73 | ### Usage 74 | 75 | #### In your model: 76 | 77 | ``` ruby 78 | class Client < ActiveRecord::Base 79 | acts_as_paranoid 80 | 81 | # ... 82 | end 83 | ``` 84 | 85 | Hey presto, it's there! Calling `destroy` will now set the `deleted_at` column: 86 | 87 | 88 | ``` ruby 89 | >> client.deleted_at 90 | # => nil 91 | >> client.destroy 92 | # => client 93 | >> client.deleted_at 94 | # => [current timestamp] 95 | ``` 96 | 97 | If you really want it gone *gone*, call `really_destroy!`: 98 | 99 | ``` ruby 100 | >> client.deleted_at 101 | # => nil 102 | >> client.really_destroy! 103 | # => client 104 | ``` 105 | 106 | If you need skip updating timestamps for deleting records, call `really_destroy!(update_destroy_attributes: false)`. 107 | 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. 108 | 109 | ``` ruby 110 | >> client.really_destroy!(update_destroy_attributes: false) 111 | # => client 112 | ``` 113 | 114 | If you want to use a column other than `deleted_at`, you can pass it as an option: 115 | 116 | ``` ruby 117 | class Client < ActiveRecord::Base 118 | acts_as_paranoid column: :destroyed_at 119 | 120 | ... 121 | end 122 | ``` 123 | 124 | 125 | If you want to skip adding the default scope: 126 | 127 | ``` ruby 128 | class Client < ActiveRecord::Base 129 | acts_as_paranoid without_default_scope: true 130 | 131 | ... 132 | end 133 | ``` 134 | 135 | If you want to access soft-deleted associations, override the getter method: 136 | 137 | ``` ruby 138 | def product 139 | Product.unscoped { super } 140 | end 141 | ``` 142 | 143 | If you want to include associated soft-deleted objects, you can (un)scope the association: 144 | 145 | ``` ruby 146 | class Person < ActiveRecord::Base 147 | belongs_to :group, -> { with_deleted } 148 | end 149 | 150 | Person.includes(:group).all 151 | ``` 152 | 153 | If you want to find all records, even those which are deleted: 154 | 155 | ``` ruby 156 | Client.with_deleted 157 | ``` 158 | 159 | If you want to exclude deleted records, when not able to use the default_scope (e.g. when using without_default_scope): 160 | 161 | ``` ruby 162 | Client.without_deleted 163 | ``` 164 | 165 | If you want to find only the deleted records: 166 | 167 | ``` ruby 168 | Client.only_deleted 169 | ``` 170 | 171 | If you want to check if a record is soft-deleted: 172 | 173 | ``` ruby 174 | client.paranoia_destroyed? 175 | # or 176 | client.deleted? 177 | ``` 178 | 179 | If you want to restore a record: 180 | 181 | ``` ruby 182 | Client.restore(id) 183 | # or 184 | client.restore 185 | ``` 186 | 187 | If you want to restore a whole bunch of records: 188 | 189 | ``` ruby 190 | Client.restore([id1, id2, ..., idN]) 191 | ``` 192 | 193 | If you want to restore a record and their dependently destroyed associated records: 194 | 195 | ``` ruby 196 | Client.restore(id, :recursive => true) 197 | # or 198 | client.restore(:recursive => true) 199 | ``` 200 | 201 | If you want to restore a record and only those dependently destroyed associated records that were deleted within 2 minutes of the object upon which they depend: 202 | 203 | ``` ruby 204 | Client.restore(id, :recursive => true, :recovery_window => 2.minutes) 205 | # or 206 | client.restore(:recursive => true, :recovery_window => 2.minutes) 207 | ``` 208 | 209 | If you want to trigger an after_commit callback when restoring a record: 210 | 211 | ``` ruby 212 | class Client < ActiveRecord::Base 213 | acts_as_paranoid after_restore_commit: true 214 | 215 | after_commit :commit_called, on: :restore 216 | # or 217 | after_restore_commit :commit_called 218 | ... 219 | end 220 | ``` 221 | 222 | Note that by default paranoia will not prevent that a soft destroyed object can't be associated with another object of a different model. 223 | A Rails validator is provided should you require this functionality: 224 | ``` ruby 225 | validates :some_assocation, association_not_soft_destroyed: true 226 | ``` 227 | This validator makes sure that `some_assocation` is not soft destroyed. If the object is soft destroyed the main object is rendered invalid and an validation error is added. 228 | 229 | For more information, please look at the tests. 230 | 231 | #### About indexes: 232 | 233 | Beware that you should adapt all your indexes for them to work as fast as previously. 234 | For example, 235 | 236 | ``` ruby 237 | add_index :clients, :group_id 238 | add_index :clients, [:group_id, :other_id] 239 | ``` 240 | 241 | should be replaced with 242 | 243 | ``` ruby 244 | add_index :clients, :group_id, where: "deleted_at IS NULL" 245 | add_index :clients, [:group_id, :other_id], where: "deleted_at IS NULL" 246 | ``` 247 | 248 | Of course, this is not necessary for the indexes you always use in association with `with_deleted` or `only_deleted`. 249 | 250 | ##### Unique Indexes 251 | 252 | Because NULL != NULL in standard SQL, we can not simply create a unique index 253 | on the deleted_at column and expect it to enforce that there only be one record 254 | with a certain combination of values. 255 | 256 | If your database supports them, good alternatives include partial indexes 257 | (above) and indexes on computed columns. E.g. 258 | 259 | ``` ruby 260 | add_index :clients, [:group_id, 'COALESCE(deleted_at, false)'], unique: true 261 | ``` 262 | 263 | If not, an alternative is to create a separate column which is maintained 264 | alongside deleted_at for the sake of enforcing uniqueness. To that end, 265 | paranoia makes use of two method to make its destroy and restore actions: 266 | paranoia_restore_attributes and paranoia_destroy_attributes. 267 | 268 | ``` ruby 269 | add_column :clients, :active, :boolean 270 | add_index :clients, [:group_id, :active], unique: true 271 | 272 | class Client < ActiveRecord::Base 273 | # optionally have paranoia make use of your unique column, so that 274 | # your lookups will benefit from the unique index 275 | acts_as_paranoid column: :active, sentinel_value: true 276 | 277 | def paranoia_restore_attributes 278 | { 279 | deleted_at: nil, 280 | active: true 281 | } 282 | end 283 | 284 | def paranoia_destroy_attributes 285 | { 286 | deleted_at: current_time_from_proper_timezone, 287 | active: nil 288 | } 289 | end 290 | end 291 | ``` 292 | 293 | ##### Destroying through association callbacks 294 | 295 | When dealing with `dependent: :destroy` associations and `acts_as_paranoid`, it's important to remember that whatever method is called on the parent model will be called on the child model. For example, given both models of an association have `acts_as_paranoid` defined: 296 | 297 | ``` ruby 298 | class Client < ActiveRecord::Base 299 | acts_as_paranoid 300 | 301 | has_many :emails, dependent: :destroy 302 | end 303 | 304 | class Email < ActiveRecord::Base 305 | acts_as_paranoid 306 | 307 | belongs_to :client 308 | end 309 | ``` 310 | 311 | When we call `destroy` on the parent `client`, it will call `destroy` on all of its associated children `emails`: 312 | 313 | ``` ruby 314 | >> client.emails.count 315 | # => 5 316 | >> client.destroy 317 | # => client 318 | >> client.deleted_at 319 | # => [current timestamp] 320 | >> Email.where(client_id: client.id).count 321 | # => 0 322 | >> Email.with_deleted.where(client_id: client.id).count 323 | # => 5 324 | ``` 325 | 326 | Similarly, when we call `really_destroy!` on the parent `client`, then each child `email` will also have `really_destroy!` called: 327 | 328 | ``` ruby 329 | >> client.emails.count 330 | # => 5 331 | >> client.id 332 | # => 12345 333 | >> client.really_destroy! 334 | # => client 335 | >> Client.find 12345 336 | # => ActiveRecord::RecordNotFound 337 | >> Email.with_deleted.where(client_id: client.id).count 338 | # => 0 339 | ``` 340 | 341 | However, if the child model `Email` does not have `acts_as_paranoid` set, then calling `destroy` on the parent `client` will also call `destroy` on each child `email`, thereby actually destroying them: 342 | 343 | ``` ruby 344 | class Client < ActiveRecord::Base 345 | acts_as_paranoid 346 | 347 | has_many :emails, dependent: :destroy 348 | end 349 | 350 | class Email < ActiveRecord::Base 351 | belongs_to :client 352 | end 353 | 354 | >> client.emails.count 355 | # => 5 356 | >> client.destroy 357 | # => client 358 | >> Email.where(client_id: client.id).count 359 | # => 0 360 | >> Email.with_deleted.where(client_id: client.id).count 361 | # => NoMethodError: undefined method `with_deleted' for # 362 | ``` 363 | 364 | #### delete_all: 365 | 366 | The gem supports `delete_all` method, however it is disabled by default, to enable it add this in your `environment` file 367 | 368 | ``` ruby 369 | Paranoia.delete_all_enabled = true 370 | ``` 371 | alternatively, you can enable/disable it for specific models as follow: 372 | 373 | ``` ruby 374 | class User < ActiveRecord::Base 375 | acts_as_paranoid(delete_all_enabled: true) 376 | end 377 | ``` 378 | 379 | ## Acts As Paranoid Migration 380 | 381 | You can replace the older `acts_as_paranoid` methods as follows: 382 | 383 | | Old Syntax | New Syntax | 384 | |:-------------------------- |:------------------------------ | 385 | |`find_with_deleted(:all)` | `Client.with_deleted` | 386 | |`find_with_deleted(:first)` | `Client.with_deleted.first` | 387 | |`find_with_deleted(id)` | `Client.with_deleted.find(id)` | 388 | 389 | 390 | The `recover` method in `acts_as_paranoid` runs `update` callbacks. Paranoia's 391 | `restore` method does not do this. 392 | 393 | ## Callbacks 394 | 395 | Paranoia provides several callbacks. It triggers `destroy` callback when the record is marked as deleted and `real_destroy` when the record is completely removed from database. It also calls `restore` callback when the record is restored via paranoia 396 | 397 | For example if you want to index your records in some search engine you can go like this: 398 | 399 | ```ruby 400 | class Product < ActiveRecord::Base 401 | acts_as_paranoid 402 | 403 | after_destroy :update_document_in_search_engine 404 | after_restore :update_document_in_search_engine 405 | after_real_destroy :remove_document_from_search_engine 406 | end 407 | ``` 408 | 409 | You can use these events just like regular Rails callbacks with before, after and around hooks. 410 | 411 | ## License 412 | 413 | This gem is released under the MIT license. 414 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | task :test do 5 | Dir['test/*_test.rb'].each do |testfile| 6 | load testfile 7 | end 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /lib/paranoia.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' unless defined? ActiveRecord 2 | 3 | if [ActiveRecord::VERSION::MAJOR, ActiveRecord::VERSION::MINOR] == [5, 2] || 4 | ActiveRecord::VERSION::MAJOR > 5 5 | require 'paranoia/active_record_5_2' 6 | end 7 | 8 | module Paranoia 9 | 10 | class << self 11 | # Change default values in a rails initializer 12 | attr_accessor :default_sentinel_value, 13 | :delete_all_enabled 14 | end 15 | 16 | def self.included(klazz) 17 | klazz.extend Query 18 | end 19 | 20 | module Query 21 | def paranoid? ; true ; end 22 | 23 | # If you want to find all records, even those which are deleted 24 | def with_deleted 25 | if ActiveRecord::VERSION::STRING >= "4.1" 26 | return unscope where: paranoia_column 27 | end 28 | all.tap { |x| x.default_scoped = false } 29 | end 30 | 31 | # If you want to find only the deleted records 32 | def only_deleted 33 | if paranoia_sentinel_value.nil? 34 | return with_deleted.where.not(paranoia_column => paranoia_sentinel_value) 35 | end 36 | # if paranoia_sentinel_value is not null, then it is possible that 37 | # some deleted rows will hold a null value in the paranoia column 38 | # these will not match != sentinel value because "NULL != value" is 39 | # NULL under the sql standard 40 | # Scoping with the table_name is mandatory to avoid ambiguous errors when joining tables. 41 | scoped_quoted_paranoia_column = "#{connection.quote_table_name(self.table_name)}.#{connection.quote_column_name(paranoia_column)}" 42 | with_deleted.where("#{scoped_quoted_paranoia_column} IS NULL OR #{scoped_quoted_paranoia_column} != ?", paranoia_sentinel_value) 43 | end 44 | alias_method :deleted, :only_deleted 45 | 46 | # If you want to restore a record 47 | def restore(id_or_ids, opts = {}) 48 | ids = Array(id_or_ids).flatten 49 | any_object_instead_of_id = ids.any? { |id| ActiveRecord::Base === id } 50 | if any_object_instead_of_id 51 | ids.map! { |id| ActiveRecord::Base === id ? id.id : id } 52 | ActiveSupport::Deprecation.warn("You are passing an instance of ActiveRecord::Base to `restore`. " \ 53 | "Please pass the id of the object by calling `.id`") 54 | end 55 | ids.map { |id| only_deleted.find(id).restore!(opts) } 56 | end 57 | 58 | def paranoia_destroy_attributes 59 | { 60 | paranoia_column => current_time_from_proper_timezone 61 | }.merge(timestamp_attributes_with_current_time) 62 | end 63 | 64 | def timestamp_attributes_with_current_time 65 | timestamp_attributes_for_update_in_model.each_with_object({}) { |attr,hash| hash[attr] = current_time_from_proper_timezone } 66 | end 67 | end 68 | 69 | def paranoia_destroy 70 | with_transaction_returning_status do 71 | result = run_callbacks(:destroy) do 72 | @_disable_counter_cache = paranoia_destroyed? 73 | result = paranoia_delete 74 | next result unless result && ActiveRecord::VERSION::STRING >= '4.2' 75 | each_counter_cached_associations do |association| 76 | foreign_key = association.reflection.foreign_key.to_sym 77 | next if destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key 78 | next unless send(association.reflection.name) 79 | association.decrement_counters 80 | end 81 | @_trigger_destroy_callback = true 82 | @_disable_counter_cache = false 83 | result 84 | end 85 | raise ActiveRecord::Rollback, "Not destroyed" unless paranoia_destroyed? 86 | result 87 | end || false 88 | end 89 | alias_method :destroy, :paranoia_destroy 90 | 91 | def paranoia_destroy! 92 | paranoia_destroy || 93 | raise(ActiveRecord::RecordNotDestroyed.new("Failed to destroy the record", self)) 94 | end 95 | 96 | def trigger_transactional_callbacks? 97 | super || @_trigger_destroy_callback && paranoia_destroyed? || 98 | @_trigger_restore_callback && !paranoia_destroyed? 99 | end 100 | 101 | def transaction_include_any_action?(actions) 102 | super || actions.any? do |action| 103 | if action == :restore 104 | paranoia_after_restore_commit && @_trigger_restore_callback 105 | end 106 | end 107 | end 108 | 109 | def paranoia_delete 110 | raise ActiveRecord::ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? 111 | if persisted? 112 | # if a transaction exists, add the record so that after_commit 113 | # callbacks can be run 114 | add_to_transaction 115 | update_columns(paranoia_destroy_attributes) 116 | elsif !frozen? 117 | assign_attributes(paranoia_destroy_attributes) 118 | end 119 | self 120 | end 121 | alias_method :delete, :paranoia_delete 122 | 123 | def restore!(opts = {}) 124 | self.class.transaction do 125 | run_callbacks(:restore) do 126 | recovery_window_range = get_recovery_window_range(opts) 127 | # Fixes a bug where the build would error because attributes were frozen. 128 | # This only happened on Rails versions earlier than 4.1. 129 | noop_if_frozen = ActiveRecord.version < Gem::Version.new("4.1") 130 | if within_recovery_window?(recovery_window_range) && ((noop_if_frozen && !@attributes.frozen?) || !noop_if_frozen) 131 | @_disable_counter_cache = !paranoia_destroyed? 132 | write_attribute paranoia_column, paranoia_sentinel_value 133 | if paranoia_after_restore_commit 134 | @_trigger_restore_callback = true 135 | add_to_transaction 136 | end 137 | update_columns(paranoia_restore_attributes) 138 | each_counter_cached_associations do |association| 139 | if send(association.reflection.name) 140 | association.increment_counters 141 | end 142 | end 143 | @_disable_counter_cache = false 144 | end 145 | restore_associated_records(recovery_window_range) if opts[:recursive] 146 | end 147 | end 148 | 149 | self 150 | ensure 151 | if paranoia_after_restore_commit 152 | @_trigger_restore_callback = false 153 | end 154 | end 155 | alias :restore :restore! 156 | 157 | def get_recovery_window_range(opts) 158 | return opts[:recovery_window_range] if opts[:recovery_window_range] 159 | return unless opts[:recovery_window] 160 | (deletion_time - opts[:recovery_window]..deletion_time + opts[:recovery_window]) 161 | end 162 | 163 | def within_recovery_window?(recovery_window_range) 164 | return true unless recovery_window_range 165 | recovery_window_range.cover?(deletion_time) 166 | end 167 | 168 | def paranoia_destroyed? 169 | paranoia_column_value != paranoia_sentinel_value 170 | end 171 | alias :deleted? :paranoia_destroyed? 172 | 173 | def really_destroy!(update_destroy_attributes: true) 174 | with_transaction_returning_status do 175 | run_callbacks(:real_destroy) do 176 | @_disable_counter_cache = paranoia_destroyed? 177 | dependent_reflections = self.class.reflections.select do |name, reflection| 178 | reflection.options[:dependent] == :destroy 179 | end 180 | if dependent_reflections.any? 181 | dependent_reflections.each do |name, reflection| 182 | association_data = self.send(name) 183 | # has_one association can return nil 184 | # .paranoid? will work for both instances and classes 185 | next unless association_data && association_data.paranoid? 186 | if reflection.collection? 187 | next association_data.with_deleted.find_each { |record| 188 | record.really_destroy!(update_destroy_attributes: update_destroy_attributes) 189 | } 190 | end 191 | association_data.really_destroy!(update_destroy_attributes: update_destroy_attributes) 192 | end 193 | end 194 | update_columns(paranoia_destroy_attributes) if update_destroy_attributes 195 | destroy_without_paranoia 196 | end 197 | end 198 | end 199 | 200 | private 201 | 202 | def counter_cache_disabled? 203 | defined?(@_disable_counter_cache) && @_disable_counter_cache 204 | end 205 | 206 | def counter_cached_association_names 207 | return [] if counter_cache_disabled? 208 | super 209 | end 210 | 211 | def each_counter_cached_associations 212 | return [] if counter_cache_disabled? 213 | 214 | if defined?(super) 215 | super 216 | else 217 | counter_cached_association_names.each do |name| 218 | yield association(name) 219 | end 220 | end 221 | end 222 | 223 | def paranoia_restore_attributes 224 | { 225 | paranoia_column => paranoia_sentinel_value 226 | }.merge(self.class.timestamp_attributes_with_current_time) 227 | end 228 | 229 | delegate :paranoia_destroy_attributes, to: 'self.class' 230 | 231 | def paranoia_find_has_one_target(association) 232 | association_foreign_key = association.options[:through].present? ? association.klass.primary_key : association.foreign_key 233 | association_find_conditions = { association_foreign_key => self.id } 234 | association_find_conditions[association.type] = self.class.name if association.type 235 | 236 | scope = association.klass.only_deleted.where(association_find_conditions) 237 | scope = scope.merge(association.scope) if association.scope 238 | scope.first 239 | end 240 | 241 | # restore associated records that have been soft deleted when 242 | # we called #destroy 243 | def restore_associated_records(recovery_window_range = nil) 244 | destroyed_associations = self.class.reflect_on_all_associations.select do |association| 245 | association.options[:dependent] == :destroy 246 | end 247 | 248 | destroyed_associations.each do |association| 249 | association_data = send(association.name) 250 | 251 | unless association_data.nil? 252 | if association_data.paranoid? 253 | if association.collection? 254 | association_data.only_deleted.each do |record| 255 | record.restore(:recursive => true, :recovery_window_range => recovery_window_range) 256 | end 257 | else 258 | association_data.restore(:recursive => true, :recovery_window_range => recovery_window_range) 259 | end 260 | end 261 | end 262 | 263 | if association_data.nil? && association.macro.to_s == "has_one" 264 | if association.klass.paranoid? 265 | paranoia_find_has_one_target(association) 266 | .try!(:restore, recursive: true, :recovery_window_range => recovery_window_range) 267 | end 268 | end 269 | end 270 | 271 | if ActiveRecord.version.to_s > '7' 272 | # Method deleted in https://github.com/rails/rails/commit/dd5886d00a2d5f31ccf504c391aad93deb014eb8 273 | @association_cache.clear if persisted? && destroyed_associations.present? 274 | else 275 | clear_association_cache if destroyed_associations.present? 276 | end 277 | end 278 | end 279 | 280 | module ActiveRecord 281 | module Transactions 282 | module RestoreSupport 283 | def self.included(base) 284 | base::ACTIONS << :restore unless base::ACTIONS.include?(:restore) 285 | end 286 | end 287 | 288 | module ClassMethods 289 | def after_restore_commit(*args, &block) 290 | set_options_for_callbacks!(args, on: :restore) 291 | set_callback(:commit, :after, *args, &block) 292 | end 293 | end 294 | end 295 | end 296 | 297 | module Paranoia::Relation 298 | def paranoia_delete_all 299 | update_all(klass.paranoia_destroy_attributes) 300 | end 301 | 302 | alias_method :delete_all, :paranoia_delete_all 303 | end 304 | 305 | ActiveSupport.on_load(:active_record) do 306 | class ActiveRecord::Base 307 | def self.acts_as_paranoid(options={}) 308 | if included_modules.include?(Paranoia) 309 | puts "[WARN] #{self.name} is calling acts_as_paranoid more than once!" 310 | 311 | return 312 | end 313 | 314 | define_model_callbacks :restore, :real_destroy 315 | 316 | alias_method :really_destroyed?, :destroyed? 317 | alias_method :really_delete, :delete 318 | alias_method :destroy_without_paranoia, :destroy 319 | class << self; delegate :really_delete_all, to: :all end 320 | 321 | include Paranoia 322 | class_attribute :paranoia_column, :paranoia_sentinel_value, :paranoia_after_restore_commit, 323 | :delete_all_enabled 324 | 325 | self.paranoia_column = (options[:column] || :deleted_at).to_s 326 | self.paranoia_sentinel_value = options.fetch(:sentinel_value) { Paranoia.default_sentinel_value } 327 | self.paranoia_after_restore_commit = options.fetch(:after_restore_commit) { false } 328 | def self.paranoia_scope 329 | where(paranoia_column => paranoia_sentinel_value) 330 | end 331 | class << self; alias_method :without_deleted, :paranoia_scope end 332 | 333 | unless options[:without_default_scope] 334 | default_scope { paranoia_scope } 335 | end 336 | 337 | before_restore { 338 | self.class.notify_observers(:before_restore, self) if self.class.respond_to?(:notify_observers) 339 | } 340 | after_restore { 341 | self.class.notify_observers(:after_restore, self) if self.class.respond_to?(:notify_observers) 342 | } 343 | 344 | if paranoia_after_restore_commit 345 | ActiveRecord::Transactions.send(:include, ActiveRecord::Transactions::RestoreSupport) 346 | end 347 | 348 | self.delete_all_enabled = options[:delete_all_enabled] || Paranoia.delete_all_enabled 349 | 350 | if self.delete_all_enabled 351 | "#{self}::ActiveRecord_Relation".constantize.class_eval do 352 | alias_method :really_delete_all, :delete_all 353 | 354 | include Paranoia::Relation 355 | end 356 | end 357 | end 358 | 359 | # Please do not use this method in production. 360 | # Pretty please. 361 | def self.I_AM_THE_DESTROYER! 362 | # TODO: actually implement spelling error fixes 363 | puts %Q{ 364 | Sharon: "There should be a method called I_AM_THE_DESTROYER!" 365 | Ryan: "What should this method do?" 366 | Sharon: "It should fix all the spelling errors on the page!" 367 | } 368 | end 369 | 370 | def self.paranoid? ; false ; end 371 | def paranoid? ; self.class.paranoid? ; end 372 | 373 | private 374 | 375 | def paranoia_column 376 | self.class.paranoia_column 377 | end 378 | 379 | def paranoia_column_value 380 | send(paranoia_column) 381 | end 382 | 383 | def paranoia_sentinel_value 384 | self.class.paranoia_sentinel_value 385 | end 386 | 387 | def deletion_time 388 | paranoia_column_value.acts_like?(:time) ? paranoia_column_value : deleted_at 389 | end 390 | end 391 | end 392 | 393 | require 'paranoia/rspec' if defined? RSpec 394 | 395 | module ActiveRecord 396 | module Validations 397 | module UniquenessParanoiaValidator 398 | def build_relation(klass, *args) 399 | relation = super 400 | return relation unless klass.respond_to?(:paranoia_column) 401 | arel_paranoia_scope = klass.arel_table[klass.paranoia_column].eq(klass.paranoia_sentinel_value) 402 | if ActiveRecord::VERSION::STRING >= "5.0" 403 | relation.where(arel_paranoia_scope) 404 | else 405 | relation.and(arel_paranoia_scope) 406 | end 407 | end 408 | end 409 | 410 | class UniquenessValidator < ActiveModel::EachValidator 411 | prepend UniquenessParanoiaValidator 412 | end 413 | 414 | class AssociationNotSoftDestroyedValidator < ActiveModel::EachValidator 415 | def validate_each(record, attribute, value) 416 | # if association is soft destroyed, add an error 417 | if value.present? && value.paranoia_destroyed? 418 | record.errors.add(attribute, 'has been soft-deleted') 419 | end 420 | end 421 | end 422 | end 423 | end 424 | -------------------------------------------------------------------------------- /lib/paranoia/active_record_5_2.rb: -------------------------------------------------------------------------------- 1 | module HandleParanoiaDestroyedInBelongsToAssociation 2 | def handle_dependency 3 | return unless load_target 4 | 5 | case options[:dependent] 6 | when :destroy 7 | target.destroy 8 | if target.respond_to?(:paranoia_destroyed?) 9 | raise ActiveRecord::Rollback unless target.paranoia_destroyed? 10 | else 11 | raise ActiveRecord::Rollback unless target.destroyed? 12 | end 13 | else 14 | target.send(options[:dependent]) 15 | end 16 | end 17 | end 18 | 19 | module HandleParanoiaDestroyedInHasOneAssociation 20 | def delete(method = options[:dependent]) 21 | if load_target 22 | case method 23 | when :delete 24 | target.delete 25 | when :destroy 26 | target.destroyed_by_association = reflection 27 | target.destroy 28 | if target.respond_to?(:paranoia_destroyed?) 29 | throw(:abort) unless target.paranoia_destroyed? 30 | else 31 | throw(:abort) unless target.destroyed? 32 | end 33 | when :nullify 34 | target.update_columns(reflection.foreign_key => nil) if target.persisted? 35 | end 36 | end 37 | end 38 | end 39 | 40 | ActiveRecord::Associations::BelongsToAssociation.prepend HandleParanoiaDestroyedInBelongsToAssociation 41 | ActiveRecord::Associations::HasOneAssociation.prepend HandleParanoiaDestroyedInHasOneAssociation 42 | -------------------------------------------------------------------------------- /lib/paranoia/rspec.rb: -------------------------------------------------------------------------------- 1 | if defined?(RSpec) 2 | require 'rspec/expectations' 3 | 4 | # Validate the subject's class did call "acts_as_paranoid" 5 | RSpec::Matchers.define :act_as_paranoid do 6 | match { |subject| subject.class.ancestors.include?(Paranoia) } 7 | 8 | failure_message_proc = lambda do 9 | "expected #{subject.class} to use `acts_as_paranoid`" 10 | end 11 | 12 | failure_message_when_negated_proc = lambda do 13 | "expected #{subject.class} not to use `acts_as_paranoid`" 14 | end 15 | 16 | if respond_to?(:failure_message_when_negated) 17 | failure_message(&failure_message_proc) 18 | failure_message_when_negated(&failure_message_when_negated_proc) 19 | else 20 | # RSpec 2 compatibility: 21 | failure_message_for_should(&failure_message_proc) 22 | failure_message_for_should_not(&failure_message_when_negated_proc) 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/paranoia/version.rb: -------------------------------------------------------------------------------- 1 | module Paranoia 2 | VERSION = '3.0.1'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /paranoia.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path("../lib/paranoia/version", __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "paranoia" 6 | s.version = Paranoia::VERSION 7 | s.platform = Gem::Platform::RUBY 8 | s.authors = %w(radarlistener@gmail.com) 9 | s.email = %w(ben@benmorgan.io john.hawthorn@gmail.com) 10 | s.homepage = "https://github.com/rubysherpas/paranoia" 11 | s.license = 'MIT' 12 | s.summary = "Paranoia is a re-implementation of acts_as_paranoid for Rails 3, 4, and 5, using much, much, much less code." 13 | s.description = <<-DSC 14 | Paranoia is a re-implementation of acts_as_paranoid for Rails 5, 6, and 7, 15 | using much, much, much less code. You would use either plugin / gem if you 16 | wished that when you called destroy on an Active Record object that it 17 | didn't actually destroy it, but just "hid" the record. Paranoia does this 18 | by setting a deleted_at field to the current time when you destroy a record, 19 | and hides it by scoping all queries on your model to only include records 20 | which do not have a deleted_at field. 21 | DSC 22 | 23 | s.required_rubygems_version = ">= 1.3.6" 24 | 25 | s.required_ruby_version = '>= 2.7' 26 | 27 | s.add_dependency 'activerecord', '>= 6', '< 8.1' 28 | 29 | s.add_development_dependency "bundler", ">= 1.0.0" 30 | s.add_development_dependency "rake" 31 | 32 | 33 | s.files = Dir.chdir(File.expand_path('..', __FILE__)) do 34 | files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)}) } 35 | files 36 | end 37 | 38 | s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact 39 | s.require_path = 'lib' 40 | end 41 | -------------------------------------------------------------------------------- /test/paranoia_test.rb: -------------------------------------------------------------------------------- 1 | require 'logger' # required for test suite to pass on rails versions earlier than 7.1. https://stackoverflow.com/questions/79360526 2 | require 'bundler/setup' 3 | require 'active_record' 4 | require 'minitest/autorun' 5 | require 'paranoia' 6 | 7 | test_framework = defined?(Minitest::Test) ? Minitest::Test : Minitest::Unit::TestCase 8 | 9 | if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=) 10 | ActiveRecord::Base.raise_in_transactional_callbacks = true 11 | end 12 | 13 | def connect! 14 | ActiveRecord::Base.establish_connection :adapter => 'sqlite3', database: ':memory:' 15 | end 16 | 17 | def setup! 18 | connect! 19 | { 20 | 'parent_model_with_counter_cache_columns' => 'related_models_count INTEGER DEFAULT 0', 21 | 'parent_models' => 'deleted_at DATETIME', 22 | 'paranoid_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 23 | 'paranoid_model_with_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 24 | 'paranoid_model_with_build_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_and_build_id INTEGER, name VARCHAR(32)', 25 | 'paranoid_model_with_anthor_class_name_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, paranoid_model_with_has_one_id INTEGER', 26 | 'paranoid_model_with_foreign_key_belongs' => 'parent_model_id INTEGER, deleted_at DATETIME, has_one_foreign_key_id INTEGER', 27 | 'paranoid_model_with_timestamps' => 'parent_model_id INTEGER, created_at DATETIME, updated_at DATETIME, deleted_at DATETIME', 28 | 'not_paranoid_model_with_belongs' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER', 29 | 'not_paranoid_model_with_belongs_and_assocation_not_soft_destroyed_validator' => 'parent_model_id INTEGER, paranoid_model_with_has_one_id INTEGER', 30 | 'paranoid_model_with_has_one_and_builds' => 'parent_model_id INTEGER, color VARCHAR(32), deleted_at DATETIME, has_one_foreign_key_id INTEGER', 31 | 'featureful_models' => 'deleted_at DATETIME, name VARCHAR(32)', 32 | 'plain_models' => 'deleted_at DATETIME', 33 | 'callback_models' => 'deleted_at DATETIME', 34 | 'after_commit_on_restore_callback_models' => 'deleted_at DATETIME', 35 | 'after_restore_commit_callback_models' => 'deleted_at DATETIME', 36 | 'after_commit_callback_restore_enabled_models' => 'deleted_at DATETIME', 37 | 'after_other_commit_callback_restore_enabled_models' => 'deleted_at DATETIME', 38 | 'after_commit_callback_models' => 'deleted_at DATETIME', 39 | 'fail_callback_models' => 'deleted_at DATETIME', 40 | 'association_with_abort_models' => 'deleted_at DATETIME', 41 | 'related_models' => 'parent_model_id INTEGER, parent_model_with_counter_cache_column_id INTEGER, deleted_at DATETIME', 42 | 'asplode_models' => 'parent_model_id INTEGER, deleted_at DATETIME', 43 | 'employers' => 'name VARCHAR(32), deleted_at DATETIME', 44 | 'employees' => 'deleted_at DATETIME', 45 | 'jobs' => 'employer_id INTEGER NOT NULL, employee_id INTEGER NOT NULL, deleted_at DATETIME', 46 | 'custom_column_models' => 'destroyed_at DATETIME', 47 | 'custom_sentinel_models' => 'deleted_at DATETIME NOT NULL', 48 | 'non_paranoid_models' => 'parent_model_id INTEGER', 49 | 'polymorphic_models' => 'parent_id INTEGER, parent_type STRING, deleted_at DATETIME', 50 | 'namespaced_paranoid_has_ones' => 'deleted_at DATETIME, paranoid_belongs_tos_id INTEGER', 51 | 'namespaced_paranoid_belongs_tos' => 'deleted_at DATETIME, paranoid_has_one_id INTEGER', 52 | 'unparanoid_unique_models' => 'name VARCHAR(32), paranoid_with_unparanoids_id INTEGER', 53 | 'active_column_models' => 'paranoid_model_id INTEGER, deleted_at DATETIME, active BOOLEAN', 54 | 'active_column_model_with_uniqueness_validations' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 55 | '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', 56 | 'active_column_model_with_has_many_relationships' => 'name VARCHAR(32), deleted_at DATETIME, active BOOLEAN', 57 | 'without_default_scope_models' => 'deleted_at DATETIME', 58 | 'paranoid_has_through_restore_parents' => 'deleted_at DATETIME', 59 | 'empty_paranoid_models' => 'deleted_at DATETIME', 60 | 'paranoid_has_one_throughs' => 'paranoid_has_through_restore_parent_id INTEGER NOT NULL, empty_paranoid_model_id INTEGER NOT NULL, deleted_at DATETIME', 61 | 'paranoid_has_many_throughs' => 'paranoid_has_through_restore_parent_id INTEGER NOT NULL, empty_paranoid_model_id INTEGER NOT NULL, deleted_at DATETIME', 62 | 'paranoid_has_one_with_scopes' => 'deleted_at DATETIME, kind STRING, paranoid_has_one_with_scope_id INTEGER', 63 | }.each do |table_name, columns_as_sql_string| 64 | ActiveRecord::Base.connection.execute "CREATE TABLE #{table_name} (id INTEGER NOT NULL PRIMARY KEY, #{columns_as_sql_string})" 65 | end 66 | end 67 | 68 | class WithDifferentConnection < ActiveRecord::Base 69 | establish_connection adapter: 'sqlite3', database: ':memory:' 70 | connection.execute 'CREATE TABLE with_different_connections (id INTEGER NOT NULL PRIMARY KEY, deleted_at DATETIME)' 71 | acts_as_paranoid 72 | end 73 | 74 | setup! 75 | 76 | class ParanoiaTest < test_framework 77 | def setup 78 | connection = ActiveRecord::Base.connection 79 | cleaner = ->(source) { 80 | ActiveRecord::Base.connection.execute "DELETE FROM #{source}" 81 | } 82 | 83 | if ActiveRecord::VERSION::MAJOR < 5 84 | connection.tables.each(&cleaner) 85 | else 86 | connection.data_sources.each(&cleaner) 87 | end 88 | end 89 | 90 | def test_plain_model_class_is_not_paranoid 91 | assert_equal false, PlainModel.paranoid? 92 | end 93 | 94 | def test_paranoid_model_class_is_paranoid 95 | assert_equal true, ParanoidModel.paranoid? 96 | end 97 | 98 | def test_doubly_paranoid_model_class_is_warned 99 | assert_output(/DoublyParanoidModel is calling acts_as_paranoid more than once!/) do 100 | DoublyParanoidModel.acts_as_paranoid 101 | end 102 | 103 | refute_equal( 104 | DoublyParanoidModel.instance_method(:destroy).source_location, 105 | DoublyParanoidModel.instance_method(:destroy_without_paranoia).source_location 106 | ) 107 | end 108 | 109 | def test_plain_models_are_not_paranoid 110 | assert_equal false, PlainModel.new.paranoid? 111 | end 112 | 113 | def test_paranoid_models_are_paranoid 114 | assert_equal true, ParanoidModel.new.paranoid? 115 | end 116 | 117 | def test_paranoid_models_to_param 118 | model = ParanoidModel.new 119 | model.save 120 | to_param = model.to_param 121 | 122 | model.destroy 123 | 124 | assert model.to_param 125 | assert_equal to_param, model.to_param 126 | end 127 | 128 | def test_destroy_behavior_for_plain_models 129 | model = PlainModel.new 130 | assert_equal 0, model.class.count 131 | model.save! 132 | assert_equal 1, model.class.count 133 | model.destroy 134 | 135 | assert_equal true, model.deleted_at.nil? 136 | 137 | assert_equal 0, model.class.count 138 | assert_equal 0, model.class.unscoped.count 139 | end 140 | 141 | # Anti-regression test for #81, which would've introduced a bug to break this test. 142 | def test_destroy_behavior_for_plain_models_callbacks 143 | model = CallbackModel.new 144 | model.save 145 | model.remove_called_variables # clear called callback flags 146 | model.destroy 147 | 148 | assert_nil model.instance_variable_get(:@update_callback_called) 149 | assert_nil model.instance_variable_get(:@save_callback_called) 150 | assert_nil model.instance_variable_get(:@validate_called) 151 | 152 | assert model.instance_variable_get(:@destroy_callback_called) 153 | assert model.instance_variable_get(:@after_destroy_callback_called) 154 | assert model.instance_variable_get(:@after_commit_callback_called) 155 | end 156 | 157 | def test_destroy_behavior_for_association_with_abort 158 | model = AssociationWithAbortModel.new 159 | model.related_models.build 160 | model.save 161 | 162 | assert_equal model.reload.related_models.count, 1 163 | 164 | model = AssociationWithAbortModel.find(model.id) 165 | return_value = model.destroy 166 | 167 | assert_equal return_value, false 168 | assert_equal model.reload.related_models.count, 1 169 | end 170 | 171 | def test_destroy_bang_behavior_for_association_with_abort 172 | model = AssociationWithAbortModel.new 173 | model.related_models.build 174 | model.save 175 | 176 | assert_equal model.reload.related_models.count, 1 177 | 178 | model = AssociationWithAbortModel.find(model.id) 179 | assert_raises ActiveRecord::RecordNotDestroyed do 180 | model.destroy! 181 | end 182 | 183 | assert_equal model.reload.related_models.count, 1 184 | end 185 | 186 | def test_destroy_behavior_for_freshly_loaded_plain_models_callbacks 187 | model = CallbackModel.new 188 | model.save 189 | 190 | model = CallbackModel.find(model.id) 191 | model.destroy 192 | 193 | assert_nil model.instance_variable_get(:@update_callback_called) 194 | assert_nil model.instance_variable_get(:@save_callback_called) 195 | assert_nil model.instance_variable_get(:@validate_called) 196 | 197 | assert model.instance_variable_get(:@destroy_callback_called) 198 | assert model.instance_variable_get(:@after_destroy_callback_called) 199 | assert model.instance_variable_get(:@after_commit_callback_called) 200 | end 201 | 202 | def test_destroy_behavior_for_freshly_saved_models_after_commit_callbacks 203 | model = AfterCommitCallbackModel.create! 204 | 205 | assert_equal 1, model.after_create_commit_called_times 206 | assert_equal 0, model.after_destroy_commit_called_times 207 | 208 | # clear the counters, but do not reload from DB 209 | model.remove_called_variables 210 | 211 | model.destroy 212 | assert_equal 0, model.after_create_commit_called_times 213 | assert_equal 1, model.after_destroy_commit_called_times 214 | end 215 | 216 | def test_delete_behavior_for_plain_models_callbacks 217 | model = CallbackModel.new 218 | model.save 219 | model.remove_called_variables # clear called callback flags 220 | model.delete 221 | 222 | assert_nil model.instance_variable_get(:@update_callback_called) 223 | assert_nil model.instance_variable_get(:@save_callback_called) 224 | assert_nil model.instance_variable_get(:@validate_called) 225 | assert_nil model.instance_variable_get(:@destroy_callback_called) 226 | assert_nil model.instance_variable_get(:@after_destroy_callback_called) 227 | assert_nil model.instance_variable_get(:@after_commit_callback_called) 228 | end 229 | 230 | def test_delete_in_transaction_behavior_for_plain_models_callbacks 231 | model = CallbackModel.new 232 | model.save 233 | model.remove_called_variables # clear called callback flags 234 | CallbackModel.transaction do 235 | model.delete 236 | end 237 | 238 | assert_nil model.instance_variable_get(:@update_callback_called) 239 | assert_nil model.instance_variable_get(:@save_callback_called) 240 | assert_nil model.instance_variable_get(:@validate_called) 241 | assert_nil model.instance_variable_get(:@destroy_callback_called) 242 | assert_nil model.instance_variable_get(:@after_destroy_callback_called) 243 | assert model.instance_variable_get(:@after_commit_callback_called) 244 | end 245 | 246 | def test_destroy_behavior_for_paranoid_models 247 | model = ParanoidModel.new 248 | assert_equal 0, model.class.count 249 | model.save! 250 | assert_equal 1, model.class.count 251 | model.destroy 252 | 253 | assert_equal false, model.deleted_at.nil? 254 | 255 | assert_equal 0, model.class.count 256 | assert_equal 1, model.class.unscoped.count 257 | end 258 | 259 | def test_update_columns_on_paranoia_destroyed 260 | record = ParentModel.create 261 | record.destroy 262 | 263 | assert record.update_columns deleted_at: Time.now 264 | end 265 | 266 | def test_scoping_behavior_for_paranoid_models 267 | parent1 = ParentModel.create 268 | parent2 = ParentModel.create 269 | p1 = ParanoidModel.create(:parent_model => parent1) 270 | p2 = ParanoidModel.create(:parent_model => parent2) 271 | p1.destroy 272 | p2.destroy 273 | 274 | assert_equal 0, parent1.paranoid_models.count 275 | assert_equal 1, parent1.paranoid_models.only_deleted.count 276 | 277 | assert_equal 2, ParanoidModel.only_deleted.joins(:parent_model).count 278 | assert_equal 1, parent1.paranoid_models.deleted.count 279 | assert_equal 0, parent1.paranoid_models.without_deleted.count 280 | p3 = ParanoidModel.create(:parent_model => parent1) 281 | assert_equal 2, parent1.paranoid_models.with_deleted.count 282 | assert_equal 1, parent1.paranoid_models.without_deleted.count 283 | assert_equal [p1,p3], parent1.paranoid_models.with_deleted 284 | end 285 | 286 | def test_paranoid_model_has_many_active_column_model 287 | parent1 = ParentModel.create 288 | p1 = ParanoidModel.create(:parent_model => parent1) 289 | acm1 = ActiveColumnModel.create(paranoid_model: p1) 290 | 291 | assert_nil p1.reload.deleted_at 292 | assert_equal 1, p1.active_column_models.count 293 | assert_equal true, acm1.active 294 | assert_nil acm1.deleted_at 295 | 296 | p1.destroy 297 | 298 | assert p1.reload.deleted_at != nil 299 | assert_equal 0, p1.active_column_models.count 300 | assert_nil acm1.reload.active 301 | assert acm1.reload.deleted_at != nil 302 | 303 | p1.restore(recursive: true, recovery_window: 10.minutes) 304 | 305 | assert_nil p1.reload.deleted_at 306 | assert_equal 1, p1.active_column_models.count 307 | assert_equal true, acm1.reload.active 308 | assert_nil acm1.reload.deleted_at 309 | end 310 | 311 | def test_only_deleted_with_joins 312 | c1 = ActiveColumnModelWithHasManyRelationship.create(name: 'Jacky') 313 | c2 = ActiveColumnModelWithHasManyRelationship.create(name: 'Thomas') 314 | p1 = ParanoidModelWithBelongsToActiveColumnModelWithHasManyRelationship.create(name: 'Hello', active_column_model_with_has_many_relationship: c1) 315 | 316 | c1.destroy 317 | assert_equal 1, ActiveColumnModelWithHasManyRelationship.count 318 | assert_equal 1, ActiveColumnModelWithHasManyRelationship.only_deleted.count 319 | assert_equal 1, ActiveColumnModelWithHasManyRelationship.only_deleted.joins(:paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships).count 320 | end 321 | 322 | def test_destroy_behavior_for_custom_column_models 323 | model = CustomColumnModel.new 324 | assert_equal 0, model.class.count 325 | model.save! 326 | assert_nil model.destroyed_at 327 | assert_equal 1, model.class.count 328 | model.destroy 329 | 330 | assert_equal false, model.destroyed_at.nil? 331 | assert model.paranoia_destroyed? 332 | 333 | assert_equal 0, model.class.count 334 | assert_equal 1, model.class.unscoped.count 335 | assert_equal 1, model.class.only_deleted.count 336 | assert_equal 1, model.class.deleted.count 337 | end 338 | 339 | def test_destroy_behavior_for_custom_column_models_with_recovery_options 340 | model = CustomColumnModel.new 341 | model.save! 342 | 343 | assert_nil model.destroyed_at 344 | 345 | model.destroy 346 | 347 | assert_equal false, model.destroyed_at.nil? 348 | assert model.paranoia_destroyed? 349 | 350 | model.restore!(recovery_window: 2.minutes) 351 | 352 | assert_equal 1, model.class.count 353 | end 354 | 355 | def test_default_sentinel_value 356 | assert_nil ParanoidModel.paranoia_sentinel_value 357 | end 358 | 359 | def test_without_default_scope_option 360 | model = WithoutDefaultScopeModel.create 361 | model.destroy 362 | assert_equal 1, model.class.count 363 | assert_equal 1, model.class.only_deleted.count 364 | assert_equal 0, model.class.where(deleted_at: nil).count 365 | end 366 | 367 | def test_active_column_model 368 | model = ActiveColumnModel.new 369 | assert_equal 0, model.class.count 370 | model.save! 371 | assert_nil model.deleted_at 372 | assert_equal true, model.active 373 | assert_equal 1, model.class.count 374 | model.destroy 375 | 376 | assert_equal false, model.deleted_at.nil? 377 | assert_nil model.active 378 | assert model.paranoia_destroyed? 379 | 380 | assert_equal 0, model.class.count 381 | assert_equal 1, model.class.unscoped.count 382 | assert_equal 1, model.class.only_deleted.count 383 | assert_equal 1, model.class.deleted.count 384 | end 385 | 386 | def test_active_column_model_with_uniqueness_validation_only_checks_non_deleted_records 387 | a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") 388 | a.destroy 389 | b = ActiveColumnModelWithUniquenessValidation.new(name: "A") 390 | assert b.valid? 391 | end 392 | 393 | def test_active_column_model_with_uniqueness_validation_still_works_on_non_deleted_records 394 | a = ActiveColumnModelWithUniquenessValidation.create!(name: "A") 395 | b = ActiveColumnModelWithUniquenessValidation.new(name: "A") 396 | refute b.valid? 397 | end 398 | 399 | def test_sentinel_value_for_custom_sentinel_models 400 | time_zero = if ActiveRecord::VERSION::MAJOR < 6 401 | Time.new(0) 402 | elsif ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR < 1 403 | Time.new(0) 404 | else 405 | DateTime.new(0) 406 | end 407 | 408 | model = CustomSentinelModel.new 409 | assert_equal 0, model.class.count 410 | model.save! 411 | assert_equal time_zero, model.deleted_at 412 | assert_equal 1, model.class.count 413 | model.destroy 414 | 415 | assert time_zero != model.deleted_at 416 | assert model.paranoia_destroyed? 417 | 418 | assert_equal 0, model.class.count 419 | assert_equal 1, model.class.unscoped.count 420 | assert_equal 1, model.class.only_deleted.count 421 | assert_equal 1, model.class.deleted.count 422 | 423 | model.restore 424 | assert_equal time_zero, model.deleted_at 425 | assert !model.destroyed? 426 | 427 | assert_equal 1, model.class.count 428 | assert_equal 1, model.class.unscoped.count 429 | assert_equal 0, model.class.only_deleted.count 430 | assert_equal 0, model.class.deleted.count 431 | end 432 | 433 | def test_destroy_behavior_for_featureful_paranoid_models 434 | model = get_featureful_model 435 | assert_equal 0, model.class.count 436 | model.save! 437 | assert_equal 1, model.class.count 438 | model.destroy 439 | 440 | assert_equal false, model.deleted_at.nil? 441 | 442 | assert_equal 0, model.class.count 443 | assert_equal 1, model.class.unscoped.count 444 | end 445 | 446 | def test_destroy_behavior_for_has_one_with_build_and_validation_error 447 | model = ParanoidModelWithHasOneAndBuild.create 448 | model.destroy 449 | end 450 | 451 | # Regression test for #24 452 | def test_chaining_for_paranoid_models 453 | scope = FeaturefulModel.where(:name => "foo").only_deleted 454 | assert_equal({'name' => "foo"}, scope.where_values_hash) 455 | end 456 | 457 | def test_only_destroyed_scope_for_paranoid_models 458 | model = ParanoidModel.new 459 | model.save 460 | model.destroy 461 | model2 = ParanoidModel.new 462 | model2.save 463 | 464 | assert_equal model, ParanoidModel.only_deleted.last 465 | assert_equal false, ParanoidModel.only_deleted.include?(model2) 466 | end 467 | 468 | def test_default_scope_for_has_many_relationships 469 | parent = ParentModel.create 470 | assert_equal 0, parent.related_models.count 471 | 472 | child = parent.related_models.create 473 | assert_equal 1, parent.related_models.count 474 | 475 | child.destroy 476 | assert_equal false, child.deleted_at.nil? 477 | 478 | assert_equal 0, parent.related_models.count 479 | assert_equal 1, parent.related_models.unscoped.count 480 | end 481 | 482 | def test_default_scope_for_has_many_through_relationships 483 | employer = Employer.create 484 | employee = Employee.create 485 | assert_equal 0, employer.jobs.count 486 | assert_equal 0, employer.employees.count 487 | assert_equal 0, employee.jobs.count 488 | assert_equal 0, employee.employers.count 489 | 490 | job = Job.create :employer => employer, :employee => employee 491 | assert_equal 1, employer.jobs.count 492 | assert_equal 1, employer.employees.count 493 | assert_equal 1, employee.jobs.count 494 | assert_equal 1, employee.employers.count 495 | 496 | employee2 = Employee.create 497 | job2 = Job.create :employer => employer, :employee => employee2 498 | employee2.destroy 499 | assert_equal 2, employer.jobs.count 500 | assert_equal 1, employer.employees.count 501 | 502 | job.destroy 503 | assert_equal 1, employer.jobs.count 504 | assert_equal 0, employer.employees.count 505 | assert_equal 0, employee.jobs.count 506 | assert_equal 0, employee.employers.count 507 | end 508 | 509 | def test_delete_behavior_for_callbacks 510 | model = CallbackModel.new 511 | model.save 512 | model.delete 513 | assert_nil model.instance_variable_get(:@destroy_callback_called) 514 | end 515 | 516 | def test_destroy_behavior_for_callbacks 517 | model = CallbackModel.new 518 | model.save 519 | model.destroy 520 | assert model.instance_variable_get(:@destroy_callback_called) 521 | end 522 | 523 | def test_destroy_on_readonly_record 524 | # Just to demonstrate the AR behaviour 525 | model = NonParanoidModel.create! 526 | model.readonly! 527 | assert_raises ActiveRecord::ReadOnlyRecord do 528 | model.destroy 529 | end 530 | 531 | # Mirrors behaviour above 532 | model = ParanoidModel.create! 533 | model.readonly! 534 | assert_raises ActiveRecord::ReadOnlyRecord do 535 | model.destroy 536 | end 537 | end 538 | 539 | def test_destroy_on_really_destroyed_record 540 | model = ParanoidModel.create! 541 | model.really_destroy! 542 | assert model.really_destroyed? 543 | assert model.paranoia_destroyed? 544 | model.destroy 545 | assert model.really_destroyed? 546 | assert model.paranoia_destroyed? 547 | end 548 | 549 | def test_destroy_on_unsaved_record 550 | # Just to demonstrate the AR behaviour 551 | model = NonParanoidModel.new 552 | model.destroy! 553 | assert model.destroyed? 554 | model.destroy! 555 | assert model.destroyed? 556 | 557 | # Mirrors behaviour above 558 | model = ParanoidModel.new 559 | model.destroy! 560 | assert model.paranoia_destroyed? 561 | model.destroy! 562 | assert model.paranoia_destroyed? 563 | end 564 | 565 | def test_restore 566 | model = ParanoidModel.new 567 | model.save 568 | id = model.id 569 | model.destroy 570 | 571 | assert model.paranoia_destroyed? 572 | 573 | model = ParanoidModel.only_deleted.find(id) 574 | model.restore! 575 | model.reload 576 | 577 | assert_equal false, model.paranoia_destroyed? 578 | end 579 | 580 | def test_restore_on_object_return_self 581 | model = ParanoidModel.create 582 | model.destroy 583 | 584 | assert_equal model.class, model.restore.class 585 | end 586 | 587 | # Regression test for #92 588 | def test_destroy_twice 589 | model = ParanoidModel.new 590 | model.save 591 | model.destroy 592 | model.destroy 593 | 594 | assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count 595 | end 596 | 597 | # Regression test for #92 598 | def test_destroy_bang_twice 599 | model = ParanoidModel.new 600 | model.save! 601 | model.destroy! 602 | model.destroy! 603 | 604 | assert_equal 1, ParanoidModel.unscoped.where(id: model.id).count 605 | end 606 | 607 | def test_destroy_return_value_on_success 608 | model = ParanoidModel.create 609 | return_value = model.destroy 610 | 611 | assert_equal(return_value, model) 612 | end 613 | 614 | def test_destroy_return_value_on_failure 615 | model = FailCallbackModel.create 616 | return_value = model.destroy 617 | 618 | assert_equal(return_value, false) 619 | end 620 | 621 | def test_restore_behavior_for_callbacks 622 | model = CallbackModel.new 623 | model.save 624 | id = model.id 625 | model.destroy 626 | 627 | assert model.paranoia_destroyed? 628 | 629 | model = CallbackModel.only_deleted.find(id) 630 | model.restore! 631 | model.reload 632 | 633 | assert model.instance_variable_get(:@restore_callback_called) 634 | assert_nil model.instance_variable_get(:@after_commit_callback_called) 635 | end 636 | 637 | def test_after_commit_on_restore 638 | model = AfterCommitOnRestoreCallbackModel.new 639 | model.save 640 | id = model.id 641 | model.destroy 642 | 643 | assert model.paranoia_destroyed? 644 | 645 | model = AfterCommitOnRestoreCallbackModel.only_deleted.find(id) 646 | model.restore! 647 | model.reload 648 | 649 | assert model.instance_variable_get(:@restore_callback_called) 650 | assert model.instance_variable_get(:@after_restore_callback_called) 651 | assert model.instance_variable_get(:@after_restore_commit_callback_called) 652 | end 653 | 654 | def test_after_restore_commit 655 | model = AfterRestoreCommitCallbackModel.new 656 | model.save 657 | id = model.id 658 | model.destroy 659 | 660 | assert model.paranoia_destroyed? 661 | 662 | model = AfterRestoreCommitCallbackModel.only_deleted.find(id) 663 | model.restore! 664 | model.reload 665 | 666 | assert model.instance_variable_get(:@restore_callback_called) 667 | assert model.instance_variable_get(:@after_restore_callback_called) 668 | assert model.instance_variable_get(:@after_restore_commit_callback_called) 669 | end 670 | 671 | def test_after_restore_commit_once 672 | model = AfterRestoreCommitCallbackModel.new 673 | model.save 674 | id = model.id 675 | model.destroy 676 | 677 | assert model.paranoia_destroyed? 678 | assert model.instance_variable_get(:@after_destroy_commit_callback_called) 679 | 680 | model.remove_called_variables 681 | model = AfterRestoreCommitCallbackModel.only_deleted.find(id) 682 | model.restore! 683 | model.reload 684 | 685 | assert model.instance_variable_get(:@restore_callback_called) 686 | assert model.instance_variable_get(:@after_restore_callback_called) 687 | assert model.instance_variable_get(:@after_restore_commit_callback_called) 688 | assert_nil model.instance_variable_get(:@after_destroy_commit_callback_called) 689 | 690 | model.remove_called_variables 691 | model.destroy 692 | assert model.instance_variable_get(:@after_destroy_commit_callback_called) 693 | assert_nil model.instance_variable_get(:@after_restore_commit_callback_called) 694 | end 695 | 696 | def test_after_commit_restore_enabled 697 | model = AfterCommitCallbackRestoreEnabledModel.new 698 | model.save 699 | id = model.id 700 | model.destroy 701 | 702 | assert model.paranoia_destroyed? 703 | 704 | model = AfterCommitCallbackRestoreEnabledModel.only_deleted.find(id) 705 | model.restore! 706 | model.reload 707 | 708 | assert model.instance_variable_get(:@restore_callback_called) 709 | assert model.instance_variable_get(:@after_restore_callback_called) 710 | assert model.instance_variable_get(:@after_commit_callback_called) 711 | end 712 | 713 | def test_not_call_after_other_commit_restore_enabled 714 | model = AfterOtherCommitCallbackRestoreEnabledModel.new 715 | model.save 716 | id = model.id 717 | model.destroy 718 | 719 | assert model.paranoia_destroyed? 720 | 721 | model = AfterOtherCommitCallbackRestoreEnabledModel.only_deleted.find(id) 722 | model.restore! 723 | model.reload 724 | 725 | assert model.instance_variable_get(:@restore_callback_called) 726 | assert model.instance_variable_get(:@after_restore_callback_called) 727 | assert_nil model.instance_variable_get(:@after_other_commit_callback_called) 728 | end 729 | 730 | def test_really_destroy 731 | model = ParanoidModel.new 732 | model.save 733 | model.really_destroy! 734 | refute ParanoidModel.unscoped.exists?(model.id) 735 | end 736 | 737 | def test_real_destroy_dependent_destroy 738 | parent = ParentModel.create 739 | child1 = parent.very_related_models.create 740 | child2 = parent.non_paranoid_models.create 741 | child3 = parent.create_non_paranoid_model 742 | 743 | parent.really_destroy! 744 | 745 | refute RelatedModel.unscoped.exists?(child1.id) 746 | refute NonParanoidModel.unscoped.exists?(child2.id) 747 | refute NonParanoidModel.unscoped.exists?(child3.id) 748 | end 749 | 750 | def test_not_destroy_child_if_abort_destroy 751 | parent = ParentModel.create 752 | child = parent.very_related_models.create 753 | parent.destroy_unavailable = true 754 | parent.destroy 755 | 756 | assert_nil parent.reload.deleted_at, "Parent must be not deleted" 757 | assert_nil child.reload.deleted_at, "Child must be not deleted" 758 | end 759 | 760 | def test_real_destroy_dependent_destroy_after_normal_destroy 761 | parent = ParentModel.create 762 | child = parent.very_related_models.create 763 | parent.destroy 764 | parent.really_destroy! 765 | refute RelatedModel.unscoped.exists?(child.id) 766 | end 767 | 768 | def test_real_destroy_dependent_destroy_after_normal_destroy_does_not_delete_other_children 769 | parent_1 = ParentModel.create 770 | child_1 = parent_1.very_related_models.create 771 | 772 | parent_2 = ParentModel.create 773 | child_2 = parent_2.very_related_models.create 774 | parent_1.destroy 775 | parent_1.really_destroy! 776 | assert RelatedModel.unscoped.exists?(child_2.id) 777 | end 778 | 779 | def test_really_destroy_behavior_for_callbacks 780 | model = CallbackModel.new 781 | model.save 782 | model.really_destroy! 783 | 784 | assert model.instance_variable_get(:@real_destroy_callback_called) 785 | end 786 | 787 | def test_really_destroy_behavior_for_active_column_model 788 | model = ActiveColumnModel.new 789 | model.save 790 | model.really_destroy! 791 | 792 | refute ParanoidModel.unscoped.exists?(model.id) 793 | end 794 | 795 | def test_really_delete 796 | model = ParanoidModel.new 797 | model.save 798 | model.really_delete 799 | 800 | refute ParanoidModel.unscoped.exists?(model.id) 801 | end 802 | 803 | def test_multiple_restore 804 | a = ParanoidModel.new 805 | a.save 806 | a_id = a.id 807 | a.destroy 808 | 809 | b = ParanoidModel.new 810 | b.save 811 | b_id = b.id 812 | b.destroy 813 | 814 | c = ParanoidModel.new 815 | c.save 816 | c_id = c.id 817 | c.destroy 818 | 819 | ParanoidModel.restore([a_id, c_id]) 820 | 821 | a.reload 822 | b.reload 823 | c.reload 824 | 825 | refute a.paranoia_destroyed? 826 | assert b.paranoia_destroyed? 827 | refute c.paranoia_destroyed? 828 | end 829 | 830 | def test_restore_with_associations_using_recovery_window 831 | parent = ParentModel.create 832 | first_child = parent.very_related_models.create 833 | second_child = parent.very_related_models.create 834 | 835 | parent.destroy 836 | second_child.update(deleted_at: parent.deleted_at + 11.minutes) 837 | 838 | parent.restore!(:recursive => true) 839 | assert_equal true, parent.deleted_at.nil? 840 | assert_equal true, first_child.reload.deleted_at.nil? 841 | assert_equal true, second_child.reload.deleted_at.nil? 842 | 843 | parent.destroy 844 | second_child.update(deleted_at: parent.deleted_at + 11.minutes) 845 | 846 | parent.restore(:recursive => true, :recovery_window => 10.minutes) 847 | assert_equal true, parent.deleted_at.nil? 848 | assert_equal true, first_child.reload.deleted_at.nil? 849 | assert_equal false, second_child.reload.deleted_at.nil? 850 | 851 | second_child.restore 852 | parent.destroy 853 | first_child.update(deleted_at: parent.deleted_at - 11.minutes) 854 | second_child.update(deleted_at: parent.deleted_at - 9.minutes) 855 | 856 | ParentModel.restore(parent.id, :recursive => true, :recovery_window => 10.minutes) 857 | assert_equal true, parent.reload.deleted_at.nil? 858 | assert_equal false, first_child.reload.deleted_at.nil? 859 | assert_equal true, second_child.reload.deleted_at.nil? 860 | end 861 | 862 | def test_restore_with_associations 863 | parent = ParentModel.create 864 | first_child = parent.very_related_models.create 865 | second_child = parent.non_paranoid_models.create 866 | 867 | parent.destroy 868 | assert_equal false, parent.deleted_at.nil? 869 | assert_equal false, first_child.reload.deleted_at.nil? 870 | assert_equal true, second_child.destroyed? 871 | 872 | parent.restore! 873 | assert_equal true, parent.deleted_at.nil? 874 | assert_equal false, first_child.reload.deleted_at.nil? 875 | assert_equal true, second_child.destroyed? 876 | 877 | parent.destroy 878 | parent.restore(:recursive => true) 879 | assert_equal true, parent.deleted_at.nil? 880 | assert_equal true, first_child.reload.deleted_at.nil? 881 | assert_equal true, second_child.destroyed? 882 | 883 | parent.destroy 884 | ParentModel.restore(parent.id, :recursive => true) 885 | assert_equal true, parent.reload.deleted_at.nil? 886 | assert_equal true, first_child.reload.deleted_at.nil? 887 | assert_equal true, second_child.destroyed? 888 | end 889 | 890 | # regression tests for #118 891 | def test_restore_with_has_one_association 892 | # setup and destroy test objects 893 | hasOne = ParanoidModelWithHasOne.create 894 | belongsTo = ParanoidModelWithBelong.create 895 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 896 | foreignKey = ParanoidModelWithForeignKeyBelong.create 897 | notParanoidModel = NotParanoidModelWithBelong.create 898 | 899 | hasOne.paranoid_model_with_belong = belongsTo 900 | hasOne.class_name_belong = anthorClassName 901 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 902 | hasOne.not_paranoid_model_with_belong = notParanoidModel 903 | hasOne.save! 904 | 905 | hasOne.destroy 906 | assert_equal false, hasOne.deleted_at.nil? 907 | assert_equal false, belongsTo.deleted_at.nil? 908 | 909 | # Does it restore has_one associations? 910 | hasOne.restore(:recursive => true) 911 | hasOne.save! 912 | 913 | assert_equal true, hasOne.reload.deleted_at.nil? 914 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 915 | assert_equal true, notParanoidModel.destroyed? 916 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 917 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 918 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 919 | end 920 | 921 | def test_new_restore_with_has_one_association 922 | # setup and destroy test objects 923 | hasOne = ParanoidModelWithHasOne.create 924 | belongsTo = ParanoidModelWithBelong.create 925 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 926 | foreignKey = ParanoidModelWithForeignKeyBelong.create 927 | notParanoidModel = NotParanoidModelWithBelong.create 928 | 929 | hasOne.paranoid_model_with_belong = belongsTo 930 | hasOne.class_name_belong = anthorClassName 931 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 932 | hasOne.not_paranoid_model_with_belong = notParanoidModel 933 | hasOne.save! 934 | 935 | hasOne.destroy 936 | assert_equal false, hasOne.deleted_at.nil? 937 | assert_equal false, belongsTo.deleted_at.nil? 938 | 939 | # Does it restore has_one associations? 940 | newHasOne = ParanoidModelWithHasOne.with_deleted.find(hasOne.id) 941 | newHasOne.restore(:recursive => true) 942 | newHasOne.save! 943 | 944 | assert_equal true, hasOne.reload.deleted_at.nil? 945 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 946 | assert_equal true, notParanoidModel.destroyed? 947 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 948 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 949 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 950 | end 951 | 952 | def test_model_restore_with_has_one_association 953 | # setup and destroy test objects 954 | hasOne = ParanoidModelWithHasOne.create 955 | belongsTo = ParanoidModelWithBelong.create 956 | anthorClassName = ParanoidModelWithAnthorClassNameBelong.create 957 | foreignKey = ParanoidModelWithForeignKeyBelong.create 958 | notParanoidModel = NotParanoidModelWithBelong.create 959 | 960 | hasOne.paranoid_model_with_belong = belongsTo 961 | hasOne.class_name_belong = anthorClassName 962 | hasOne.paranoid_model_with_foreign_key_belong = foreignKey 963 | hasOne.not_paranoid_model_with_belong = notParanoidModel 964 | hasOne.save! 965 | 966 | hasOne.destroy 967 | assert_equal false, hasOne.deleted_at.nil? 968 | assert_equal false, belongsTo.deleted_at.nil? 969 | 970 | # Does it restore has_one associations? 971 | ParanoidModelWithHasOne.restore(hasOne.id, :recursive => true) 972 | hasOne.save! 973 | 974 | assert_equal true, hasOne.reload.deleted_at.nil? 975 | assert_equal true, belongsTo.reload.deleted_at.nil?, "#{belongsTo.deleted_at}" 976 | assert_equal true, notParanoidModel.destroyed? 977 | assert ParanoidModelWithBelong.with_deleted.reload.count != 0, "There should be a record" 978 | assert ParanoidModelWithAnthorClassNameBelong.with_deleted.reload.count != 0, "There should be an other record" 979 | assert ParanoidModelWithForeignKeyBelong.with_deleted.reload.count != 0, "There should be a foreign_key record" 980 | end 981 | 982 | def test_restore_with_nil_has_one_association 983 | # setup and destroy test object 984 | hasOne = ParanoidModelWithHasOne.create 985 | hasOne.destroy 986 | assert_equal false, hasOne.reload.deleted_at.nil? 987 | 988 | # Does it raise NoMethodException on restore of nil 989 | hasOne.restore(:recursive => true) 990 | 991 | assert hasOne.reload.deleted_at.nil? 992 | end 993 | 994 | def test_restore_with_module_scoped_has_one_association 995 | # setup and destroy test object 996 | hasOne = Namespaced::ParanoidHasOne.create 997 | hasOne.destroy 998 | assert_equal false, hasOne.reload.deleted_at.nil? 999 | 1000 | # Does it raise "uninitialized constant ParanoidBelongsTo" 1001 | # on restore of ParanoidHasOne? 1002 | hasOne.restore(:recursive => true) 1003 | 1004 | assert hasOne.reload.deleted_at.nil? 1005 | end 1006 | 1007 | # covers #185 1008 | def test_restoring_recursive_has_one_restores_correct_object 1009 | hasOnes = 2.times.map { ParanoidModelWithHasOne.create } 1010 | belongsTos = 2.times.map { ParanoidModelWithBelong.create } 1011 | 1012 | hasOnes[0].update paranoid_model_with_belong: belongsTos[0] 1013 | hasOnes[1].update paranoid_model_with_belong: belongsTos[1] 1014 | 1015 | hasOnes.each(&:destroy) 1016 | 1017 | ParanoidModelWithHasOne.restore(hasOnes[1].id, :recursive => true) 1018 | hasOnes.each(&:reload) 1019 | belongsTos.each(&:reload) 1020 | 1021 | # without #185, belongsTos[0] will be restored instead of belongsTos[1] 1022 | refute_nil hasOnes[0].deleted_at 1023 | refute_nil belongsTos[0].deleted_at 1024 | assert_nil hasOnes[1].deleted_at 1025 | assert_nil belongsTos[1].deleted_at 1026 | end 1027 | 1028 | # covers #131 1029 | def test_has_one_really_destroy_with_nil 1030 | model = ParanoidModelWithHasOne.create 1031 | model.really_destroy! 1032 | 1033 | refute ParanoidModelWithBelong.unscoped.exists?(model.id) 1034 | end 1035 | 1036 | def test_has_one_really_destroy_with_record 1037 | model = ParanoidModelWithHasOne.create { |record| record.build_paranoid_model_with_belong } 1038 | model.really_destroy! 1039 | 1040 | refute ParanoidModelWithBelong.unscoped.exists?(model.id) 1041 | end 1042 | 1043 | def test_observers_notified 1044 | a = ParanoidModelWithObservers.create 1045 | a.destroy 1046 | a.restore! 1047 | 1048 | assert a.observers_notified.select {|args| args == [:before_restore, a]} 1049 | assert a.observers_notified.select {|args| args == [:after_restore, a]} 1050 | end 1051 | 1052 | def test_observers_not_notified_if_not_supported 1053 | a = ParanoidModelWithObservers.create 1054 | a.destroy 1055 | a.restore! 1056 | # essentially, we're just ensuring that this doesn't crash 1057 | end 1058 | 1059 | def test_validates_uniqueness_only_checks_non_deleted_records 1060 | a = Employer.create!(name: "A") 1061 | a.destroy 1062 | b = Employer.new(name: "A") 1063 | assert b.valid? 1064 | end 1065 | 1066 | def test_validates_uniqueness_still_works_on_non_deleted_records 1067 | a = Employer.create!(name: "A") 1068 | b = Employer.new(name: "A") 1069 | refute b.valid? 1070 | end 1071 | 1072 | def test_updated_at_modification_on_destroy 1073 | paranoid_model = ParanoidModelWithTimestamp.create(:parent_model => ParentModel.create, :updated_at => 1.day.ago) 1074 | assert paranoid_model.updated_at < 10.minutes.ago 1075 | paranoid_model.destroy 1076 | assert paranoid_model.updated_at > 10.minutes.ago 1077 | end 1078 | 1079 | def test_updated_at_modification_on_restore 1080 | parent1 = ParentModel.create 1081 | pt1 = ParanoidModelWithTimestamp.create(:parent_model => parent1) 1082 | ParanoidModelWithTimestamp.record_timestamps = false 1083 | pt1.update_columns(created_at: 20.years.ago, updated_at: 10.years.ago, deleted_at: 10.years.ago) 1084 | ParanoidModelWithTimestamp.record_timestamps = true 1085 | assert pt1.updated_at < 10.minutes.ago 1086 | refute pt1.deleted_at.nil? 1087 | pt1.restore! 1088 | assert pt1.deleted_at.nil? 1089 | assert pt1.updated_at > 10.minutes.ago 1090 | end 1091 | 1092 | def test_i_am_the_destroyer 1093 | expected = %Q{ 1094 | Sharon: "There should be a method called I_AM_THE_DESTROYER!" 1095 | Ryan: "What should this method do?" 1096 | Sharon: "It should fix all the spelling errors on the page!" 1097 | } 1098 | assert_output expected do 1099 | ParanoidModel.I_AM_THE_DESTROYER! 1100 | end 1101 | end 1102 | 1103 | def test_destroy_fails_if_callback_raises_exception 1104 | parent = AsplodeModel.create 1105 | 1106 | assert_raises(StandardError) { parent.destroy } 1107 | 1108 | #transaction should be rolled back, so parent NOT deleted 1109 | refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' 1110 | end 1111 | 1112 | def test_destroy_fails_if_association_callback_raises_exception 1113 | parent = ParentModel.create 1114 | children = [] 1115 | 3.times { children << parent.asplode_models.create } 1116 | 1117 | assert_raises(StandardError) { parent.destroy } 1118 | 1119 | #transaction should be rolled back, so parent and children NOT deleted 1120 | refute parent.destroyed?, 'Parent record was destroyed, even though AR callback threw exception' 1121 | refute children.any?(&:destroyed?), 'Child record was destroyed, even though AR callback threw exception' 1122 | end 1123 | 1124 | def test_restore_model_with_different_connection 1125 | ActiveRecord::Base.remove_connection # Disconnect the main connection 1126 | a = WithDifferentConnection.create 1127 | a.destroy! 1128 | a.restore! 1129 | # This test passes if no exception is raised 1130 | ensure 1131 | setup! # Reconnect the main connection 1132 | end 1133 | 1134 | def test_restore_clear_association_cache_if_associations_present 1135 | parent = ParentModel.create 1136 | 3.times { parent.very_related_models.create } 1137 | 1138 | parent.destroy 1139 | 1140 | assert_equal 0, parent.very_related_models.count 1141 | assert_equal 0, parent.very_related_models.size 1142 | 1143 | parent.restore(recursive: true) 1144 | 1145 | assert_equal 3, parent.very_related_models.count 1146 | assert_equal 3, parent.very_related_models.size 1147 | end 1148 | 1149 | def test_model_without_db_connection 1150 | ActiveRecord::Base.remove_connection 1151 | 1152 | NoConnectionModel.class_eval{ acts_as_paranoid } 1153 | ensure 1154 | setup! 1155 | end 1156 | 1157 | def test_restore_recursive_on_polymorphic_has_one_association 1158 | parent = ParentModel.create 1159 | polymorphic = PolymorphicModel.create(parent: parent) 1160 | 1161 | parent.destroy 1162 | 1163 | assert_equal 0, polymorphic.class.count 1164 | 1165 | parent.restore(recursive: true) 1166 | 1167 | assert_equal 1, polymorphic.class.count 1168 | end 1169 | 1170 | def test_recursive_restore_with_has_through_associations 1171 | parent = ParanoidHasThroughRestoreParent.create 1172 | one = EmptyParanoidModel.create 1173 | ParanoidHasOneThrough.create( 1174 | :paranoid_has_through_restore_parent => parent, 1175 | :empty_paranoid_model => one, 1176 | ) 1177 | many = Array.new(3) do 1178 | many = EmptyParanoidModel.create 1179 | ParanoidHasManyThrough.create( 1180 | :paranoid_has_through_restore_parent => parent, 1181 | :empty_paranoid_model => many, 1182 | ) 1183 | 1184 | many 1185 | end 1186 | 1187 | assert_equal true, parent.empty_paranoid_model.present? 1188 | assert_equal 3, parent.empty_paranoid_models.count 1189 | 1190 | parent.destroy 1191 | 1192 | assert_equal true, parent.empty_paranoid_model.reload.deleted? 1193 | assert_equal 0, parent.empty_paranoid_models.count 1194 | 1195 | parent = ParanoidHasThroughRestoreParent.with_deleted.first 1196 | parent.restore(recursive: true) 1197 | 1198 | assert_equal false, parent.empty_paranoid_model.deleted? 1199 | assert_equal one, parent.empty_paranoid_model 1200 | assert_equal 3, parent.empty_paranoid_models.count 1201 | assert_equal many, parent.empty_paranoid_models 1202 | end 1203 | 1204 | # Ensure that we're checking parent_type when restoring 1205 | def test_missing_restore_recursive_on_polymorphic_has_one_association 1206 | parent = ParentModel.create 1207 | polymorphic = PolymorphicModel.create(parent_id: parent.id, parent_type: 'ParanoidModel') 1208 | 1209 | parent.destroy 1210 | polymorphic.destroy 1211 | 1212 | assert_equal 0, polymorphic.class.count 1213 | 1214 | parent.restore(recursive: true) 1215 | 1216 | assert_equal 0, polymorphic.class.count 1217 | end 1218 | 1219 | def test_counter_cache_column_update_on_destroy#_and_restore_and_really_destroy 1220 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1221 | related_model = parent_model_with_counter_cache_column.related_models.create 1222 | 1223 | assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count 1224 | related_model.destroy 1225 | assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count 1226 | end 1227 | 1228 | def test_callbacks_for_counter_cache_column_update_on_destroy 1229 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1230 | related_model = parent_model_with_counter_cache_column.related_models.create 1231 | 1232 | assert_nil related_model.instance_variable_get(:@after_destroy_callback_called) 1233 | assert_nil related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 1234 | 1235 | related_model.destroy 1236 | 1237 | assert related_model.instance_variable_get(:@after_destroy_callback_called) 1238 | # assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 1239 | end 1240 | 1241 | def test_uniqueness_for_unparanoid_associated 1242 | parent_model = ParanoidWithUnparanoids.create 1243 | related = parent_model.unparanoid_unique_models.create 1244 | # will raise exception if model is not checked for paranoia 1245 | related.valid? 1246 | end 1247 | 1248 | def test_assocation_not_soft_destroyed_validator 1249 | notParanoidModel = NotParanoidModelWithBelongsAndAssocationNotSoftDestroyedValidator.create 1250 | parentModel = ParentModel.create 1251 | assert notParanoidModel.valid? 1252 | 1253 | notParanoidModel.parent_model = parentModel 1254 | assert notParanoidModel.valid? 1255 | parentModel.destroy 1256 | assert !notParanoidModel.valid? 1257 | assert notParanoidModel.errors.full_messages.include? "Parent model has been soft-deleted" 1258 | end 1259 | 1260 | # TODO: find a fix for Rails 4.1 1261 | if ActiveRecord::VERSION::STRING !~ /\A4\.1/ 1262 | def test_counter_cache_column_update_on_really_destroy 1263 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1264 | related_model = parent_model_with_counter_cache_column.related_models.create 1265 | 1266 | assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count 1267 | related_model.really_destroy! 1268 | assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count 1269 | end 1270 | end 1271 | 1272 | # TODO: find a fix for Rails 4.0 and 4.1 1273 | if ActiveRecord::VERSION::STRING >= '4.2' 1274 | def test_callbacks_for_counter_cache_column_update_on_really_destroy! 1275 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1276 | related_model = parent_model_with_counter_cache_column.related_models.create 1277 | 1278 | assert_nil related_model.instance_variable_get(:@after_destroy_callback_called) 1279 | assert_nil related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 1280 | 1281 | related_model.really_destroy! 1282 | 1283 | assert related_model.instance_variable_get(:@after_destroy_callback_called) 1284 | assert related_model.instance_variable_get(:@after_commit_on_destroy_callback_called) 1285 | end 1286 | 1287 | def test_counter_cache_column_on_double_destroy 1288 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1289 | related_model = parent_model_with_counter_cache_column.related_models.create 1290 | 1291 | related_model.destroy 1292 | related_model.destroy 1293 | assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count 1294 | end 1295 | 1296 | def test_counter_cache_column_on_double_restore 1297 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1298 | related_model = parent_model_with_counter_cache_column.related_models.create 1299 | 1300 | related_model.destroy 1301 | related_model.restore 1302 | related_model.restore 1303 | assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count 1304 | end 1305 | 1306 | def test_counter_cache_column_on_destroy_and_really_destroy 1307 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1308 | related_model = parent_model_with_counter_cache_column.related_models.create 1309 | 1310 | related_model.destroy 1311 | related_model.really_destroy! 1312 | assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count 1313 | end 1314 | 1315 | def test_counter_cache_column_on_restore 1316 | parent_model_with_counter_cache_column = ParentModelWithCounterCacheColumn.create 1317 | related_model = parent_model_with_counter_cache_column.related_models.create 1318 | 1319 | related_model.destroy 1320 | assert_equal 0, parent_model_with_counter_cache_column.reload.related_models_count 1321 | related_model.restore 1322 | assert_equal 1, parent_model_with_counter_cache_column.reload.related_models_count 1323 | end 1324 | end 1325 | 1326 | def test_has_one_with_scope_missed 1327 | parent = ParanoidHasOneWithScope.create 1328 | gamma = ParanoidHasOneWithScope.create(kind: :gamma, paranoid_has_one_with_scope: parent) # this has to be first 1329 | alpha = ParanoidHasOneWithScope.create(kind: :alpha, paranoid_has_one_with_scope: parent) 1330 | beta = ParanoidHasOneWithScope.create(kind: :beta, paranoid_has_one_with_scope: parent) 1331 | 1332 | parent.destroy 1333 | assert !gamma.reload.destroyed? 1334 | gamma.destroy 1335 | assert_equal 0, ParanoidHasOneWithScope.count # all destroyed 1336 | parent.reload # we unload associations 1337 | parent.restore(recursive: true) 1338 | 1339 | assert_equal "alpha", parent.alpha&.kind, "record was not restored" 1340 | assert_equal "beta", parent.beta&.kind, "record was not restored" 1341 | assert_nil parent.gamma, "record was incorrectly restored" 1342 | end 1343 | 1344 | def test_has_one_with_scope_not_restored 1345 | parent = ParanoidHasOneWithScope.create 1346 | gamma = ParanoidHasOneWithScope.create(kind: :gamma, paranoid_has_one_with_scope: parent) 1347 | parent.destroy 1348 | assert_equal 1, ParanoidHasOneWithScope.count # gamma not deleted 1349 | gamma.destroy 1350 | parent.reload # we unload associations 1351 | parent.restore(recursive: true) 1352 | 1353 | assert gamma.reload.deleted?, "the record was incorrectly restored" 1354 | assert_equal 1, ParanoidHasOneWithScope.count # gamma deleted 1355 | end 1356 | 1357 | def test_delete_all_disabled_by_default 1358 | assert_nil ParanoidModel.delete_all_enabled 1359 | 1360 | (0...3).each{ ParanoidModel.create } 1361 | assert_equal 3, ParanoidModel.count 1362 | ParanoidModel.delete_all 1363 | assert_equal 0, ParanoidModel.count 1364 | assert_equal 0, ParanoidModel.unscoped.count 1365 | end 1366 | 1367 | def test_delete_all_called_on_class 1368 | assert Employee.delete_all_enabled 1369 | 1370 | (0...3).each{ Employee.create } 1371 | assert_equal 3, Employee.count 1372 | Employee.delete_all 1373 | assert_equal 0, Employee.count 1374 | assert_equal 3, Employee.unscoped.count 1375 | end 1376 | 1377 | def test_delete_all_called_on_relation 1378 | assert Employee.delete_all_enabled 1379 | 1380 | (0...3).each{ Employee.create } 1381 | assert_equal 3, Employee.count 1382 | Employee.where(id: 1).delete_all 1383 | assert_equal 2, Employee.count 1384 | assert_equal 3, Employee.unscoped.count 1385 | end 1386 | 1387 | def test_really_delete_all_called_on_class 1388 | assert Employee.delete_all_enabled 1389 | 1390 | (0...3).each{ Employee.create } 1391 | assert_equal 3, Employee.count 1392 | Employee.really_delete_all 1393 | assert_equal 0, Employee.count 1394 | assert_equal 0, Employee.unscoped.count 1395 | end 1396 | 1397 | def test_delete_all_called_on_relation 1398 | assert Employee.delete_all_enabled 1399 | 1400 | (0...3).each{ Employee.create } 1401 | assert_equal 3, Employee.count 1402 | Employee.where(id: 1).really_delete_all 1403 | assert_equal 2, Employee.count 1404 | assert_equal 2, Employee.unscoped.count 1405 | end 1406 | 1407 | def test_update_has_many_through_relation_delete_associations 1408 | employer = Employer.create 1409 | employee1 = Employee.create 1410 | employee2 = Employee.create 1411 | job = Job.create :employer => employer, :employee => employee1 1412 | 1413 | assert_equal 1, employer.jobs.count 1414 | assert_equal 1, employer.jobs.with_deleted.count 1415 | 1416 | employer.update(employee_ids: [employee2.id]) 1417 | 1418 | assert_equal 1, employer.jobs.count 1419 | assert_equal 2, employer.jobs.with_deleted.count 1420 | end 1421 | 1422 | private 1423 | def get_featureful_model 1424 | FeaturefulModel.new(:name => "not empty") 1425 | end 1426 | end 1427 | 1428 | # Helper classes 1429 | 1430 | class ParanoidModel < ActiveRecord::Base 1431 | acts_as_paranoid 1432 | belongs_to :parent_model 1433 | 1434 | has_many :active_column_models, dependent: :destroy 1435 | 1436 | end 1437 | 1438 | class DoublyParanoidModel < ActiveRecord::Base 1439 | self.table_name = 'plain_models' 1440 | acts_as_paranoid 1441 | end 1442 | 1443 | class ParanoidWithUnparanoids < ActiveRecord::Base 1444 | self.table_name = 'plain_models' 1445 | has_many :unparanoid_unique_models 1446 | end 1447 | 1448 | class UnparanoidUniqueModel < ActiveRecord::Base 1449 | belongs_to :paranoid_with_unparanoids 1450 | validates :name, :uniqueness => true 1451 | end 1452 | 1453 | class FailCallbackModel < ActiveRecord::Base 1454 | belongs_to :parent_model 1455 | acts_as_paranoid 1456 | 1457 | before_destroy { |_| 1458 | if ActiveRecord::VERSION::MAJOR < 5 1459 | false 1460 | else 1461 | throw :abort 1462 | end 1463 | } 1464 | end 1465 | 1466 | class FeaturefulModel < ActiveRecord::Base 1467 | acts_as_paranoid 1468 | validates :name, :presence => true, :uniqueness => true 1469 | end 1470 | 1471 | class NonParanoidChildModel < ActiveRecord::Base 1472 | validates :name, :presence => true, :uniqueness => true 1473 | end 1474 | 1475 | class PlainModel < ActiveRecord::Base 1476 | end 1477 | 1478 | class CallbackModel < ActiveRecord::Base 1479 | acts_as_paranoid 1480 | before_destroy { |model| model.instance_variable_set :@destroy_callback_called, true } 1481 | before_restore { |model| model.instance_variable_set :@restore_callback_called, true } 1482 | before_update { |model| model.instance_variable_set :@update_callback_called, true } 1483 | before_save { |model| model.instance_variable_set :@save_callback_called, true} 1484 | before_real_destroy { |model| model.instance_variable_set :@real_destroy_callback_called, true } 1485 | 1486 | after_destroy { |model| model.instance_variable_set :@after_destroy_callback_called, true } 1487 | after_commit { |model| model.instance_variable_set :@after_commit_callback_called, true } 1488 | 1489 | validate { |model| model.instance_variable_set :@validate_called, true } 1490 | 1491 | def remove_called_variables 1492 | instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil} 1493 | end 1494 | end 1495 | 1496 | class AfterCommitOnRestoreCallbackModel < ActiveRecord::Base 1497 | acts_as_paranoid after_restore_commit: true 1498 | before_restore { |model| model.instance_variable_set :@restore_callback_called, true } 1499 | after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true } 1500 | after_commit :set_after_restore_commit_called, on: :restore 1501 | 1502 | def set_after_restore_commit_called 1503 | @after_restore_commit_callback_called = true 1504 | end 1505 | end 1506 | 1507 | class AfterRestoreCommitCallbackModel < ActiveRecord::Base 1508 | acts_as_paranoid after_restore_commit: true 1509 | before_restore { |model| model.instance_variable_set :@restore_callback_called, true } 1510 | after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true } 1511 | after_restore_commit { |model| model.instance_variable_set :@after_restore_commit_callback_called, true } 1512 | after_destroy_commit { |model| model.instance_variable_set :@after_destroy_commit_callback_called, true } 1513 | 1514 | def remove_called_variables 1515 | instance_variables.each {|name| (name.to_s.end_with?('_called')) ? remove_instance_variable(name) : nil} 1516 | end 1517 | end 1518 | 1519 | class AfterCommitCallbackRestoreEnabledModel < ActiveRecord::Base 1520 | acts_as_paranoid after_restore_commit: true 1521 | before_restore { |model| model.instance_variable_set :@restore_callback_called, true } 1522 | after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true } 1523 | after_commit { |model| model.instance_variable_set :@after_commit_callback_called, true } 1524 | end 1525 | 1526 | class AfterOtherCommitCallbackRestoreEnabledModel < ActiveRecord::Base 1527 | acts_as_paranoid after_restore_commit: true 1528 | before_restore { |model| model.instance_variable_set :@restore_callback_called, true } 1529 | after_restore { |model| model.instance_variable_set :@after_restore_callback_called, true } 1530 | after_commit :set_after_other_commit_called, on: [:create, :destroy, :update] 1531 | 1532 | def set_after_other_commit_called 1533 | @after_other_commit_callback_called = true 1534 | end 1535 | end 1536 | 1537 | class AssociationWithAbortModel < ActiveRecord::Base 1538 | acts_as_paranoid 1539 | has_many :related_models, class_name: 'RelatedModel', foreign_key: :parent_model_id, dependent: :destroy 1540 | before_destroy { |_| 1541 | if ActiveRecord::VERSION::MAJOR < 5 1542 | false 1543 | else 1544 | throw :abort 1545 | end 1546 | } 1547 | end 1548 | 1549 | class AfterCommitCallbackModel < ActiveRecord::Base 1550 | acts_as_paranoid 1551 | 1552 | after_commit :increment_after_create_commit_called_times, on: :create 1553 | after_commit :increment_after_destroy_commit_called_times, on: :destroy 1554 | 1555 | def increment_after_create_commit_called_times 1556 | @after_create_commit_called_times = after_create_commit_called_times + 1 1557 | end 1558 | 1559 | def increment_after_destroy_commit_called_times 1560 | @after_destroy_commit_called_times = after_destroy_commit_called_times + 1 1561 | end 1562 | 1563 | def after_create_commit_called_times 1564 | @after_create_commit_called_times || 0 1565 | end 1566 | 1567 | def after_destroy_commit_called_times 1568 | @after_destroy_commit_called_times || 0 1569 | end 1570 | 1571 | def remove_called_variables 1572 | instance_variables.each {|name| (name.to_s.end_with?('_called_times')) ? remove_instance_variable(name) : nil} 1573 | end 1574 | end 1575 | 1576 | class ParentModel < ActiveRecord::Base 1577 | attr_accessor :destroy_unavailable 1578 | acts_as_paranoid 1579 | has_many :paranoid_models, dependent: :destroy 1580 | has_many :related_models 1581 | has_many :very_related_models, :class_name => 'RelatedModel', dependent: :destroy 1582 | has_many :non_paranoid_models, dependent: :destroy 1583 | has_one :non_paranoid_model, dependent: :destroy 1584 | has_many :asplode_models, dependent: :destroy 1585 | has_one :polymorphic_model, as: :parent, dependent: :destroy 1586 | before_destroy :validate_destroy 1587 | 1588 | def validate_destroy 1589 | return unless self.destroy_unavailable 1590 | 1591 | if ActiveRecord::VERSION::MAJOR < 5 1592 | false 1593 | else 1594 | throw :abort 1595 | end 1596 | end 1597 | end 1598 | 1599 | class ParentModelWithCounterCacheColumn < ActiveRecord::Base 1600 | has_many :related_models 1601 | end 1602 | 1603 | class RelatedModel < ActiveRecord::Base 1604 | acts_as_paranoid 1605 | belongs_to :parent_model 1606 | belongs_to :parent_model_with_counter_cache_column, counter_cache: true 1607 | 1608 | after_destroy do |model| 1609 | if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 1610 | model.instance_variable_set :@after_destroy_callback_called, true 1611 | end 1612 | end 1613 | after_commit :set_after_commit_on_destroy_callback_called, on: :destroy 1614 | 1615 | def set_after_commit_on_destroy_callback_called 1616 | if parent_model_with_counter_cache_column && parent_model_with_counter_cache_column.reload.related_models_count == 0 1617 | self.instance_variable_set :@after_commit_on_destroy_callback_called, true 1618 | end 1619 | end 1620 | end 1621 | 1622 | class Employer < ActiveRecord::Base 1623 | acts_as_paranoid 1624 | validates_uniqueness_of :name 1625 | has_many :jobs 1626 | has_many :employees, :through => :jobs, dependent: :destroy 1627 | end 1628 | 1629 | class Employee < ActiveRecord::Base 1630 | acts_as_paranoid(delete_all_enabled: true) 1631 | has_many :jobs 1632 | has_many :employers, :through => :jobs 1633 | end 1634 | 1635 | class Job < ActiveRecord::Base 1636 | acts_as_paranoid(delete_all_enabled: true) 1637 | acts_as_paranoid 1638 | belongs_to :employer 1639 | belongs_to :employee 1640 | end 1641 | 1642 | class CustomColumnModel < ActiveRecord::Base 1643 | acts_as_paranoid column: :destroyed_at 1644 | end 1645 | 1646 | class CustomSentinelModel < ActiveRecord::Base 1647 | acts_as_paranoid sentinel_value: DateTime.new(0) 1648 | end 1649 | 1650 | class WithoutDefaultScopeModel < ActiveRecord::Base 1651 | acts_as_paranoid without_default_scope: true 1652 | end 1653 | 1654 | 1655 | class ActiveColumnModel < ActiveRecord::Base 1656 | acts_as_paranoid column: :active, sentinel_value: true 1657 | 1658 | belongs_to :paranoid_model 1659 | 1660 | def paranoia_restore_attributes 1661 | { 1662 | deleted_at: nil, 1663 | active: true 1664 | } 1665 | end 1666 | 1667 | def paranoia_destroy_attributes 1668 | { 1669 | deleted_at: current_time_from_proper_timezone, 1670 | active: nil 1671 | } 1672 | end 1673 | end 1674 | 1675 | class ActiveColumnModelWithUniquenessValidation < ActiveRecord::Base 1676 | validates :name, :uniqueness => true 1677 | acts_as_paranoid column: :active, sentinel_value: true 1678 | 1679 | def paranoia_restore_attributes 1680 | { 1681 | deleted_at: nil, 1682 | active: true 1683 | } 1684 | end 1685 | 1686 | def paranoia_destroy_attributes 1687 | { 1688 | deleted_at: current_time_from_proper_timezone, 1689 | active: nil 1690 | } 1691 | end 1692 | end 1693 | 1694 | class ActiveColumnModelWithHasManyRelationship < ActiveRecord::Base 1695 | has_many :paranoid_model_with_belongs_to_active_column_model_with_has_many_relationships 1696 | acts_as_paranoid column: :active, sentinel_value: true 1697 | 1698 | def paranoia_restore_attributes 1699 | { 1700 | deleted_at: nil, 1701 | active: true 1702 | } 1703 | end 1704 | 1705 | def paranoia_destroy_attributes 1706 | { 1707 | deleted_at: current_time_from_proper_timezone, 1708 | active: nil 1709 | } 1710 | end 1711 | end 1712 | 1713 | class ParanoidModelWithBelongsToActiveColumnModelWithHasManyRelationship < ActiveRecord::Base 1714 | belongs_to :active_column_model_with_has_many_relationship 1715 | 1716 | acts_as_paranoid column: :active, sentinel_value: true 1717 | 1718 | def paranoia_restore_attributes 1719 | { 1720 | deleted_at: nil, 1721 | active: true 1722 | } 1723 | end 1724 | 1725 | def paranoia_destroy_attributes 1726 | { 1727 | deleted_at: current_time_from_proper_timezone, 1728 | active: nil 1729 | } 1730 | end 1731 | end 1732 | 1733 | class NonParanoidModel < ActiveRecord::Base 1734 | end 1735 | 1736 | class ParanoidModelWithObservers < ParanoidModel 1737 | def observers_notified 1738 | @observers_notified ||= [] 1739 | end 1740 | 1741 | def self.notify_observer(*args) 1742 | observers_notified << args 1743 | end 1744 | end 1745 | 1746 | class ParanoidModelWithoutObservers < ParanoidModel 1747 | self.class.send(remove_method :notify_observers) if method_defined?(:notify_observers) 1748 | end 1749 | 1750 | # refer back to regression test for #118 1751 | class ParanoidModelWithHasOne < ParanoidModel 1752 | has_one :paranoid_model_with_belong, :dependent => :destroy 1753 | has_one :class_name_belong, :dependent => :destroy, :class_name => "ParanoidModelWithAnthorClassNameBelong" 1754 | has_one :paranoid_model_with_foreign_key_belong, :dependent => :destroy, :foreign_key => "has_one_foreign_key_id" 1755 | has_one :not_paranoid_model_with_belong, :dependent => :destroy 1756 | end 1757 | 1758 | class ParanoidModelWithHasOneAndBuild < ActiveRecord::Base 1759 | has_one :paranoid_model_with_build_belong, :dependent => :destroy 1760 | validates :color, :presence => true 1761 | after_validation :build_paranoid_model_with_build_belong, on: :create 1762 | 1763 | private 1764 | def build_paranoid_model_with_build_belong 1765 | super.tap { |child| child.name = "foo" } 1766 | end 1767 | end 1768 | 1769 | class ParanoidModelWithBuildBelong < ActiveRecord::Base 1770 | acts_as_paranoid 1771 | validates :name, :presence => true 1772 | belongs_to :paranoid_model_with_has_one_and_build 1773 | end 1774 | 1775 | class ParanoidModelWithBelong < ActiveRecord::Base 1776 | acts_as_paranoid 1777 | belongs_to :paranoid_model_with_has_one 1778 | end 1779 | 1780 | class ParanoidModelWithAnthorClassNameBelong < ActiveRecord::Base 1781 | acts_as_paranoid 1782 | belongs_to :paranoid_model_with_has_one 1783 | end 1784 | 1785 | class ParanoidModelWithForeignKeyBelong < ActiveRecord::Base 1786 | acts_as_paranoid 1787 | belongs_to :paranoid_model_with_has_one 1788 | end 1789 | 1790 | class ParanoidModelWithTimestamp < ActiveRecord::Base 1791 | belongs_to :parent_model 1792 | acts_as_paranoid 1793 | end 1794 | 1795 | class NotParanoidModelWithBelong < ActiveRecord::Base 1796 | belongs_to :paranoid_model_with_has_one 1797 | end 1798 | 1799 | class NotParanoidModelWithBelongsAndAssocationNotSoftDestroyedValidator < NotParanoidModelWithBelong 1800 | belongs_to :parent_model 1801 | validates :parent_model, association_not_soft_destroyed: true 1802 | end 1803 | 1804 | class FlaggedModel < PlainModel 1805 | acts_as_paranoid :flag_column => :is_deleted 1806 | end 1807 | 1808 | class FlaggedModelWithCustomIndex < PlainModel 1809 | acts_as_paranoid :flag_column => :is_deleted, :indexed_column => :is_deleted 1810 | end 1811 | 1812 | class AsplodeModel < ActiveRecord::Base 1813 | acts_as_paranoid 1814 | before_destroy do |r| 1815 | raise StandardError, 'ASPLODE!' 1816 | end 1817 | end 1818 | 1819 | class NoConnectionModel < ActiveRecord::Base 1820 | end 1821 | 1822 | class PolymorphicModel < ActiveRecord::Base 1823 | acts_as_paranoid 1824 | belongs_to :parent, polymorphic: true 1825 | end 1826 | 1827 | module Namespaced 1828 | def self.table_name_prefix 1829 | "namespaced_" 1830 | end 1831 | 1832 | class ParanoidHasOne < ActiveRecord::Base 1833 | acts_as_paranoid 1834 | has_one :paranoid_belongs_to, dependent: :destroy 1835 | end 1836 | 1837 | class ParanoidBelongsTo < ActiveRecord::Base 1838 | acts_as_paranoid 1839 | belongs_to :paranoid_has_one 1840 | end 1841 | end 1842 | 1843 | class ParanoidHasThroughRestoreParent < ActiveRecord::Base 1844 | acts_as_paranoid 1845 | 1846 | has_one :paranoid_has_one_through, dependent: :destroy 1847 | has_one :empty_paranoid_model, through: :paranoid_has_one_through, dependent: :destroy 1848 | 1849 | has_many :paranoid_has_many_throughs, dependent: :destroy 1850 | has_many :empty_paranoid_models, through: :paranoid_has_many_throughs, dependent: :destroy 1851 | end 1852 | 1853 | class EmptyParanoidModel < ActiveRecord::Base 1854 | acts_as_paranoid 1855 | end 1856 | 1857 | class ParanoidHasOneThrough < ActiveRecord::Base 1858 | acts_as_paranoid 1859 | belongs_to :paranoid_has_through_restore_parent 1860 | belongs_to :empty_paranoid_model, dependent: :destroy 1861 | end 1862 | 1863 | class ParanoidHasManyThrough < ActiveRecord::Base 1864 | acts_as_paranoid 1865 | belongs_to :paranoid_has_through_restore_parent 1866 | belongs_to :empty_paranoid_model, dependent: :destroy 1867 | end 1868 | 1869 | class ParanoidHasOneWithScope < ActiveRecord::Base 1870 | acts_as_paranoid 1871 | has_one :alpha, -> () { where(kind: :alpha) }, class_name: "ParanoidHasOneWithScope", dependent: :destroy 1872 | has_one :beta, -> () { where(kind: :beta) }, class_name: "ParanoidHasOneWithScope", dependent: :destroy 1873 | has_one :gamma, -> () { where(kind: :gamma) }, class_name: "ParanoidHasOneWithScope" 1874 | belongs_to :paranoid_has_one_with_scope 1875 | end 1876 | --------------------------------------------------------------------------------