├── .github ├── dependabot.yml └── workflows │ └── ruby.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── Appraisals ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── Manifest.txt ├── README.md ├── Rakefile ├── acts_as_paranoid.gemspec ├── gemfiles ├── active_record_61.gemfile ├── active_record_70.gemfile ├── active_record_71.gemfile ├── active_record_72.gemfile └── active_record_80.gemfile ├── lib ├── acts_as_paranoid.rb └── acts_as_paranoid │ ├── association_reflection.rb │ ├── associations.rb │ ├── core.rb │ ├── relation.rb │ ├── validations.rb │ └── version.rb └── test ├── integration └── associations_test.rb ├── legacy ├── associations_test.rb ├── core_test.rb ├── default_scopes_test.rb ├── dependent_recovery_test.rb ├── inheritance_test.rb ├── relations_test.rb ├── table_namespace_test.rb └── validations_test.rb └── test_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "bundler" 7 | directory: "/" 8 | schedule: 9 | interval: "daily" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow will download a prebuilt Ruby version, install dependencies and 2 | # run tests with Rake 3 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 4 | 5 | name: CI 6 | 7 | "on": 8 | push: 9 | branches: [master] 10 | pull_request: 11 | branches: [master] 12 | schedule: 13 | - cron: '16 4 12 * *' 14 | workflow_dispatch: 15 | 16 | jobs: 17 | test: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | strategy: 22 | matrix: 23 | ruby: ["3.1", "3.2", "3.3", "3.4", "jruby-9.4"] 24 | gemfile: 25 | - active_record_61 26 | - active_record_70 27 | - active_record_71 28 | - active_record_72 29 | - active_record_80 30 | exclude: 31 | # The activerecord-jdbcsqlite3-adapter gem does not work with Rails 32 | # 7.1 and above yet 33 | - ruby: "jruby-9.4" 34 | gemfile: active_record_71 35 | - ruby: "jruby-9.4" 36 | gemfile: active_record_72 37 | - ruby: "jruby-9.4" 38 | gemfile: active_record_80 39 | # Rails 8.0 requires Ruby 3.2 40 | - ruby: "3.1" 41 | gemfile: active_record_80 42 | # Rails 6.1 and 7.0 do not support Ruby 3.4 43 | - ruby: "3.4" 44 | gemfile: active_record_61 45 | - ruby: "3.4" 46 | gemfile: active_record_70 47 | 48 | env: 49 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 50 | 51 | steps: 52 | - uses: actions/checkout@v4 53 | - name: Set up Ruby 54 | uses: ruby/setup-ruby@v1 55 | with: 56 | ruby-version: ${{ matrix.ruby }} 57 | bundler-cache: true 58 | - name: Run tests 59 | run: bundle exec rake test 60 | 61 | rubocop: 62 | 63 | runs-on: ubuntu-latest 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | - name: Set up Ruby 68 | uses: ruby/setup-ruby@v1 69 | with: 70 | ruby-version: "3.3" 71 | bundler-cache: true 72 | - name: Run RuboCop 73 | run: bundle exec rubocop -P 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | .bundle 3 | .DS_Store 4 | Gemfile.lock 5 | gemfiles/*.lock 6 | .idea/ 7 | .ruby-version 8 | coverage/ 9 | log/ 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | inherit_mode: 4 | merge: 5 | - Exclude 6 | 7 | require: 8 | - rubocop-minitest 9 | - rubocop-packaging 10 | - rubocop-performance 11 | - rubocop-rake 12 | 13 | AllCops: 14 | NewCops: enable 15 | TargetRubyVersion: 3.1 16 | 17 | # Put development dependencies in the gemspec so rubygems.org knows about them 18 | Gemspec/DevelopmentDependencies: 19 | EnforcedStyle: gemspec 20 | 21 | Layout/LineContinuationLeadingSpace: 22 | EnforcedStyle: leading 23 | 24 | # Be lenient with line length 25 | Layout/LineLength: 26 | Max: 92 27 | 28 | # Multi-line method calls should be simply indented. Aligning them makes it 29 | # even harder to keep a sane line length. 30 | Layout/MultilineMethodCallIndentation: 31 | EnforcedStyle: indented 32 | 33 | # Multi-line assignment should be simply indented. Aligning them makes it even 34 | # harder to keep a sane line length. 35 | Layout/MultilineOperationIndentation: 36 | EnforcedStyle: indented 37 | 38 | # Allow minitest-spec blocks to have any length 39 | Metrics/BlockLength: 40 | Exclude: 41 | - 'test/integration/**_test.rb' 42 | 43 | # Allow test classes to have any length 44 | Metrics/ClassLength: 45 | Exclude: 46 | - 'test/**/*' 47 | 48 | # Allow test methods to have any length 49 | Metrics/MethodLength: 50 | Exclude: 51 | - 'test/**/*' 52 | 53 | # Allow else clauses with explicit nil value 54 | Style/EmptyElse: 55 | EnforcedStyle: empty 56 | 57 | # In guard clauses, if ! is often more immediately clear 58 | Style/NegatedIf: 59 | Enabled: false 60 | 61 | # Do not commit to use of interpolation 62 | Style/StringLiterals: 63 | EnforcedStyle: double_quotes 64 | 65 | # Prefer symbols to look like symbols 66 | Style/SymbolArray: 67 | EnforcedStyle: brackets 68 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --no-offense-counts --no-auto-gen-timestamp` 3 | # using RuboCop version 1.65.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. 10 | Metrics/AbcSize: 11 | Max: 47 12 | 13 | # Configuration parameters: AllowedMethods, AllowedPatterns. 14 | Metrics/CyclomaticComplexity: 15 | Max: 10 16 | 17 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 18 | Metrics/MethodLength: 19 | Max: 26 20 | 21 | # Configuration parameters: CountComments, CountAsOne. 22 | Metrics/ModuleLength: 23 | Max: 158 24 | 25 | # Configuration parameters: AllowedMethods, AllowedPatterns. 26 | Metrics/PerceivedComplexity: 27 | Max: 10 28 | 29 | Minitest/AssertionInLifecycleHook: 30 | Exclude: 31 | - 'test/legacy/default_scopes_test.rb' 32 | - 'test/legacy/relations_test.rb' 33 | 34 | Minitest/MultipleAssertions: 35 | Max: 16 36 | 37 | # This cop supports safe autocorrection (--autocorrect). 38 | Minitest/TestMethodName: 39 | Exclude: 40 | - 'test/legacy/core_test.rb' 41 | 42 | # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. 43 | # SupportedStyles: snake_case, normalcase, non_integer 44 | # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 45 | Naming/VariableNumber: 46 | Exclude: 47 | - 'test/legacy/core_test.rb' 48 | - 'test/legacy/relations_test.rb' 49 | 50 | # Configuration parameters: AllowedConstants. 51 | Style/Documentation: 52 | Exclude: 53 | - 'lib/acts_as_paranoid.rb' 54 | - 'lib/acts_as_paranoid/associations.rb' 55 | - 'lib/acts_as_paranoid/core.rb' 56 | - 'lib/acts_as_paranoid/relation.rb' 57 | - 'lib/acts_as_paranoid/validations.rb' 58 | 59 | # This cop supports unsafe autocorrection (--autocorrect-all). 60 | # Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. 61 | # SupportedStyles: predicate, comparison 62 | Style/NumericPredicate: 63 | Exclude: 64 | - 'lib/acts_as_paranoid/associations.rb' 65 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # HACK: This uses odd syntax to make appraisal customization work on newer Rubies. 4 | # See https://github.com/thoughtbot/appraisal/pull/214. Once that one has been 5 | # released, we should use customize_gemfiles instead. 6 | Customize.new heading: <<~HEADING.chomp 7 | frozen_string_literal: true 8 | 9 | This file was generated by Appraisal 10 | HEADING 11 | 12 | appraise "active_record_61" do 13 | gem "activerecord", "~> 6.1.0", require: "active_record" 14 | gem "activesupport", "~> 6.1.0", require: "active_support" 15 | 16 | group :development do 17 | gem "activerecord-jdbcsqlite3-adapter", "~> 61.1", platforms: [:jruby] 18 | gem "sqlite3", "~> 1.4", platforms: [:ruby] 19 | end 20 | end 21 | 22 | appraise "active_record_70" do 23 | gem "activerecord", "~> 7.0.0", require: "active_record" 24 | gem "activesupport", "~> 7.0.0", require: "active_support" 25 | 26 | group :development do 27 | gem "activerecord-jdbcsqlite3-adapter", "~> 70.0", platforms: [:jruby] 28 | gem "sqlite3", "~> 1.4", platforms: [:ruby] 29 | end 30 | end 31 | 32 | appraise "active_record_71" do 33 | gem "activerecord", "~> 7.1.0", require: "active_record" 34 | gem "activesupport", "~> 7.1.0", require: "active_support" 35 | 36 | group :development do 37 | gem "sqlite3", "~> 1.4", platforms: [:ruby] 38 | end 39 | end 40 | 41 | appraise "active_record_72" do 42 | gem "activerecord", "~> 7.2.0", require: "active_record" 43 | gem "activesupport", "~> 7.2.0", require: "active_support" 44 | 45 | group :development do 46 | gem "sqlite3", "~> 2.0", platforms: [:ruby] 47 | end 48 | end 49 | 50 | appraise "active_record_80" do 51 | gem "activerecord", "~> 8.0.0", require: "active_record" 52 | gem "activesupport", "~> 8.0.0", require: "active_support" 53 | 54 | group :development do 55 | gem "sqlite3", "~> 2.0", platforms: [:ruby] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | Notable changes to this project will be documented in this file. 4 | 5 | ## 0.10.3 6 | 7 | * Fix CI Badge ([#350] by [tagliala]) 8 | * Support Rails 8.0 ([#354] by [mvz]) 9 | 10 | [tagliala]: https://github.com/tagliala 11 | [#350]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/350 12 | [#354]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/354 13 | 14 | ## 0.10.2 15 | 16 | * Support Rails 7.2 ([#341] by [kalashnikovisme]) 17 | 18 | [kalashnikovisme]: https://github.com/kalashnikovisme 19 | 20 | [#341]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/341 21 | 22 | ## 0.10.1 23 | 24 | * Add changelog_uri to gemspec ([#332] by [fynsta]) 25 | * Improve contribution instructions ([#338] by [mvz]) 26 | * Make with_deleted work with paranoid join records ([#339] by [mvz]) 27 | 28 | [fynsta]: https://github.com/fynsta 29 | 30 | [#332]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/332 31 | [#338]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/338 32 | [#339]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/339 33 | 34 | ## 0.10.0 35 | 36 | * Support Ruby 3.0 through 3.3, dropping support for 2.7 ([#322] by [mvz]) 37 | * Use correct sqlite3 versions in tests ([#329] by [fatkodima]) 38 | * Do not load `ActiveRecord` too early ([#330] by [fatkodima]) 39 | 40 | [fatkodima]: https://github.com/fatkodima 41 | 42 | [#322]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/322 43 | [#329]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/329 44 | [#330]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/330 45 | 46 | ## 0.9.0 47 | 48 | * Support JRuby 9.4 ([#299] by [Matijs van Zuijlen][mvz]) 49 | * Add support for Ruby 3.2 ([#300] by [Matijs van Zuijlen][mvz]) 50 | * Drop support for Ruby 2.6 ([#301] by [Matijs van Zuijlen][mvz]) 51 | * Support Rails 7.1 ([#312] and [#317] by [Matijs van Zuijlen][mvz]) 52 | * Drop support for Rails 5.2 and 6.0 ([#315] by [Matijs van Zuijlen][mvz]) 53 | 54 | [#299]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/299 55 | [#300]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/300 56 | [#301]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/301 57 | [#312]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/312 58 | [#315]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/315 59 | [#317]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/317 60 | 61 | ## 0.8.1 62 | 63 | * Officially support Ruby 3.1 ([#268], by [Matijs van Zuijlen][mvz]) 64 | * Fix association building for `belongs_to` with `:with_deleted` option 65 | ([#277], by [Matijs van Zuijlen][mvz]) 66 | 67 | ## 0.8.0 68 | 69 | * Do not set `paranoid_value` when destroying fully ([#238], by [Aymeric Le Dorze][aymeric-ledorze]) 70 | * Make helper methods for dependent associations private 71 | ([#239], by [Matijs van Zuijlen][mvz]) 72 | * Raise ActiveRecord::RecordNotDestroyed if destroy returns false 73 | ([#240], by [Hao Liu][leomayleomay]) 74 | * Make unscoping by `with_deleted` less blunt ([#241], by [Matijs van Zuijlen][mvz]) 75 | * Drop support for Ruby 2.4 and 2.5 ([#243] and [#245] by [Matijs van Zuijlen][mvz]) 76 | * Remove deprecated methods ([#244] by [Matijs van Zuijlen][mvz]) 77 | * Remove test files from the gem ([#261] by [Matijs van Zuijlen][mvz]) 78 | * Add support for Rails 7 ([#262] by [Vederis Leunardus][cloudsbird]) 79 | 80 | ## 0.7.3 81 | 82 | ### Improvements 83 | 84 | * Fix deletion time scopes ([#212] by [Matijs van Zuijlen][mvz]) 85 | * Reload `has_one` associations after dependent recovery ([#214], 86 | by [Matijs van Zuijlen][mvz]) 87 | * Make dependent recovery work when parent is non-optional ([#227], 88 | by [Matijs van Zuijlen][mvz]) 89 | * Avoid querying nil `belongs_to` associations when recovering ([#219], 90 | by [Matijs van Zuijlen][mvz]) 91 | * On relations, deprecate `destroy!` in favour of `destroy_fully!` ([#222], 92 | by [Matijs van Zuijlen][mvz]) 93 | * Deprecate the undocumented `:recovery_value` setting. Calculate the correct 94 | value instead. ([#220], by [Matijs van Zuijlen][mvz]) 95 | 96 | ### Developer experience 97 | 98 | * Log ActiveRecord activity to a visible log during tests ([#218], 99 | by [Matijs van Zuijlen][mvz]) 100 | 101 | ## 0.7.2 102 | 103 | * Do not set boolean column to NULL on recovery if nulls are not allowed 104 | ([#193], by [Shodai Suzuki][soartec-lab]) 105 | * Add a CONTRIBUTING.md file ([#207], by [Matijs van Zuijlen][mvz]) 106 | 107 | ## 0.7.1 108 | 109 | * Support Rails 6.1 ([#191], by [Matijs van Zuijlen][mvz]) 110 | * Support `belongs_to` with both `:touch` and `:counter_cache` options ([#208], 111 | by [Matijs van Zuijlen][mvz] with [Paul Druziak][pauldruziak]) 112 | * Support Ruby 3.0 ([#209], by [Matijs van Zuijlen][mvz]) 113 | 114 | ## 0.7.0 115 | 116 | ### Breaking changes 117 | 118 | * Support Rails 5.2+ only ([#126], by [Daniel Rice][danielricecodes]) 119 | * Update set of supported rubies to 2.4-2.7 ([#144], [#173] by [Matijs van Zuijlen][mvz]) 120 | 121 | ### Improvements 122 | 123 | * Handle `with_deleted` association option as a scope ([#147], by [Matijs van Zuijlen][mvz]) 124 | * Simplify validation override ([#158], by [Matijs van Zuijlen][mvz]) 125 | * Use correct unscope syntax so unscope works on Rails Edge ([#160], 126 | by [Matijs van Zuijlen][mvz]) 127 | * Fix ruby 2.7 keyword argument deprecation warning ([#161], by [Jon Riddle][wtfspm]) 128 | 129 | ### Documentation 130 | 131 | * Document save after destroy behavior ([#146], by [Matijs van Zuijlen][mvz]) 132 | * Update version number instructions for installing gem ([#164], 133 | by [Kevin McAlear][kevinmcalear]) 134 | * Add example with `destroyed_fully?` and `deleted_fully?` to the readme ([#170], 135 | by [Kiril Mitov][thebravoman]) 136 | 137 | ### Internal 138 | 139 | * Improve code quality using RuboCop ([#148], [#152], [#159], [#163], [#171] and [#173], 140 | by [Matijs van Zuijlen][mvz]) 141 | * Measure code coverage using SimpleCov ([#150] and [#175] by [Matijs van Zuijlen][mvz]) 142 | * Silence warnings emitted during tests ([#156], by [Matijs van Zuijlen][mvz]) 143 | * Make rake tasks more robust and intuitive ([#157], by [Matijs van Zuijlen][mvz]) 144 | 145 | ## 0.6.3 146 | 147 | * Update Travis CI configuration ([#137], by [Matijs van Zuijlen][mvz]) 148 | * Add predicate to check if record was soft deleted or hard deleted ([#136], 149 | by [Aymeric Le Dorze][aymeric-ledorze]) 150 | * Add support for recover! method ([#75], by [vinoth][avinoth]) 151 | * Fix a record being dirty after destroying it ([#135], by 152 | [Aymeric Le Dorze][aymeric-ledorze]) 153 | 154 | ## 0.6.2 155 | 156 | * Prevent recovery of non-deleted records 157 | ([#133], by [Mary Beliveau][marycodes2] and [Valerie Woolard][valeriecodes]) 158 | * Allow model to set `table_name` after `acts_as_paranoid` macro 159 | ([#131], by [Alex Wheeler][AlexWheeler]) 160 | * Make counter cache work with a custom column name and with optional 161 | associations ([#123], by [Ned Campion][nedcampion]) 162 | 163 | ## 0.6.1 164 | 165 | * Add support for Rails 6 ([#124], by [Daniel Rice][danielricecodes], 166 | [Josh Bryant][jbryant92], and [Romain Alexandre][RomainAlexandre]) 167 | * Add support for incrementing and decrementing counter cache columns on 168 | associated objects ([#119], by [Dimitar Lukanov][shadydealer]) 169 | * Add `:double_tap_destroys_fully` option, with default `true` ([#116], 170 | by [Michael Riviera][ri4a]) 171 | * Officially support Ruby 2.6 ([#114], by [Matijs van Zuijlen][mvz]) 172 | 173 | ## 0.6.0 and earlier 174 | 175 | (To be added) 176 | 177 | 178 | 179 | [AlexWheeler]: https://github.com/AlexWheeler 180 | [RomainAlexandre]: https://github.com/RomainAlexandre 181 | [avinoth]: https://github.com/avinoth 182 | [cloudsbird]: https://github.com/cloudsbird 183 | [aymeric-ledorze]: https://github.com/aymeric-ledorze 184 | [danielricecodes]: https://github.com/danielricecodes 185 | [jbryant92]: https://github.com/jbryant92 186 | [kevinmcalear]: https://github.com/kevinmcalear 187 | [leomayleomay]: https://github.com/leomayleomay 188 | [marycodes2]: https://github.com/marycodes2 189 | [mvz]: https://github.com/mvz 190 | [nedcampion]: https://github.com/nedcampion 191 | [ri4a]: https://github.com/ri4a 192 | [pauldruziak]: https://github.com/pauldruziak 193 | [shadydealer]: https://github.com/shadydealer 194 | [soartec-lab]: https://github.com/soartec-lab 195 | [thebravoman]: https://github.com/thebravoman 196 | [valeriecodes]: https://github.com/valeriecodes 197 | [wtfspm]: https://github.com/wtfspm 198 | 199 | 200 | 201 | [#277]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/277 202 | [#268]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/268 203 | [#262]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/262 204 | [#261]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/261 205 | [#245]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/245 206 | [#244]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/244 207 | [#243]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/243 208 | [#241]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/241 209 | [#240]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/240 210 | [#239]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/239 211 | [#238]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/238 212 | [#227]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/227 213 | [#222]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/222 214 | [#220]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/220 215 | [#219]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/219 216 | [#218]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/218 217 | [#214]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/214 218 | [#212]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/212 219 | [#209]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/209 220 | [#208]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/208 221 | [#207]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/207 222 | [#193]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/193 223 | [#191]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/191 224 | [#175]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/175 225 | [#173]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/173 226 | [#171]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/171 227 | [#170]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/170 228 | [#164]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/164 229 | [#163]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/163 230 | [#161]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/161 231 | [#160]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/160 232 | [#159]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/159 233 | [#158]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/158 234 | [#157]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/157 235 | [#156]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/156 236 | [#152]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/152 237 | [#150]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/150 238 | [#148]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/148 239 | [#147]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/147 240 | [#146]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/146 241 | [#144]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/144 242 | [#137]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/137 243 | [#136]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/136 244 | [#135]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/135 245 | [#133]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/133 246 | [#131]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/131 247 | [#126]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/126 248 | [#124]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/124 249 | [#123]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/123 250 | [#119]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/119 251 | [#116]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/116 252 | [#114]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/114 253 | [#75]: https://github.com/ActsAsParanoid/acts_as_paranoid/pull/75 254 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ActsAsParanoid 2 | 3 | We welcome contributions to ActsAsParanoid. Please follow the guidelines below to help the 4 | process of handling issues and pull requests go smoothly. 5 | 6 | ## Issues 7 | 8 | When creating an issue, please provide as much information as possible, and 9 | follow the guidelines below to make it easier for us to figure out what's going 10 | on. If you miss any of these points we will probably ask you to improve the 11 | ticket. 12 | 13 | - Include a clear title describing the problem 14 | - Describe what you are trying to achieve 15 | - Describe what you did, preferably including relevant code 16 | - Describe what you expected to happen 17 | - Describe what happened instead. Include relevant output if possible 18 | - State the version of ActsAsParanoid you are using 19 | - Use [code blocks](https://github.github.com/gfm/#fenced-code-blocks) to 20 | format any code and output in your ticket to make it readable. 21 | 22 | ## Pull Requests 23 | 24 | If you have an idea for a particular feature, it's probably best to create a 25 | GitHub issue for it before trying to implement it yourself. That way, we can 26 | discuss the feature and whether it makes sense to include in ActsAsParanoid itself 27 | before putting in the work to implement it. 28 | 29 | When sending a pull request, please follow **all of** the instructions below: 30 | 31 | - Make sure `bundle exec rake` runs without reporting any failures. See 32 | *Testing your changes* below for more details. 33 | - Add tests for your feature. Otherwise, we can't see if it works or if 34 | we break it later. 35 | - Create a separate branch for your feature based off of latest master. 36 | - Write [good commit messages](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 37 | - Do not include changes that are irrelevant to your feature in the same 38 | commit. 39 | - Keep an eye on the build results in GitHub Actions. If the build fails and it 40 | seems due to your changes, please update your pull request with a fix. 41 | 42 | If you're not sure how to test the problem, or what the best solution is, or 43 | get stuck on something else, please open an issue first so that we can discuss 44 | the best approach. 45 | 46 | ### Testing your changes 47 | 48 | You can run the test suite with the latest version of all dependencies by running the following: 49 | 50 | - Run `bundle install` if you haven't done so already, or `bundle update` to update the dependencies 51 | - Run `bundle exec rake` to run the tests 52 | 53 | To run the tests suite for a particular version of ActiveRecord use 54 | [appraisal](https://github.com/thoughtbot/appraisal). For example, to run the 55 | specs with ActiveRecord 6.1, run `appraisal active_record_61 rake`. See appraisal's 56 | documentation for details. 57 | 58 | ### The review process 59 | 60 | - We will try to review your pull request as soon as possible but we can make no 61 | guarantees. Feel free to ping us now and again. 62 | - We will probably ask you to rebase your branch on current master at some point 63 | during the review process. 64 | If you are unsure how to do this, 65 | [this in-depth guide](https://git-rebase.io/) should help out. 66 | - If you have any unclear commit messages, work-in-progress commits, or commits 67 | that just fix a mistake in a previous commits, we will ask you to clean up 68 | the history. 69 | Again, [the git-rebase guide](https://git-rebase.io/) should help out. 70 | Note that we will not squash-merge pull requests, since that results in a loss of history. 71 | - **At the end of the review process we may still choose not to merge your pull 72 | request.** For example, this could happen if we decide the proposed feature 73 | should not be part of ActsAsParanoid, or if the technical implementation does not 74 | match where we want to go with the architecture of the project. 75 | - We will generally not merge any pull requests that make the build fail, unless 76 | it's very clearly not related to the changes in the pull request. 77 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Development dependencies 6 | group :development do 7 | gem "sqlite3", ">= 1.4", "< 3.0", platforms: [:ruby] 8 | end 9 | 10 | gemspec 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2017 Zachary Scott, Gonçalo Silva, Rick Olson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | lib/acts_as_paranoid.rb 2 | lib/acts_as_paranoid/association_reflection.rb 3 | lib/acts_as_paranoid/associations.rb 4 | lib/acts_as_paranoid/core.rb 5 | lib/acts_as_paranoid/relation.rb 6 | lib/acts_as_paranoid/validations.rb 7 | lib/acts_as_paranoid/version.rb 8 | LICENSE 9 | CHANGELOG.md 10 | CONTRIBUTING.md 11 | README.md 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActsAsParanoid 2 | 3 | [![CI](https://github.com/ActsAsParanoid/acts_as_paranoid/actions/workflows/ruby.yml/badge.svg)](https://github.com/ActsAsParanoid/acts_as_paranoid/actions/workflows/ruby.yml) 4 | 5 | A Rails plugin to add soft delete. 6 | 7 | This gem can be used to hide records instead of deleting them, making them 8 | recoverable later. 9 | 10 | ## Support 11 | 12 | **This version targets Rails 6.1+ and Ruby 3.0+ only** 13 | 14 | If you're working with Rails 6.0 and earlier, or with Ruby 2.7 or earlier, 15 | please require an older version of the `acts_as_paranoid` gem. 16 | 17 | ### Known issues 18 | 19 | * Using `acts_as_paranoid` and ActiveStorage on the same model 20 | [leads to a SystemStackError](https://github.com/ActsAsParanoid/acts_as_paranoid/issues/103). 21 | * You cannot directly create a model in a deleted state, or update a model 22 | after it's been deleted. 23 | 24 | ## Usage 25 | 26 | #### Install gem 27 | 28 | ```ruby 29 | gem "acts_as_paranoid", "~> 0.10.3" 30 | ``` 31 | 32 | ```shell 33 | bundle install 34 | ``` 35 | 36 | #### Create migration 37 | 38 | ```shell 39 | bin/rails generate migration AddDeletedAtToParanoiac deleted_at:datetime:index 40 | ``` 41 | 42 | #### Enable ActsAsParanoid 43 | 44 | ```ruby 45 | class Paranoiac < ActiveRecord::Base 46 | acts_as_paranoid 47 | end 48 | ``` 49 | 50 | By default, ActsAsParanoid assumes a record's *deletion* is stored in a 51 | `datetime` column called `deleted_at`. 52 | 53 | ### Options 54 | 55 | If you are using a different column name and type to store a record's 56 | *deletion*, you can specify them as follows: 57 | 58 | - `column: 'deleted'` 59 | - `column_type: 'boolean'` 60 | 61 | While *column* can be anything (as long as it exists in your database), *type* 62 | is restricted to: 63 | 64 | - `boolean` 65 | - `time` or 66 | - `string` 67 | 68 | Note that the `time` type corresponds to the database column type `datetime` 69 | in your Rails migrations and schema. 70 | 71 | If your column type is a `string`, you can also specify which value to use when 72 | marking an object as deleted by passing `:deleted_value` (default is 73 | "deleted"). Any records with a non-matching value in this column will be 74 | treated normally, i.e., as not deleted. 75 | 76 | If your column type is a `boolean`, it is possible to specify `allow_nulls` 77 | option which is `true` by default. When set to `false`, entities that have 78 | `false` value in this column will be considered not deleted, and those which 79 | have `true` will be considered deleted. When `true` everything that has a 80 | not-null value will be considered deleted. 81 | 82 | ### Filtering 83 | 84 | If a record is deleted by ActsAsParanoid, it won't be retrieved when accessing 85 | the database. 86 | 87 | So, `Paranoiac.all` will **not** include the **deleted records**. 88 | 89 | When you want to access them, you have 2 choices: 90 | 91 | ```ruby 92 | Paranoiac.only_deleted # retrieves only the deleted records 93 | Paranoiac.with_deleted # retrieves all records, deleted or not 94 | ``` 95 | 96 | When using the default `column_type` of `'time'`, the following extra scopes 97 | are provided: 98 | 99 | ```ruby 100 | time = Time.now 101 | 102 | Paranoiac.deleted_after_time(time) 103 | Paranoiac.deleted_before_time(time) 104 | 105 | # Or roll it all up and get a nice window: 106 | Paranoiac.deleted_inside_time_window(time, 2.minutes) 107 | ``` 108 | 109 | ### Real deletion 110 | 111 | In order to really delete a record, just use: 112 | 113 | ```ruby 114 | paranoiac.destroy_fully! 115 | Paranoiac.delete_all!(conditions) 116 | ``` 117 | 118 | **NOTE:** The `.destroy!` method is still usable, but equivalent to `.destroy`. 119 | It just hides the object. 120 | 121 | Alternatively you can permanently delete a record by calling `destroy` or 122 | `delete_all` on the object **twice**. 123 | 124 | If a record was already deleted (hidden by `ActsAsParanoid`) and you delete it 125 | again, it will be removed from the database. 126 | 127 | Take this example: 128 | 129 | ```ruby 130 | p = Paranoiac.first 131 | 132 | # does NOT delete the first record, just hides it 133 | p.destroy 134 | 135 | # deletes the first record from the database 136 | Paranoiac.only_deleted.where(id: p.id).first.destroy 137 | ``` 138 | 139 | This behaviour can be disabled by setting the configuration option. In a future 140 | version, `false` will be the default setting. 141 | 142 | - `double_tap_destroys_fully: false` 143 | 144 | ### Recovery 145 | 146 | Recovery is easy. Just invoke `recover` on it, like this: 147 | 148 | ```ruby 149 | Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover 150 | ``` 151 | 152 | All associations marked as `dependent: :destroy` are also recursively recovered. 153 | 154 | If you would like to disable this behavior, you can call `recover` with the 155 | `recursive` option: 156 | 157 | ```ruby 158 | Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(recursive: false) 159 | ``` 160 | 161 | If you would like to change this default behavior for one model, you can use 162 | the `recover_dependent_associations` option 163 | 164 | ```ruby 165 | class Paranoiac < ActiveRecord::Base 166 | acts_as_paranoid recover_dependent_associations: false 167 | end 168 | ``` 169 | 170 | By default, dependent records will be recovered if they were deleted within 2 171 | minutes of the object upon which they depend. 172 | 173 | This restores the objects to the state before the recursive deletion without 174 | restoring other objects that were deleted earlier. 175 | 176 | The behavior is only available when both parent and dependant are using 177 | timestamp fields to mark deletion, which is the default behavior. 178 | 179 | This window can be changed with the `dependent_recovery_window` option: 180 | 181 | ```ruby 182 | class Paranoiac < ActiveRecord::Base 183 | acts_as_paranoid 184 | has_many :paranoids, dependent: :destroy 185 | end 186 | 187 | class Paranoid < ActiveRecord::Base 188 | belongs_to :paranoic 189 | 190 | # Paranoid objects will be recovered alongside Paranoic objects 191 | # if they were deleted within 10 minutes of the Paranoic object 192 | acts_as_paranoid dependent_recovery_window: 10.minutes 193 | end 194 | ``` 195 | 196 | or in the recover statement 197 | 198 | ```ruby 199 | Paranoiac.only_deleted.where("name = ?", "not dead yet").first 200 | .recover(recovery_window: 30.seconds) 201 | ``` 202 | 203 | ### recover! 204 | 205 | You can invoke `recover!` if you wish to raise an error if the recovery fails. 206 | The error generally stems from ActiveRecord. 207 | 208 | ```ruby 209 | Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover! 210 | # => ActiveRecord::RecordInvalid: Validation failed: Name already exists 211 | ``` 212 | 213 | Optionally, you may also raise the error by passing `raise_error: true` to the 214 | `recover` method. This behaves the same as `recover!`. 215 | 216 | ```ruby 217 | Paranoiac.only_deleted.where("name = ?", "not dead yet").first.recover(raise_error: true) 218 | ``` 219 | 220 | ### Validation 221 | 222 | ActiveRecord's built-in uniqueness validation does not account for records 223 | deleted by ActsAsParanoid. If you want to check for uniqueness among 224 | non-deleted records only, use the macro `validates_as_paranoid` in your model. 225 | Then, instead of using `validates_uniqueness_of`, use 226 | `validates_uniqueness_of_without_deleted`. This will keep deleted records from 227 | counting against the uniqueness check. 228 | 229 | ```ruby 230 | class Paranoiac < ActiveRecord::Base 231 | acts_as_paranoid 232 | validates_as_paranoid 233 | validates_uniqueness_of_without_deleted :name 234 | end 235 | 236 | p1 = Paranoiac.create(name: 'foo') 237 | p1.destroy 238 | 239 | p2 = Paranoiac.new(name: 'foo') 240 | p2.valid? #=> true 241 | p2.save 242 | 243 | p1.recover #=> fails validation! 244 | ``` 245 | 246 | ### Status 247 | 248 | A paranoid object could be deleted or destroyed fully. 249 | 250 | You can check if the object is deleted with the `deleted?` helper 251 | 252 | ```ruby 253 | Paranoiac.create(name: 'foo').destroy 254 | Paranoiac.with_deleted.first.deleted? #=> true 255 | ``` 256 | 257 | After the first call to `.destroy` the object is `deleted?`. 258 | 259 | You can check if the object is fully destroyed with `destroyed_fully?` or `deleted_fully?`. 260 | 261 | ```ruby 262 | Paranoiac.create(name: 'foo').destroy 263 | Paranoiac.with_deleted.first.deleted? #=> true 264 | Paranoiac.with_deleted.first.destroyed_fully? #=> false 265 | p1 = Paranoiac.with_deleted.first 266 | p1.destroy # this fully destroys the object 267 | p1.destroyed_fully? #=> true 268 | p1.deleted_fully? #=> true 269 | ``` 270 | 271 | ### Scopes 272 | 273 | As you've probably guessed, `with_deleted` and `only_deleted` are scopes. You 274 | can, however, chain them freely with other scopes you might have. 275 | 276 | For example: 277 | 278 | ```ruby 279 | Paranoiac.pretty.with_deleted 280 | ``` 281 | 282 | This is exactly the same as: 283 | 284 | ```ruby 285 | Paranoiac.with_deleted.pretty 286 | ``` 287 | 288 | You can work freely with scopes and it will just work: 289 | 290 | ```ruby 291 | class Paranoiac < ActiveRecord::Base 292 | acts_as_paranoid 293 | scope :pretty, where(pretty: true) 294 | end 295 | 296 | Paranoiac.create(pretty: true) 297 | 298 | Paranoiac.pretty.count #=> 1 299 | Paranoiac.only_deleted.count #=> 0 300 | Paranoiac.pretty.only_deleted.count #=> 0 301 | 302 | Paranoiac.first.destroy 303 | 304 | Paranoiac.pretty.count #=> 0 305 | Paranoiac.only_deleted.count #=> 1 306 | Paranoiac.pretty.only_deleted.count #=> 1 307 | ``` 308 | 309 | ### Associations 310 | 311 | Associations are also supported. 312 | 313 | From the simplest behaviors you'd expect to more nifty things like the ones 314 | mentioned previously or the usage of the `:with_deleted` option with 315 | `belongs_to` 316 | 317 | ```ruby 318 | class Parent < ActiveRecord::Base 319 | has_many :children, class_name: "ParanoiacChild" 320 | end 321 | 322 | class ParanoiacChild < ActiveRecord::Base 323 | acts_as_paranoid 324 | belongs_to :parent 325 | 326 | # You may need to provide a foreign_key like this 327 | belongs_to :parent_including_deleted, class_name: "Parent", 328 | foreign_key: 'parent_id', with_deleted: true 329 | end 330 | 331 | parent = Parent.first 332 | child = parent.children.create 333 | parent.destroy 334 | 335 | child.parent #=> nil 336 | child.parent_including_deleted #=> Parent (it works!) 337 | ``` 338 | 339 | ### Callbacks 340 | 341 | There are couple of callbacks that you may use when dealing with deletion and 342 | recovery of objects. There is `before_recover` and `after_recover` which will 343 | be triggered before and after the recovery of an object respectively. 344 | 345 | Default ActiveRecord callbacks such as `before_destroy` and `after_destroy` will 346 | be triggered around `.destroy!` and `.destroy_fully!`. 347 | 348 | ```ruby 349 | class Paranoiac < ActiveRecord::Base 350 | acts_as_paranoid 351 | 352 | before_recover :set_counts 353 | after_recover :update_logs 354 | end 355 | ``` 356 | 357 | ## Caveats 358 | 359 | Watch out for these caveats: 360 | 361 | - You cannot use scopes named `with_deleted` and `only_deleted` 362 | - You cannot use scopes named `deleted_inside_time_window`, 363 | `deleted_before_time`, `deleted_after_time` **if** your paranoid column's 364 | type is `time` 365 | - You cannot name association `*_with_deleted` 366 | - `unscoped` will return all records, deleted or not 367 | 368 | # Acknowledgements 369 | 370 | * To [Rick Olson](https://github.com/technoweenie) for creating `acts_as_paranoid` 371 | * To [cheerfulstoic](https://github.com/cheerfulstoic) for adding recursive recovery 372 | * To [Jonathan Vaught](https://github.com/gravelpup) for adding paranoid validations 373 | * To [Geoffrey Hichborn](https://github.com/phene) for improving the overral code quality and adding support for after_commit 374 | * To [flah00](https://github.com/flah00) for adding support for STI-based associations (with :dependent) 375 | * To [vikramdhillon](https://github.com/vikramdhillon) for the idea and initial implementation of support for string column type 376 | * To [Craig Walker](https://github.com/softcraft-development) for Rails 3.1 support and fixing various pending issues 377 | * To [Charles G.](https://github.com/chuckg) for Rails 3.2 support and for making a desperately needed global code refactoring 378 | * To [Gonçalo Silva](https://github.com/goncalossilva) for supporting this gem prior to v0.4.3 379 | * To [Jean Boussier](https://github.com/byroot) for initial Rails 4.0.0 support 380 | * To [Matijs van Zuijlen](https://github.com/mvz) for Rails 4.1 and 4.2 support 381 | * To [Andrey Ponomarenko](https://github.com/sjke) for Rails 5 support 382 | * To [Daniel Rice](https://github.com/danielricecodes), [Josh Bryant](https://github.com/jbryant92), and [Romain Alexandre](https://github.com/RomainAlexandre) for Rails 6.0 support. 383 | 384 | See `LICENSE`. 385 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rake/manifest/task" 5 | require "rake/testtask" 6 | require "rdoc/task" 7 | require "rubocop/rake_task" 8 | 9 | Rake::TestTask.new(:test) do |t| 10 | t.libs << "test" 11 | t.pattern = "test/**/*_test.rb" 12 | t.verbose = true 13 | end 14 | 15 | RuboCop::RakeTask.new 16 | 17 | desc "Generate documentation for the acts_as_paranoid plugin." 18 | Rake::RDocTask.new(:rdoc) do |rdoc| 19 | rdoc.rdoc_dir = "rdoc" 20 | rdoc.title = "ActsAsParanoid" 21 | rdoc.options << "--line-numbers" << "--inline-source" 22 | rdoc.rdoc_files.include("README") 23 | rdoc.rdoc_files.include("lib/**/*.rb") 24 | end 25 | 26 | desc "Clean automatically generated files" 27 | task :clean do 28 | FileUtils.rm_rf "pkg" 29 | end 30 | 31 | Rake::Manifest::Task.new do |t| 32 | t.patterns = ["{lib}/**/*", "LICENSE", "*.md"] 33 | end 34 | 35 | task build: ["manifest:check"] 36 | 37 | desc "Default: run tests and check manifest" 38 | task default: ["test", "manifest:check"] 39 | -------------------------------------------------------------------------------- /acts_as_paranoid.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/acts_as_paranoid/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "acts_as_paranoid" 7 | spec.version = ActsAsParanoid::VERSION 8 | spec.authors = ["Zachary Scott", "Goncalo Silva", "Rick Olson"] 9 | spec.email = ["e@zzak.io"] 10 | 11 | spec.summary = "Active Record plugin which allows you to hide and restore" \ 12 | " records without actually deleting them." 13 | spec.description = "Check the home page for more in-depth information." 14 | spec.homepage = "https://github.com/ActsAsParanoid/acts_as_paranoid" 15 | spec.license = "MIT" 16 | spec.required_ruby_version = ">= 3.1.0" 17 | 18 | spec.metadata["homepage_uri"] = spec.homepage 19 | spec.metadata["changelog_uri"] = "https://github.com/ActsAsParanoid/acts_as_paranoid/blob/master/CHANGELOG.md" 20 | spec.metadata["rubygems_mfa_required"] = "true" 21 | 22 | spec.files = File.read("Manifest.txt").split 23 | spec.require_paths = ["lib"] 24 | 25 | spec.add_dependency "activerecord", ">= 6.1", "< 8.1" 26 | spec.add_dependency "activesupport", ">= 6.1", "< 8.1" 27 | 28 | spec.add_development_dependency "appraisal", "~> 2.3" 29 | spec.add_development_dependency "minitest", "~> 5.14" 30 | spec.add_development_dependency "minitest-around", "~> 0.5" 31 | spec.add_development_dependency "minitest-focus", "~> 1.3" 32 | spec.add_development_dependency "minitest-stub-const", "~> 0.6" 33 | spec.add_development_dependency "rake", "~> 13.0" 34 | spec.add_development_dependency "rake-manifest", "~> 0.2.0" 35 | spec.add_development_dependency "rdoc", "~> 6.3" 36 | spec.add_development_dependency "rubocop", "~> 1.52" 37 | spec.add_development_dependency "rubocop-minitest", "~> 0.38.0" 38 | spec.add_development_dependency "rubocop-packaging", "~> 0.6.0" 39 | spec.add_development_dependency "rubocop-performance", "~> 1.18" 40 | spec.add_development_dependency "rubocop-rake", "~> 0.7.1" 41 | spec.add_development_dependency "simplecov", "~> 0.22.0" 42 | end 43 | -------------------------------------------------------------------------------- /gemfiles/active_record_61.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source "https://rubygems.org" 6 | 7 | gem "activerecord", "~> 6.1.0", require: "active_record" 8 | gem "activesupport", "~> 6.1.0", require: "active_support" 9 | 10 | group :development do 11 | gem "activerecord-jdbcsqlite3-adapter", "~> 61.1", platforms: [:jruby] 12 | gem "sqlite3", "~> 1.4", platforms: [:ruby] 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /gemfiles/active_record_70.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source "https://rubygems.org" 6 | 7 | gem "activerecord", "~> 7.0.0", require: "active_record" 8 | gem "activesupport", "~> 7.0.0", require: "active_support" 9 | 10 | group :development do 11 | gem "activerecord-jdbcsqlite3-adapter", "~> 70.0", platforms: [:jruby] 12 | gem "sqlite3", "~> 1.4", platforms: [:ruby] 13 | end 14 | 15 | gemspec path: "../" 16 | -------------------------------------------------------------------------------- /gemfiles/active_record_71.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source "https://rubygems.org" 6 | 7 | gem "activerecord", "~> 7.1.0", require: "active_record" 8 | gem "activesupport", "~> 7.1.0", require: "active_support" 9 | 10 | group :development do 11 | gem "sqlite3", "~> 1.4", platforms: [:ruby] 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/active_record_72.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source "https://rubygems.org" 6 | 7 | gem "activerecord", "~> 7.2.0", require: "active_record" 8 | gem "activesupport", "~> 7.2.0", require: "active_support" 9 | 10 | group :development do 11 | gem "sqlite3", "~> 2.0", platforms: [:ruby] 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/active_record_80.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file was generated by Appraisal 4 | 5 | source "https://rubygems.org" 6 | 7 | gem "activerecord", "~> 8.0.0", require: "active_record" 8 | gem "activesupport", "~> 8.0.0", require: "active_support" 9 | 10 | group :development do 11 | gem "sqlite3", "~> 2.0", platforms: [:ruby] 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /lib/acts_as_paranoid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "acts_as_paranoid/core" 5 | require "acts_as_paranoid/associations" 6 | require "acts_as_paranoid/validations" 7 | require "acts_as_paranoid/relation" 8 | require "acts_as_paranoid/association_reflection" 9 | 10 | module ActsAsParanoid 11 | def paranoid? 12 | included_modules.include?(ActsAsParanoid::Core) 13 | end 14 | 15 | def validates_as_paranoid 16 | include ActsAsParanoid::Validations 17 | end 18 | 19 | def acts_as_paranoid(options = {}) 20 | if !options.is_a?(Hash) && !options.empty? 21 | raise ArgumentError, "Hash expected, got #{options.class.name}" 22 | end 23 | 24 | class_attribute :paranoid_configuration 25 | 26 | self.paranoid_configuration = { 27 | column: "deleted_at", 28 | column_type: "time", 29 | recover_dependent_associations: true, 30 | dependent_recovery_window: 2.minutes, 31 | double_tap_destroys_fully: true 32 | } 33 | paranoid_configuration[:deleted_value] = "deleted" if options[:column_type] == "string" 34 | 35 | paranoid_configuration.merge!(options) # user options 36 | 37 | unless %w[time boolean string].include? paranoid_configuration[:column_type] 38 | raise ArgumentError, 39 | "'time', 'boolean' or 'string' expected for :column_type option," \ 40 | " got #{paranoid_configuration[:column_type]}" 41 | end 42 | 43 | return if paranoid? 44 | 45 | include ActsAsParanoid::Core 46 | 47 | # Magic! 48 | default_scope { where(paranoid_default_scope) } 49 | 50 | define_deleted_time_scopes if paranoid_column_type == :time 51 | end 52 | end 53 | 54 | ActiveSupport.on_load(:active_record) do 55 | # Extend ActiveRecord's functionality 56 | extend ActsAsParanoid 57 | 58 | # Extend ActiveRecord::Base with paranoid associations 59 | include ActsAsParanoid::Associations 60 | 61 | # Override ActiveRecord::Relation's behavior 62 | ActiveRecord::Relation.include ActsAsParanoid::Relation 63 | 64 | # Push the recover callback onto the activerecord callback list 65 | ActiveRecord::Callbacks::CALLBACKS.push(:before_recover, :after_recover) 66 | 67 | ActiveRecord::Reflection::AssociationReflection 68 | .prepend ActsAsParanoid::AssociationReflection 69 | end 70 | -------------------------------------------------------------------------------- /lib/acts_as_paranoid/association_reflection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActsAsParanoid 4 | # Override for ActiveRecord::Reflection::AssociationReflection 5 | # 6 | # This makes automatic finding of inverse associations work where the 7 | # inverse is a belongs_to association with the :with_deleted option set. 8 | # 9 | # Specifying :with_deleted for the belongs_to association would stop the 10 | # inverse from being calculated because it sets scope where there was none, 11 | # and normally an association having a scope means ActiveRecord will not 12 | # automatically find the inverse association. 13 | # 14 | # This override adds an exception to that rule only for the case where the 15 | # scope was added just to support the :with_deleted option. 16 | module AssociationReflection 17 | if ActiveRecord::VERSION::MAJOR < 7 18 | def can_find_inverse_of_automatically?(reflection) 19 | options = reflection.options 20 | 21 | if reflection.macro == :belongs_to && options[:with_deleted] 22 | return false if options[:inverse_of] == false 23 | return false if options[:foreign_key] 24 | 25 | !options.fetch(:original_scope) 26 | else 27 | super 28 | end 29 | end 30 | else 31 | def scope_allows_automatic_inverse_of?(reflection, inverse_reflection) 32 | if reflection.scope 33 | options = reflection.options 34 | return true if options[:with_deleted] && !options.fetch(:original_scope) 35 | end 36 | 37 | super 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/acts_as_paranoid/associations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActsAsParanoid 4 | # This module is included in ActiveRecord::Base to provide paranoid associations. 5 | module Associations 6 | def self.included(base) 7 | base.extend ClassMethods 8 | class << base 9 | alias_method :belongs_to_without_deleted, :belongs_to 10 | alias_method :belongs_to, :belongs_to_with_deleted 11 | end 12 | end 13 | 14 | module ClassMethods 15 | def belongs_to_with_deleted(target, scope = nil, options = {}) 16 | if scope.is_a?(Hash) 17 | options = scope 18 | scope = nil 19 | end 20 | 21 | with_deleted = options.delete(:with_deleted) 22 | if with_deleted 23 | original_scope = scope 24 | scope = make_scope_with_deleted(scope) 25 | end 26 | 27 | result = belongs_to_without_deleted(target, scope, **options) 28 | 29 | if with_deleted 30 | options = result.values.last.options 31 | options[:with_deleted] = with_deleted 32 | options[:original_scope] = original_scope 33 | end 34 | 35 | result 36 | end 37 | 38 | private 39 | 40 | def make_scope_with_deleted(scope) 41 | if scope 42 | old_scope = scope 43 | scope = proc do |*args| 44 | if old_scope.arity == 0 45 | instance_exec(&old_scope).with_deleted 46 | else 47 | old_scope.call(*args).with_deleted 48 | end 49 | end 50 | else 51 | scope = proc do 52 | if respond_to? :with_deleted 53 | with_deleted 54 | else 55 | all 56 | end 57 | end 58 | end 59 | 60 | scope 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/acts_as_paranoid/core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActsAsParanoid 4 | module Core 5 | def self.included(base) 6 | base.extend ClassMethods 7 | end 8 | 9 | module ClassMethods 10 | def self.extended(base) 11 | base.define_callbacks :recover 12 | end 13 | 14 | def before_recover(method) 15 | set_callback :recover, :before, method 16 | end 17 | 18 | def after_recover(method) 19 | set_callback :recover, :after, method 20 | end 21 | 22 | def with_deleted 23 | without_paranoid_default_scope 24 | end 25 | 26 | def only_deleted 27 | if string_type_with_deleted_value? 28 | without_paranoid_default_scope 29 | .where(paranoid_column_reference => paranoid_configuration[:deleted_value]) 30 | elsif boolean_type_not_nullable? 31 | without_paranoid_default_scope.where(paranoid_column_reference => true) 32 | else 33 | without_paranoid_default_scope.where.not(paranoid_column_reference => nil) 34 | end 35 | end 36 | 37 | def delete_all!(conditions = nil) 38 | without_paranoid_default_scope.delete_all!(conditions) 39 | end 40 | 41 | def delete_all(conditions = nil) 42 | where(conditions) 43 | .update_all(["#{paranoid_configuration[:column]} = ?", delete_now_value]) 44 | end 45 | 46 | def paranoid_default_scope 47 | if string_type_with_deleted_value? 48 | all.table[paranoid_column].eq(nil) 49 | .or(all.table[paranoid_column].not_eq(paranoid_configuration[:deleted_value])) 50 | elsif boolean_type_not_nullable? 51 | all.table[paranoid_column].eq(false) 52 | else 53 | all.table[paranoid_column].eq(nil) 54 | end 55 | end 56 | 57 | def string_type_with_deleted_value? 58 | paranoid_column_type == :string && !paranoid_configuration[:deleted_value].nil? 59 | end 60 | 61 | def boolean_type_not_nullable? 62 | paranoid_column_type == :boolean && !paranoid_configuration[:allow_nulls] 63 | end 64 | 65 | def paranoid_column 66 | paranoid_configuration[:column].to_sym 67 | end 68 | 69 | def paranoid_column_type 70 | paranoid_configuration[:column_type].to_sym 71 | end 72 | 73 | def paranoid_column_reference 74 | "#{table_name}.#{paranoid_column}" 75 | end 76 | 77 | DESTROYING_ASSOCIATION_DEPENDENCY_TYPES = [:destroy, :delete_all].freeze 78 | 79 | def dependent_associations 80 | reflect_on_all_associations.select do |a| 81 | DESTROYING_ASSOCIATION_DEPENDENCY_TYPES.include?(a.options[:dependent]) 82 | end 83 | end 84 | 85 | def delete_now_value 86 | case paranoid_configuration[:column_type] 87 | when "time" then Time.now 88 | when "boolean" then true 89 | when "string" then paranoid_configuration[:deleted_value] 90 | end 91 | end 92 | 93 | def recovery_value 94 | if boolean_type_not_nullable? 95 | false 96 | else 97 | nil 98 | end 99 | end 100 | 101 | protected 102 | 103 | def define_deleted_time_scopes 104 | scope :deleted_inside_time_window, lambda { |time, window| 105 | deleted_after_time((time - window)).deleted_before_time((time + window)) 106 | } 107 | 108 | scope :deleted_after_time, lambda { |time| 109 | only_deleted 110 | .where("#{table_name}.#{paranoid_column} > ?", time) 111 | } 112 | scope :deleted_before_time, lambda { |time| 113 | only_deleted 114 | .where("#{table_name}.#{paranoid_column} < ?", time) 115 | } 116 | end 117 | 118 | def without_paranoid_default_scope 119 | scope = all 120 | 121 | unless scope.unscope_values.include?({ where: paranoid_column }) 122 | # unscope avoids applying the default scope when using this scope for associations 123 | scope = scope.unscope(where: paranoid_column) 124 | end 125 | 126 | scope 127 | end 128 | end 129 | 130 | def persisted? 131 | !(new_record? || @destroyed) 132 | end 133 | 134 | def paranoid_value 135 | send(self.class.paranoid_column) 136 | end 137 | 138 | # Straight from ActiveRecord 5.1! 139 | def delete 140 | self.class.delete(id) if persisted? 141 | stale_paranoid_value 142 | freeze 143 | end 144 | 145 | def destroy_fully! 146 | with_transaction_returning_status do 147 | run_callbacks :destroy do 148 | destroy_dependent_associations! 149 | 150 | if persisted? 151 | # Handle composite keys, otherwise we would just use 152 | # `self.class.primary_key.to_sym => self.id`. 153 | self.class 154 | .delete_all!([Array(self.class.primary_key), Array(id)].transpose.to_h) 155 | decrement_counters_on_associations 156 | end 157 | 158 | @destroyed = true 159 | freeze 160 | end 161 | end 162 | end 163 | 164 | def destroy! 165 | destroy || raise( 166 | ActiveRecord::RecordNotDestroyed.new("Failed to destroy the record", self) 167 | ) 168 | end 169 | 170 | def destroy 171 | if !deleted? 172 | with_transaction_returning_status do 173 | run_callbacks :destroy do 174 | if persisted? 175 | # Handle composite keys, otherwise we would just use 176 | # `self.class.primary_key.to_sym => self.id`. 177 | self.class 178 | .delete_all([Array(self.class.primary_key), Array(id)].transpose.to_h) 179 | decrement_counters_on_associations 180 | end 181 | 182 | @_trigger_destroy_callback = true 183 | 184 | stale_paranoid_value 185 | self 186 | end 187 | end 188 | elsif paranoid_configuration[:double_tap_destroys_fully] 189 | destroy_fully! 190 | end 191 | end 192 | 193 | def recover(options = {}) 194 | return if !deleted? 195 | 196 | options = { 197 | recursive: self.class.paranoid_configuration[:recover_dependent_associations], 198 | recovery_window: self.class.paranoid_configuration[:dependent_recovery_window], 199 | raise_error: false 200 | }.merge(options) 201 | 202 | self.class.transaction do 203 | run_callbacks :recover do 204 | increment_counters_on_associations 205 | deleted_value = paranoid_value 206 | self.paranoid_value = self.class.recovery_value 207 | result = if options[:raise_error] 208 | save! 209 | else 210 | save 211 | end 212 | recover_dependent_associations(deleted_value, options) if options[:recursive] 213 | result 214 | end 215 | end 216 | end 217 | 218 | def recover!(options = {}) 219 | options[:raise_error] = true 220 | 221 | recover(options) 222 | end 223 | 224 | def deleted? 225 | return true if @destroyed 226 | 227 | if self.class.string_type_with_deleted_value? 228 | paranoid_value == paranoid_configuration[:deleted_value] 229 | elsif self.class.boolean_type_not_nullable? 230 | paranoid_value == true 231 | else 232 | !paranoid_value.nil? 233 | end 234 | end 235 | 236 | alias destroyed? deleted? 237 | 238 | def deleted_fully? 239 | @destroyed 240 | end 241 | 242 | alias destroyed_fully? deleted_fully? 243 | 244 | private 245 | 246 | def recover_dependent_associations(deleted_value, options) 247 | self.class.dependent_associations.each do |reflection| 248 | recover_dependent_association(reflection, deleted_value, options) 249 | end 250 | end 251 | 252 | def destroy_dependent_associations! 253 | self.class.dependent_associations.each do |reflection| 254 | assoc = association(reflection.name) 255 | next unless (klass = assoc.klass).paranoid? 256 | 257 | klass 258 | .only_deleted.merge(get_association_scope(assoc)) 259 | .each(&:destroy!) 260 | end 261 | end 262 | 263 | def recover_dependent_association(reflection, deleted_value, options) 264 | assoc = association(reflection.name) 265 | return unless (klass = assoc.klass).paranoid? 266 | 267 | if reflection.belongs_to? && attributes[reflection.association_foreign_key].nil? 268 | return 269 | end 270 | 271 | scope = klass.only_deleted.merge(get_association_scope(assoc)) 272 | 273 | # We can only recover by window if both parent and dependant have a 274 | # paranoid column type of :time. 275 | if self.class.paranoid_column_type == :time && klass.paranoid_column_type == :time 276 | scope = scope.deleted_inside_time_window(deleted_value, options[:recovery_window]) 277 | end 278 | 279 | recovered = false 280 | scope.each do |object| 281 | object.recover(options) 282 | recovered = true 283 | end 284 | 285 | assoc.reload if recovered && reflection.has_one? && assoc.loaded? 286 | end 287 | 288 | def get_association_scope(dependent_association) 289 | ActiveRecord::Associations::AssociationScope.scope(dependent_association) 290 | end 291 | 292 | def paranoid_value=(value) 293 | write_attribute(self.class.paranoid_column, value) 294 | end 295 | 296 | def update_counters_on_associations(method_sym) 297 | each_counter_cached_association_reflection do |assoc_reflection| 298 | reflection_options = assoc_reflection.options 299 | next unless reflection_options[:counter_cache] 300 | 301 | associated_object = send(assoc_reflection.name) 302 | next unless associated_object 303 | 304 | counter_cache_column = assoc_reflection.counter_cache_column 305 | associated_object.class.send(method_sym, counter_cache_column, 306 | associated_object.id) 307 | associated_object.touch if reflection_options[:touch] 308 | end 309 | end 310 | 311 | def each_counter_cached_association_reflection 312 | _reflections.each_value do |reflection| 313 | yield reflection if reflection.belongs_to? && reflection.counter_cache_column 314 | end 315 | end 316 | 317 | def increment_counters_on_associations 318 | update_counters_on_associations :increment_counter 319 | end 320 | 321 | def decrement_counters_on_associations 322 | update_counters_on_associations :decrement_counter 323 | end 324 | 325 | def stale_paranoid_value 326 | self.paranoid_value = self.class.delete_now_value 327 | clear_attribute_changes([self.class.paranoid_column]) 328 | end 329 | end 330 | end 331 | -------------------------------------------------------------------------------- /lib/acts_as_paranoid/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActsAsParanoid 4 | module Relation 5 | def self.included(base) 6 | base.class_eval do 7 | def paranoid? 8 | klass.try(:paranoid?) ? true : false 9 | end 10 | 11 | def paranoid_deletion_attributes 12 | { klass.paranoid_column => klass.delete_now_value } 13 | end 14 | 15 | alias_method :orig_delete_all, :delete_all 16 | def delete_all!(conditions = nil) 17 | if conditions 18 | where(conditions).delete_all! 19 | else 20 | orig_delete_all 21 | end 22 | end 23 | 24 | def delete_all(conditions = nil) 25 | if paranoid? 26 | where(conditions).update_all(paranoid_deletion_attributes) 27 | else 28 | delete_all!(conditions) 29 | end 30 | end 31 | 32 | def destroy_fully!(id_or_array) 33 | where(primary_key => id_or_array).orig_delete_all 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/acts_as_paranoid/validations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_support/core_ext/array/wrap" 4 | 5 | module ActsAsParanoid 6 | module Validations 7 | def self.included(base) 8 | base.extend ClassMethods 9 | end 10 | 11 | class UniquenessWithoutDeletedValidator < ActiveRecord::Validations::UniquenessValidator 12 | private 13 | 14 | def build_relation(klass, attribute, value) 15 | super.where(klass.paranoid_default_scope) 16 | end 17 | end 18 | 19 | module ClassMethods 20 | def validates_uniqueness_of_without_deleted(*attr_names) 21 | validates_with UniquenessWithoutDeletedValidator, _merge_attributes(attr_names) 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/acts_as_paranoid/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActsAsParanoid 4 | VERSION = "0.10.3" 5 | end 6 | -------------------------------------------------------------------------------- /test/integration/associations_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "minitest/spec" 5 | require "minitest/stub_const" 6 | require "minitest/around" 7 | 8 | class AssociationsTest < ActiveSupport::TestCase 9 | describe "a many-to-many association specified with has_many through:" do 10 | before do 11 | ActiveSupport::Dependencies::Reference.clear! if ActiveRecord::VERSION::MAJOR == 6 12 | ActiveRecord::Schema.define(version: 1) do 13 | create_table :authors do |t| 14 | t.datetime :deleted_at 15 | timestamps t 16 | end 17 | 18 | create_table :books do |t| 19 | t.datetime :deleted_at 20 | t.timestamps 21 | end 22 | 23 | create_table :authorships do |t| 24 | t.integer :author_id 25 | t.integer :book_id 26 | t.datetime :deleted_at 27 | t.timestamps 28 | end 29 | end 30 | end 31 | 32 | after do 33 | ActiveRecord::Base.connection.data_sources.each do |table| 34 | ActiveRecord::Base.connection.drop_table(table) 35 | end 36 | end 37 | 38 | around do |test| 39 | AssociationsTest.stub_consts(const_map) do 40 | test.call 41 | end 42 | end 43 | 44 | describe "when relation to join table is marked as dependent: destroy" do 45 | let(:const_map) do 46 | author_class = Class.new(ActiveRecord::Base) do 47 | has_many :authorships, dependent: :destroy 48 | has_many :books, through: :authorships 49 | end 50 | 51 | authorship_class = Class.new(ActiveRecord::Base) do 52 | belongs_to :author 53 | belongs_to :book 54 | end 55 | 56 | book_class = Class.new(ActiveRecord::Base) do 57 | has_many :authorships, dependent: :destroy 58 | has_many :authors, through: :authorships 59 | end 60 | 61 | { 62 | Author: author_class, 63 | Authorship: authorship_class, 64 | Book: book_class 65 | } 66 | end 67 | 68 | let(:author) { Author.first } 69 | let(:book) { Book.first } 70 | 71 | before do 72 | author = Author.create! 73 | author.books.create! 74 | end 75 | 76 | describe "when classes are not paranoid" do 77 | it "destroys the join record when calling destroy on the associated record" do 78 | book.destroy 79 | 80 | _(author.reload.books).must_equal [] 81 | _(author.authorships).must_equal [] 82 | end 83 | 84 | it "destroys just the join record when calling destroy on the association" do 85 | author.books.destroy(book) 86 | 87 | _(author.reload.books).must_equal [] 88 | _(author.authorships).must_equal [] 89 | _(Book.all).must_equal [book] 90 | end 91 | end 92 | 93 | describe "when classes are paranoid" do 94 | before do 95 | # NOTE: Because Book.authorships is dependent: destroy, if Book is 96 | # paranoid, Authorship should also be paranoid. 97 | Authorship.acts_as_paranoid 98 | Book.acts_as_paranoid 99 | end 100 | 101 | it "destroys the join record when calling destroy on the associated record" do 102 | book.destroy 103 | 104 | _(author.reload.books).must_equal [] 105 | _(author.authorships).must_equal [] 106 | end 107 | 108 | it "destroys just the join record when calling destroy on the association" do 109 | author.books.destroy(book) 110 | 111 | _(author.reload.books).must_equal [] 112 | _(author.authorships).must_equal [] 113 | _(Book.all).must_equal [book] 114 | end 115 | 116 | it "includes destroyed records with deleted join records in .with_deleted scope" do 117 | book.destroy 118 | 119 | _(author.reload.books.with_deleted).must_equal [book] 120 | end 121 | 122 | it "includes records with deleted join records in .with_deleted scope" do 123 | author.books.destroy(book) 124 | 125 | _(author.reload.books.with_deleted).must_equal [book] 126 | end 127 | end 128 | end 129 | 130 | describe "when relation to join table is not marked as dependent" do 131 | let(:const_map) do 132 | author_class = Class.new(ActiveRecord::Base) do 133 | has_many :authorships 134 | has_many :books, through: :authorships 135 | end 136 | 137 | authorship_class = Class.new(ActiveRecord::Base) do 138 | belongs_to :author 139 | belongs_to :book 140 | end 141 | 142 | book_class = Class.new(ActiveRecord::Base) do 143 | has_many :authorships 144 | has_many :authors, through: :authorships 145 | end 146 | 147 | { 148 | Author: author_class, 149 | Authorship: authorship_class, 150 | Book: book_class 151 | } 152 | end 153 | let(:author) { Author.first } 154 | let(:book) { Book.first } 155 | 156 | before do 157 | author = Author.create! 158 | author.books.create! 159 | end 160 | 161 | describe "when classes are not paranoid" do 162 | it "destroys just the associated record when calling destroy on it" do 163 | book.destroy 164 | 165 | _(author.reload.books).must_equal [] 166 | _(author.authorships).wont_equal [] 167 | end 168 | 169 | it "destroys just the join record when calling destroy on the association" do 170 | author.books.destroy(book) 171 | 172 | _(author.reload.books).must_equal [] 173 | _(author.authorships).must_equal [] 174 | _(Book.all).must_equal [book] 175 | end 176 | end 177 | 178 | describe "when classes are paranoid" do 179 | before do 180 | # NOTE: Because Book.authorships is dependent: destroy, if Book is 181 | # paranoid, Authorship should also be paranoid. 182 | Authorship.acts_as_paranoid 183 | Book.acts_as_paranoid 184 | end 185 | 186 | it "destroys the join record when calling destroy on the associated record" do 187 | book.destroy 188 | 189 | _(author.reload.books).must_equal [] 190 | _(author.authorships).wont_equal [] 191 | end 192 | 193 | it "destroys just the join record when calling destroy on the association" do 194 | author.books.destroy(book) 195 | 196 | _(author.reload.books).must_equal [] 197 | _(author.authorships).must_equal [] 198 | _(Book.all).must_equal [book] 199 | end 200 | 201 | it "includes destroyed associated records in .with_deleted scope" do 202 | book.destroy 203 | 204 | _(author.reload.books.with_deleted).must_equal [book] 205 | end 206 | 207 | it "includes records with deleted join records in .with_deleted scope" do 208 | author.books.destroy(book) 209 | 210 | _(author.reload.books.with_deleted).must_equal [book] 211 | end 212 | end 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /test/legacy/associations_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class AssociationsTest < ActiveSupport::TestCase 6 | class ParanoidManyManyParentLeft < ActiveRecord::Base 7 | has_many :paranoid_many_many_children 8 | has_many :paranoid_many_many_parent_rights, through: :paranoid_many_many_children 9 | end 10 | 11 | class ParanoidManyManyParentRight < ActiveRecord::Base 12 | has_many :paranoid_many_many_children 13 | has_many :paranoid_many_many_parent_lefts, through: :paranoid_many_many_children 14 | end 15 | 16 | class ParanoidManyManyChild < ActiveRecord::Base 17 | acts_as_paranoid 18 | belongs_to :paranoid_many_many_parent_left 19 | belongs_to :paranoid_many_many_parent_right 20 | end 21 | 22 | class ParanoidDestroyCompany < ActiveRecord::Base 23 | acts_as_paranoid 24 | validates :name, presence: true 25 | has_many :paranoid_products, dependent: :destroy 26 | end 27 | 28 | class ParanoidDeleteCompany < ActiveRecord::Base 29 | acts_as_paranoid 30 | validates :name, presence: true 31 | has_many :paranoid_products, dependent: :delete_all 32 | end 33 | 34 | class ParanoidProduct < ActiveRecord::Base 35 | acts_as_paranoid 36 | belongs_to :paranoid_destroy_company 37 | belongs_to :paranoid_delete_company 38 | validates_presence_of :name 39 | end 40 | 41 | class ParanoidBelongsToPolymorphic < ActiveRecord::Base 42 | acts_as_paranoid 43 | belongs_to :parent, polymorphic: true, with_deleted: true 44 | end 45 | 46 | class NotParanoidHasManyAsParent < ActiveRecord::Base 47 | has_many :paranoid_belongs_to_polymorphics, as: :parent, dependent: :destroy 48 | end 49 | 50 | class ParanoidHasManyAsParent < ActiveRecord::Base 51 | acts_as_paranoid 52 | has_many :paranoid_belongs_to_polymorphics, as: :parent, dependent: :destroy 53 | end 54 | 55 | class ParanoidHasManyDependant < ActiveRecord::Base 56 | acts_as_paranoid 57 | belongs_to :paranoid_time 58 | belongs_to :paranoid_time_with_scope, 59 | -> { where(name: "hello").includes(:not_paranoid) }, 60 | class_name: "ParanoidTime", foreign_key: :paranoid_time_id 61 | belongs_to :paranoid_time_with_deleted, class_name: "ParanoidTime", 62 | foreign_key: :paranoid_time_id, 63 | with_deleted: true 64 | belongs_to :paranoid_time_with_scope_with_deleted, 65 | -> { where(name: "hello").includes(:not_paranoid) }, 66 | class_name: "ParanoidTime", foreign_key: :paranoid_time_id, 67 | with_deleted: true 68 | belongs_to :paranoid_time_polymorphic_with_deleted, class_name: "ParanoidTime", 69 | foreign_key: :paranoid_time_id, 70 | polymorphic: true, 71 | with_deleted: true 72 | 73 | belongs_to :paranoid_belongs_dependant, dependent: :destroy 74 | end 75 | 76 | class ParanoidBelongsDependant < ActiveRecord::Base 77 | acts_as_paranoid 78 | 79 | has_many :paranoid_has_many_dependants 80 | end 81 | 82 | class ParanoidTime < ActiveRecord::Base 83 | acts_as_paranoid 84 | 85 | validates_uniqueness_of :name 86 | 87 | has_many :paranoid_has_many_dependants, dependent: :destroy 88 | has_many :paranoid_booleans, dependent: :destroy 89 | has_many :not_paranoids, dependent: :delete_all 90 | 91 | has_one :has_one_not_paranoid, dependent: :destroy 92 | 93 | belongs_to :not_paranoid, dependent: :destroy 94 | 95 | attr_accessor :destroyable 96 | 97 | before_destroy :ensure_destroyable 98 | 99 | protected 100 | 101 | def ensure_destroyable 102 | return if destroyable.nil? 103 | 104 | throw(:abort) unless destroyable 105 | end 106 | end 107 | 108 | class ParanoidBoolean < ActiveRecord::Base 109 | acts_as_paranoid column_type: "boolean", column: "is_deleted" 110 | validates_as_paranoid 111 | validates_uniqueness_of_without_deleted :name 112 | 113 | belongs_to :paranoid_time 114 | has_one :paranoid_has_one_dependant, dependent: :destroy 115 | has_many :paranoid_with_counter_cache, dependent: :destroy 116 | has_many :paranoid_with_custom_counter_cache, dependent: :destroy 117 | has_many :paranoid_with_touch_and_counter_cache, dependent: :destroy 118 | has_many :paranoid_with_touch, dependent: :destroy 119 | end 120 | 121 | class NotParanoid < ActiveRecord::Base 122 | has_many :paranoid_times 123 | end 124 | 125 | class HasOneNotParanoid < ActiveRecord::Base 126 | belongs_to :paranoid_time, with_deleted: true 127 | end 128 | 129 | class DoubleHasOneNotParanoid < HasOneNotParanoid 130 | belongs_to :paranoid_time, with_deleted: true 131 | begin 132 | verbose = $VERBOSE 133 | $VERBOSE = false 134 | belongs_to :paranoid_time, with_deleted: true 135 | ensure 136 | $VERBOSE = verbose 137 | end 138 | end 139 | 140 | class ParanoidParent < ActiveRecord::Base 141 | acts_as_paranoid 142 | has_many :paranoid_children 143 | has_many :paranoid_no_inverse_children 144 | has_many :paranoid_foreign_key_children 145 | end 146 | 147 | class ParanoidChild < ActiveRecord::Base 148 | acts_as_paranoid 149 | belongs_to :paranoid_parent, with_deleted: true 150 | end 151 | 152 | class ParanoidNoInverseChild < ActiveRecord::Base 153 | acts_as_paranoid 154 | belongs_to :paranoid_parent, with_deleted: true, inverse_of: false 155 | end 156 | 157 | class ParanoidForeignKeyChild < ActiveRecord::Base 158 | acts_as_paranoid 159 | belongs_to :paranoid_parent, with_deleted: true, foreign_key: :paranoid_parent_id 160 | end 161 | 162 | # rubocop:disable Metrics/AbcSize 163 | def setup 164 | ActiveRecord::Schema.define(version: 1) do # rubocop:disable Metrics/BlockLength 165 | create_table :paranoid_many_many_parent_lefts do |t| 166 | t.string :name 167 | timestamps t 168 | end 169 | 170 | create_table :paranoid_many_many_parent_rights do |t| 171 | t.string :name 172 | timestamps t 173 | end 174 | 175 | create_table :paranoid_many_many_children do |t| 176 | t.integer :paranoid_many_many_parent_left_id 177 | t.integer :paranoid_many_many_parent_right_id 178 | t.datetime :deleted_at 179 | timestamps t 180 | end 181 | 182 | create_table :paranoid_has_many_dependants do |t| 183 | t.string :name 184 | t.datetime :deleted_at 185 | t.integer :paranoid_time_id 186 | t.string :paranoid_time_polymorphic_with_deleted_type 187 | t.integer :paranoid_belongs_dependant_id 188 | 189 | timestamps t 190 | end 191 | 192 | create_table :paranoid_belongs_dependants do |t| 193 | t.string :name 194 | t.datetime :deleted_at 195 | 196 | timestamps t 197 | end 198 | 199 | create_table :paranoid_destroy_companies do |t| 200 | t.string :name 201 | t.datetime :deleted_at 202 | 203 | timestamps t 204 | end 205 | 206 | create_table :paranoid_delete_companies do |t| 207 | t.string :name 208 | t.datetime :deleted_at 209 | 210 | timestamps t 211 | end 212 | 213 | create_table :paranoid_products do |t| 214 | t.integer :paranoid_destroy_company_id 215 | t.integer :paranoid_delete_company_id 216 | t.string :name 217 | t.datetime :deleted_at 218 | 219 | timestamps t 220 | end 221 | 222 | create_table :paranoid_times do |t| 223 | t.string :name 224 | t.datetime :deleted_at 225 | t.integer :paranoid_belongs_dependant_id 226 | t.integer :not_paranoid_id 227 | 228 | timestamps t 229 | end 230 | 231 | create_table :paranoid_booleans do |t| 232 | t.string :name 233 | t.boolean :is_deleted 234 | t.integer :paranoid_time_id 235 | t.integer :paranoid_with_counter_caches_count 236 | t.integer :paranoid_with_touch_and_counter_caches_count 237 | t.integer :custom_counter_cache 238 | timestamps t 239 | end 240 | 241 | create_table :not_paranoid_has_many_as_parents do |t| 242 | t.string :name 243 | 244 | timestamps t 245 | end 246 | 247 | create_table :paranoid_has_many_as_parents do |t| 248 | t.string :name 249 | t.datetime :deleted_at 250 | 251 | timestamps t 252 | end 253 | 254 | create_table :not_paranoids do |t| 255 | t.string :name 256 | t.integer :paranoid_time_id 257 | 258 | timestamps t 259 | end 260 | 261 | create_table :has_one_not_paranoids do |t| 262 | t.string :name 263 | t.integer :paranoid_time_id 264 | 265 | timestamps t 266 | end 267 | 268 | create_table :paranoid_belongs_to_polymorphics do |t| 269 | t.string :name 270 | t.string :parent_type 271 | t.integer :parent_id 272 | t.datetime :deleted_at 273 | 274 | timestamps t 275 | end 276 | 277 | create_table :paranoid_parents do |t| 278 | t.datetime :deleted_at 279 | 280 | timestamps t 281 | end 282 | 283 | create_table :paranoid_children do |t| 284 | t.datetime :deleted_at 285 | t.integer :paranoid_parent_id 286 | 287 | timestamps t 288 | end 289 | 290 | create_table :paranoid_no_inverse_children do |t| 291 | t.datetime :deleted_at 292 | t.integer :paranoid_parent_id 293 | 294 | timestamps t 295 | end 296 | 297 | create_table :paranoid_foreign_key_children do |t| 298 | t.datetime :deleted_at 299 | t.integer :paranoid_parent_id 300 | 301 | timestamps t 302 | end 303 | end 304 | end 305 | # rubocop:enable Metrics/AbcSize 306 | 307 | def teardown 308 | teardown_db 309 | end 310 | 311 | def test_removal_with_destroy_associations 312 | paranoid_company = ParanoidDestroyCompany.create! name: "ParanoidDestroyCompany #1" 313 | paranoid_company.paranoid_products.create! name: "ParanoidProduct #1" 314 | 315 | assert_equal 1, ParanoidDestroyCompany.count 316 | assert_equal 1, ParanoidProduct.count 317 | 318 | ParanoidDestroyCompany.first.destroy 319 | 320 | assert_equal 0, ParanoidDestroyCompany.count 321 | assert_equal 0, ParanoidProduct.count 322 | assert_equal 1, ParanoidDestroyCompany.with_deleted.count 323 | assert_equal 1, ParanoidProduct.with_deleted.count 324 | 325 | ParanoidDestroyCompany.with_deleted.first.destroy 326 | 327 | assert_equal 0, ParanoidDestroyCompany.count 328 | assert_equal 0, ParanoidProduct.count 329 | assert_equal 0, ParanoidDestroyCompany.with_deleted.count 330 | assert_equal 0, ParanoidProduct.with_deleted.count 331 | end 332 | 333 | def test_removal_with_delete_all_associations 334 | paranoid_company = ParanoidDeleteCompany.create! name: "ParanoidDestroyCompany #1" 335 | paranoid_company.paranoid_products.create! name: "ParanoidProduct #2" 336 | 337 | assert_equal 1, ParanoidDeleteCompany.count 338 | assert_equal 1, ParanoidProduct.count 339 | 340 | ParanoidDeleteCompany.first.destroy 341 | 342 | assert_equal 0, ParanoidDeleteCompany.count 343 | assert_equal 0, ParanoidProduct.count 344 | assert_equal 1, ParanoidDeleteCompany.with_deleted.count 345 | assert_equal 1, ParanoidProduct.with_deleted.count 346 | 347 | ParanoidDeleteCompany.with_deleted.first.destroy 348 | 349 | assert_equal 0, ParanoidDeleteCompany.count 350 | assert_equal 0, ParanoidProduct.count 351 | assert_equal 0, ParanoidDeleteCompany.with_deleted.count 352 | assert_equal 0, ParanoidProduct.with_deleted.count 353 | end 354 | 355 | def test_belongs_to_with_scope_option 356 | paranoid_has_many_dependant = ParanoidHasManyDependant.new 357 | 358 | expected_includes_values = ParanoidTime.includes(:not_paranoid).includes_values 359 | includes_values = paranoid_has_many_dependant 360 | .association(:paranoid_time_with_scope).scope.includes_values 361 | 362 | assert_equal expected_includes_values, includes_values 363 | 364 | paranoid_time = ParanoidTime.create(name: "not-hello") 365 | paranoid_has_many_dependant.paranoid_time = paranoid_time 366 | paranoid_has_many_dependant.save! 367 | 368 | assert_nil paranoid_has_many_dependant.paranoid_time_with_scope 369 | 370 | paranoid_time.update(name: "hello") 371 | 372 | paranoid_has_many_dependant.reload 373 | 374 | assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time_with_scope 375 | 376 | paranoid_time.destroy 377 | 378 | paranoid_has_many_dependant.reload 379 | 380 | assert_nil paranoid_has_many_dependant.paranoid_time_with_scope 381 | end 382 | 383 | def test_belongs_to_with_scope_and_deleted_option 384 | paranoid_has_many_dependant = ParanoidHasManyDependant.new 385 | includes_values = ParanoidTime.includes(:not_paranoid).includes_values 386 | 387 | assert_equal includes_values, paranoid_has_many_dependant 388 | .association(:paranoid_time_with_scope_with_deleted).scope.includes_values 389 | 390 | paranoid_time = ParanoidTime.create(name: "not-hello") 391 | paranoid_has_many_dependant.paranoid_time = paranoid_time 392 | paranoid_has_many_dependant.save! 393 | 394 | assert_nil paranoid_has_many_dependant.paranoid_time_with_scope_with_deleted 395 | 396 | paranoid_time.update(name: "hello") 397 | paranoid_has_many_dependant.reload 398 | 399 | assert_equal paranoid_time, paranoid_has_many_dependant 400 | .paranoid_time_with_scope_with_deleted 401 | 402 | paranoid_time.destroy 403 | paranoid_has_many_dependant.reload 404 | 405 | assert_equal paranoid_time, paranoid_has_many_dependant 406 | .paranoid_time_with_scope_with_deleted 407 | end 408 | 409 | def test_belongs_to_with_deleted 410 | paranoid_time = ParanoidTime.create! name: "paranoid" 411 | paranoid_has_many_dependant = paranoid_time.paranoid_has_many_dependants 412 | .create(name: "dependant!") 413 | 414 | assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time 415 | assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time_with_deleted 416 | 417 | paranoid_time.destroy 418 | paranoid_has_many_dependant.reload 419 | 420 | assert_nil paranoid_has_many_dependant.paranoid_time 421 | assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time_with_deleted 422 | end 423 | 424 | def test_building_belongs_to_associations 425 | paranoid_product = ParanoidProduct.new 426 | paranoid_destroy_company = 427 | ParanoidDestroyCompany.new(paranoid_products: [paranoid_product]) 428 | 429 | assert_equal paranoid_destroy_company, 430 | paranoid_destroy_company.paranoid_products.first.paranoid_destroy_company 431 | end 432 | 433 | def test_building_belongs_to_associations_with_deleted 434 | paranoid_child = ParanoidChild.new 435 | paranoid_parent = ParanoidParent.new(paranoid_children: [paranoid_child]) 436 | 437 | assert_equal paranoid_parent, paranoid_parent.paranoid_children.first.paranoid_parent 438 | end 439 | 440 | def test_building_polymorphic_belongs_to_associations_with_deleted 441 | paranoid_belongs_to = ParanoidBelongsToPolymorphic.new 442 | paranoid_has_many = 443 | ParanoidHasManyAsParent.new(paranoid_belongs_to_polymorphics: [paranoid_belongs_to]) 444 | 445 | assert_equal paranoid_has_many, 446 | paranoid_has_many.paranoid_belongs_to_polymorphics.first.parent 447 | end 448 | 449 | def test_building_belongs_to_associations_with_deleted_with_inverse_of_false 450 | paranoid_child = ParanoidNoInverseChild.new 451 | paranoid_parent = ParanoidParent.new(paranoid_no_inverse_children: [paranoid_child]) 452 | 453 | assert_nil paranoid_parent.paranoid_no_inverse_children.first.paranoid_parent 454 | end 455 | 456 | def test_building_belongs_to_associations_with_deleted_with_foreign_key 457 | paranoid_child = ParanoidForeignKeyChild.new 458 | paranoid_parent = ParanoidParent.new(paranoid_foreign_key_children: [paranoid_child]) 459 | 460 | assert_nil paranoid_parent.paranoid_foreign_key_children.first.paranoid_parent 461 | end 462 | 463 | def test_belongs_to_with_deleted_as_inverse_of_has_many 464 | has_many_reflection = ParanoidParent.reflect_on_association :paranoid_children 465 | belongs_to_reflection = ParanoidChild.reflect_on_association :paranoid_parent 466 | 467 | assert_equal belongs_to_reflection, has_many_reflection.inverse_of 468 | end 469 | 470 | def test_belongs_to_polymorphic_with_deleted 471 | paranoid_time = ParanoidTime.create! name: "paranoid" 472 | paranoid_has_many_dependant = ParanoidHasManyDependant 473 | .create!(name: "dependant!", paranoid_time_polymorphic_with_deleted: paranoid_time) 474 | 475 | assert_equal paranoid_time, paranoid_has_many_dependant.paranoid_time 476 | assert_equal paranoid_time, paranoid_has_many_dependant 477 | .paranoid_time_polymorphic_with_deleted 478 | 479 | paranoid_time.destroy 480 | 481 | assert_nil paranoid_has_many_dependant.reload.paranoid_time 482 | assert_equal paranoid_time, paranoid_has_many_dependant 483 | .reload.paranoid_time_polymorphic_with_deleted 484 | end 485 | 486 | def test_belongs_to_nil_polymorphic_with_deleted 487 | paranoid_time = ParanoidTime.create! name: "paranoid" 488 | paranoid_has_many_dependant = 489 | ParanoidHasManyDependant.create!(name: "dependant!", 490 | paranoid_time_polymorphic_with_deleted: nil) 491 | 492 | assert_nil paranoid_has_many_dependant.paranoid_time 493 | assert_nil paranoid_has_many_dependant.paranoid_time_polymorphic_with_deleted 494 | 495 | paranoid_time.destroy 496 | 497 | assert_nil paranoid_has_many_dependant.reload.paranoid_time 498 | assert_nil paranoid_has_many_dependant.reload.paranoid_time_polymorphic_with_deleted 499 | end 500 | 501 | def test_belongs_to_options 502 | paranoid_time = ParanoidHasManyDependant.reflections 503 | .with_indifferent_access[:paranoid_time] 504 | 505 | assert_equal :belongs_to, paranoid_time.macro 506 | assert_nil paranoid_time.options[:with_deleted] 507 | end 508 | 509 | def test_belongs_to_with_deleted_options 510 | paranoid_time_with_deleted = 511 | ParanoidHasManyDependant.reflections 512 | .with_indifferent_access[:paranoid_time_with_deleted] 513 | 514 | assert_equal :belongs_to, paranoid_time_with_deleted.macro 515 | assert paranoid_time_with_deleted.options[:with_deleted] 516 | end 517 | 518 | def test_belongs_to_polymorphic_with_deleted_options 519 | paranoid_time_polymorphic_with_deleted = ParanoidHasManyDependant.reflections 520 | .with_indifferent_access[:paranoid_time_polymorphic_with_deleted] 521 | 522 | assert_equal :belongs_to, paranoid_time_polymorphic_with_deleted.macro 523 | assert paranoid_time_polymorphic_with_deleted.options[:with_deleted] 524 | end 525 | 526 | def test_only_find_associated_records_when_finding_with_paranoid_deleted 527 | parent = ParanoidBelongsDependant.create 528 | child = ParanoidHasManyDependant.create 529 | parent.paranoid_has_many_dependants << child 530 | 531 | unrelated_parent = ParanoidBelongsDependant.create 532 | unrelated_child = ParanoidHasManyDependant.create 533 | unrelated_parent.paranoid_has_many_dependants << unrelated_child 534 | 535 | child.destroy 536 | 537 | assert_paranoid_deletion(child) 538 | 539 | parent.reload 540 | 541 | assert_empty parent.paranoid_has_many_dependants.to_a 542 | assert_equal [child], parent.paranoid_has_many_dependants.with_deleted.to_a 543 | end 544 | 545 | def test_join_with_model_with_deleted 546 | obj = ParanoidHasManyDependant.create(paranoid_time: ParanoidTime.create) 547 | 548 | assert_not_nil obj.paranoid_time 549 | assert_not_nil obj.paranoid_time_with_deleted 550 | 551 | obj.paranoid_time.destroy 552 | obj.reload 553 | 554 | assert_nil obj.paranoid_time 555 | assert_not_nil obj.paranoid_time_with_deleted 556 | 557 | # Note that obj is destroyed because of dependent: :destroy in ParanoidTime 558 | assert_predicate obj, :destroyed? 559 | 560 | assert_empty ParanoidHasManyDependant.with_deleted.joins(:paranoid_time) 561 | assert_equal [obj], 562 | ParanoidHasManyDependant.with_deleted.joins(:paranoid_time_with_deleted) 563 | end 564 | 565 | def test_includes_with_deleted 566 | paranoid_time = ParanoidTime.create! name: "paranoid" 567 | paranoid_time.paranoid_has_many_dependants.create(name: "dependant!") 568 | 569 | paranoid_time.destroy 570 | 571 | ParanoidHasManyDependant.with_deleted 572 | .includes(:paranoid_time_with_deleted).each do |hasmany| 573 | assert_not_nil hasmany.paranoid_time_with_deleted 574 | end 575 | end 576 | 577 | def test_includes_with_deleted_with_polymorphic_parent 578 | not_paranoid_parent = NotParanoidHasManyAsParent.create(name: "not paranoid parent") 579 | paranoid_parent = ParanoidHasManyAsParent.create(name: "paranoid parent") 580 | ParanoidBelongsToPolymorphic.create(name: "belongs_to", parent: not_paranoid_parent) 581 | ParanoidBelongsToPolymorphic.create(name: "belongs_to", parent: paranoid_parent) 582 | 583 | paranoid_parent.destroy 584 | 585 | ParanoidBelongsToPolymorphic.with_deleted.includes(:parent).each do |hasmany| 586 | assert_not_nil hasmany.parent 587 | end 588 | end 589 | 590 | def test_cannot_find_a_paranoid_deleted_many_many_association 591 | left = ParanoidManyManyParentLeft.create 592 | right = ParanoidManyManyParentRight.create 593 | left.paranoid_many_many_parent_rights << right 594 | 595 | left.paranoid_many_many_parent_rights.delete(right) 596 | 597 | left.reload 598 | 599 | assert_empty left.paranoid_many_many_children, "Linking objects not deleted" 600 | assert_empty left.paranoid_many_many_parent_rights, 601 | "Associated objects not unlinked" 602 | assert_equal right, ParanoidManyManyParentRight.find(right.id), 603 | "Associated object deleted" 604 | end 605 | 606 | def test_cannot_find_a_paranoid_destroyed_many_many_association 607 | left = ParanoidManyManyParentLeft.create 608 | right = ParanoidManyManyParentRight.create 609 | left.paranoid_many_many_parent_rights << right 610 | 611 | left.paranoid_many_many_parent_rights.destroy(right) 612 | 613 | left.reload 614 | 615 | assert_empty left.paranoid_many_many_children, "Linking objects not deleted" 616 | assert_empty left.paranoid_many_many_parent_rights, 617 | "Associated objects not unlinked" 618 | assert_equal right, ParanoidManyManyParentRight.find(right.id), 619 | "Associated object deleted" 620 | end 621 | 622 | def test_cannot_find_a_has_many_through_object_when_its_linking_object_is_soft_destroyed 623 | left = ParanoidManyManyParentLeft.create 624 | right = ParanoidManyManyParentRight.create 625 | left.paranoid_many_many_parent_rights << right 626 | 627 | child = left.paranoid_many_many_children.first 628 | 629 | child.destroy 630 | 631 | left.reload 632 | 633 | assert_empty left.paranoid_many_many_parent_rights, "Associated objects not deleted" 634 | end 635 | 636 | def test_cannot_find_a_paranoid_deleted_model 637 | model = ParanoidBelongsDependant.create 638 | model.destroy 639 | 640 | assert_raises ActiveRecord::RecordNotFound do 641 | ParanoidBelongsDependant.find(model.id) 642 | end 643 | end 644 | 645 | def test_bidirectional_has_many_through_association_clear_is_paranoid 646 | left = ParanoidManyManyParentLeft.create 647 | right = ParanoidManyManyParentRight.create 648 | left.paranoid_many_many_parent_rights << right 649 | 650 | child = left.paranoid_many_many_children.first 651 | 652 | assert_equal left, child.paranoid_many_many_parent_left, 653 | "Child's left parent is incorrect" 654 | assert_equal right, child.paranoid_many_many_parent_right, 655 | "Child's right parent is incorrect" 656 | 657 | left.paranoid_many_many_parent_rights.clear 658 | 659 | assert_paranoid_deletion(child) 660 | end 661 | 662 | def test_bidirectional_has_many_through_association_destroy_is_paranoid 663 | left = ParanoidManyManyParentLeft.create 664 | right = ParanoidManyManyParentRight.create 665 | left.paranoid_many_many_parent_rights << right 666 | 667 | child = left.paranoid_many_many_children.first 668 | 669 | assert_equal left, child.paranoid_many_many_parent_left, 670 | "Child's left parent is incorrect" 671 | assert_equal right, child.paranoid_many_many_parent_right, 672 | "Child's right parent is incorrect" 673 | 674 | left.paranoid_many_many_parent_rights.destroy(right) 675 | 676 | assert_paranoid_deletion(child) 677 | end 678 | 679 | def test_bidirectional_has_many_through_association_delete_is_paranoid 680 | left = ParanoidManyManyParentLeft.create 681 | right = ParanoidManyManyParentRight.create 682 | left.paranoid_many_many_parent_rights << right 683 | 684 | child = left.paranoid_many_many_children.first 685 | 686 | assert_equal left, child.paranoid_many_many_parent_left, 687 | "Child's left parent is incorrect" 688 | assert_equal right, child.paranoid_many_many_parent_right, 689 | "Child's right parent is incorrect" 690 | 691 | left.paranoid_many_many_parent_rights.delete(right) 692 | 693 | assert_paranoid_deletion(child) 694 | end 695 | 696 | def test_belongs_to_on_normal_model_is_paranoid 697 | not_paranoid = HasOneNotParanoid.create 698 | not_paranoid.paranoid_time = ParanoidTime.create 699 | 700 | assert not_paranoid.save 701 | assert_not_nil not_paranoid.paranoid_time 702 | end 703 | 704 | def test_double_belongs_to_with_deleted 705 | not_paranoid = DoubleHasOneNotParanoid.create 706 | not_paranoid.paranoid_time = ParanoidTime.create 707 | 708 | assert not_paranoid.save 709 | assert_not_nil not_paranoid.paranoid_time 710 | end 711 | 712 | def test_mass_assignment_of_paranoid_column_disabled 713 | assert_raises ActiveRecord::RecordNotSaved do 714 | ParanoidTime.create! name: "Foo", deleted_at: Time.now 715 | end 716 | end 717 | end 718 | -------------------------------------------------------------------------------- /test/legacy/core_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ParanoidTest < ActiveSupport::TestCase 6 | class ParanoidTime < ActiveRecord::Base 7 | acts_as_paranoid 8 | 9 | validates_uniqueness_of :name 10 | 11 | has_many :paranoid_has_many_dependants, dependent: :destroy 12 | has_many :paranoid_booleans, dependent: :destroy 13 | has_many :not_paranoids, dependent: :delete_all 14 | has_many :paranoid_sections, dependent: :destroy 15 | 16 | has_one :has_one_not_paranoid, dependent: :destroy 17 | 18 | belongs_to :not_paranoid, dependent: :destroy 19 | 20 | attr_accessor :destroyable 21 | 22 | before_destroy :ensure_destroyable 23 | 24 | protected 25 | 26 | def ensure_destroyable 27 | return if destroyable.nil? 28 | 29 | throw(:abort) unless destroyable 30 | end 31 | end 32 | 33 | class ParanoidBoolean < ActiveRecord::Base 34 | acts_as_paranoid column_type: "boolean", column: "is_deleted" 35 | validates_as_paranoid 36 | validates_uniqueness_of_without_deleted :name 37 | 38 | belongs_to :paranoid_time 39 | has_one :paranoid_has_one_dependant, dependent: :destroy 40 | has_many :paranoid_with_counter_cache, dependent: :destroy 41 | has_many :paranoid_with_custom_counter_cache, dependent: :destroy 42 | has_many :paranoid_with_touch_and_counter_cache, dependent: :destroy 43 | has_many :paranoid_with_touch, dependent: :destroy 44 | end 45 | 46 | class ParanoidString < ActiveRecord::Base 47 | acts_as_paranoid column_type: "string", column: "deleted", deleted_value: "dead" 48 | end 49 | 50 | class NotParanoid < ActiveRecord::Base 51 | has_many :paranoid_times 52 | end 53 | 54 | class ParanoidNoDoubleTapDestroysFully < ActiveRecord::Base 55 | acts_as_paranoid double_tap_destroys_fully: false 56 | end 57 | 58 | class HasOneNotParanoid < ActiveRecord::Base 59 | belongs_to :paranoid_time, with_deleted: true 60 | end 61 | 62 | class ParanoidWithCounterCache < ActiveRecord::Base 63 | acts_as_paranoid 64 | belongs_to :paranoid_boolean, counter_cache: true 65 | end 66 | 67 | class ParanoidWithCustomCounterCache < ActiveRecord::Base 68 | self.table_name = "paranoid_with_counter_caches" 69 | 70 | acts_as_paranoid 71 | belongs_to :paranoid_boolean, counter_cache: :custom_counter_cache 72 | end 73 | 74 | class ParanoidWithCounterCacheOnOptionalBelognsTo < ActiveRecord::Base 75 | self.table_name = "paranoid_with_counter_caches" 76 | 77 | acts_as_paranoid 78 | belongs_to :paranoid_boolean, counter_cache: true, optional: true 79 | end 80 | 81 | class ParanoidWithTouch < ActiveRecord::Base 82 | self.table_name = "paranoid_with_counter_caches" 83 | acts_as_paranoid 84 | belongs_to :paranoid_boolean, touch: true 85 | end 86 | 87 | class ParanoidWithTouchAndCounterCache < ActiveRecord::Base 88 | self.table_name = "paranoid_with_counter_caches" 89 | acts_as_paranoid 90 | belongs_to :paranoid_boolean, touch: true, counter_cache: true 91 | end 92 | 93 | class ParanoidHasManyDependant < ActiveRecord::Base 94 | acts_as_paranoid 95 | 96 | belongs_to :paranoid_belongs_dependant, dependent: :destroy 97 | end 98 | 99 | class ParanoidBelongsDependant < ActiveRecord::Base 100 | acts_as_paranoid 101 | 102 | has_many :paranoid_has_many_dependants 103 | end 104 | 105 | class ParanoidHasOneDependant < ActiveRecord::Base 106 | acts_as_paranoid 107 | 108 | belongs_to :paranoid_boolean 109 | end 110 | 111 | class ParanoidWithCallback < ActiveRecord::Base 112 | acts_as_paranoid 113 | 114 | attr_accessor :called_before_destroy, :called_after_destroy, 115 | :called_after_commit_on_destroy, :called_before_recover, 116 | :called_after_recover 117 | 118 | before_destroy :call_me_before_destroy 119 | after_destroy :call_me_after_destroy 120 | 121 | after_commit :call_me_after_commit_on_destroy, on: :destroy 122 | 123 | before_recover :call_me_before_recover 124 | after_recover :call_me_after_recover 125 | 126 | def initialize(*attrs) 127 | @called_before_destroy = false 128 | @called_after_destroy = false 129 | @called_after_commit_on_destroy = false 130 | super 131 | end 132 | 133 | def call_me_before_destroy 134 | @called_before_destroy = true 135 | end 136 | 137 | def call_me_after_destroy 138 | @called_after_destroy = true 139 | end 140 | 141 | def call_me_after_commit_on_destroy 142 | @called_after_commit_on_destroy = true 143 | end 144 | 145 | def call_me_before_recover 146 | @called_before_recover = true 147 | end 148 | 149 | def call_me_after_recover 150 | @called_after_recover = true 151 | end 152 | end 153 | 154 | class ParanoidPolygon < ActiveRecord::Base 155 | acts_as_paranoid 156 | default_scope { where("sides = ?", 3) } 157 | end 158 | 159 | class ParanoidAndroid < ActiveRecord::Base 160 | acts_as_paranoid 161 | end 162 | 163 | class ParanoidSection < ActiveRecord::Base 164 | acts_as_paranoid 165 | belongs_to :paranoid_time 166 | belongs_to :paranoid_thing, polymorphic: true, dependent: :destroy 167 | end 168 | 169 | class ParanoidBooleanNotNullable < ActiveRecord::Base 170 | acts_as_paranoid column: "deleted", column_type: "boolean", allow_nulls: false 171 | end 172 | 173 | class ParanoidWithExplicitTableNameAfterMacro < ActiveRecord::Base 174 | acts_as_paranoid 175 | self.table_name = "explicit_table" 176 | end 177 | 178 | # rubocop:disable Metrics/AbcSize 179 | def setup_db 180 | ActiveRecord::Schema.define(version: 1) do # rubocop:disable Metrics/BlockLength 181 | create_table :paranoid_times do |t| 182 | t.string :name 183 | t.datetime :deleted_at 184 | t.integer :paranoid_belongs_dependant_id 185 | t.integer :not_paranoid_id 186 | 187 | timestamps t 188 | end 189 | 190 | create_table :paranoid_booleans do |t| 191 | t.string :name 192 | t.boolean :is_deleted 193 | t.integer :paranoid_time_id 194 | t.integer :paranoid_with_counter_caches_count 195 | t.integer :paranoid_with_touch_and_counter_caches_count 196 | t.integer :custom_counter_cache 197 | timestamps t 198 | end 199 | 200 | create_table :paranoid_strings do |t| 201 | t.string :name 202 | t.string :deleted 203 | end 204 | 205 | create_table :not_paranoids do |t| 206 | t.string :name 207 | t.integer :paranoid_time_id 208 | 209 | timestamps t 210 | end 211 | 212 | create_table :has_one_not_paranoids do |t| 213 | t.string :name 214 | t.integer :paranoid_time_id 215 | 216 | timestamps t 217 | end 218 | 219 | create_table :paranoid_has_many_dependants do |t| 220 | t.string :name 221 | t.datetime :deleted_at 222 | t.integer :paranoid_time_id 223 | t.string :paranoid_time_polymorphic_with_deleted_type 224 | t.integer :paranoid_belongs_dependant_id 225 | 226 | timestamps t 227 | end 228 | 229 | create_table :paranoid_belongs_dependants do |t| 230 | t.string :name 231 | t.datetime :deleted_at 232 | 233 | timestamps t 234 | end 235 | 236 | create_table :paranoid_has_one_dependants do |t| 237 | t.string :name 238 | t.datetime :deleted_at 239 | t.integer :paranoid_boolean_id 240 | 241 | timestamps t 242 | end 243 | 244 | create_table :paranoid_with_callbacks do |t| 245 | t.string :name 246 | t.datetime :deleted_at 247 | 248 | timestamps t 249 | end 250 | 251 | create_table :paranoid_polygons do |t| 252 | t.integer :sides 253 | t.datetime :deleted_at 254 | 255 | timestamps t 256 | end 257 | 258 | create_table :paranoid_androids do |t| 259 | t.datetime :deleted_at 260 | end 261 | 262 | create_table :paranoid_sections do |t| 263 | t.integer :paranoid_time_id 264 | t.integer :paranoid_thing_id 265 | t.string :paranoid_thing_type 266 | t.datetime :deleted_at 267 | end 268 | 269 | create_table :paranoid_boolean_not_nullables do |t| 270 | t.string :name 271 | t.boolean :deleted, :boolean, null: false, default: false 272 | end 273 | 274 | create_table :paranoid_no_double_tap_destroys_fullies do |t| 275 | t.datetime :deleted_at 276 | end 277 | 278 | create_table :paranoid_with_counter_caches do |t| 279 | t.string :name 280 | t.datetime :deleted_at 281 | t.integer :paranoid_boolean_id 282 | 283 | timestamps t 284 | end 285 | end 286 | end 287 | # rubocop:enable Metrics/AbcSize 288 | 289 | def setup 290 | setup_db 291 | 292 | ["paranoid", "really paranoid", "extremely paranoid"].each do |name| 293 | ParanoidTime.create! name: name 294 | ParanoidBoolean.create! name: name 295 | end 296 | 297 | ParanoidString.create! name: "strings can be paranoid" 298 | NotParanoid.create! name: "no paranoid goals" 299 | ParanoidWithCallback.create! name: "paranoid with callbacks" 300 | end 301 | 302 | def teardown 303 | teardown_db 304 | end 305 | 306 | def test_paranoid? 307 | refute_predicate NotParanoid, :paranoid? 308 | assert_raise(NoMethodError) { NotParanoid.delete_all! } 309 | assert_raise(NoMethodError) { NotParanoid.with_deleted } 310 | assert_raise(NoMethodError) { NotParanoid.only_deleted } 311 | 312 | assert_predicate ParanoidTime, :paranoid? 313 | end 314 | 315 | def test_scope_inclusion_with_time_column_type 316 | assert_respond_to ParanoidTime, :deleted_inside_time_window 317 | assert_respond_to ParanoidTime, :deleted_before_time 318 | assert_respond_to ParanoidTime, :deleted_after_time 319 | 320 | refute_respond_to ParanoidBoolean, :deleted_inside_time_window 321 | refute_respond_to ParanoidBoolean, :deleted_before_time 322 | refute_respond_to ParanoidBoolean, :deleted_after_time 323 | end 324 | 325 | def test_fake_removal 326 | assert_equal 3, ParanoidTime.count 327 | assert_equal 3, ParanoidBoolean.count 328 | assert_equal 1, ParanoidString.count 329 | 330 | ParanoidTime.first.destroy 331 | ParanoidBoolean.delete_all("name = 'paranoid' OR name = 'really paranoid'") 332 | ParanoidString.first.destroy 333 | 334 | assert_equal 2, ParanoidTime.count 335 | assert_equal 1, ParanoidBoolean.count 336 | assert_equal 0, ParanoidString.count 337 | assert_equal 1, ParanoidTime.only_deleted.count 338 | assert_equal 2, ParanoidBoolean.only_deleted.count 339 | assert_equal 1, ParanoidString.only_deleted.count 340 | assert_equal 3, ParanoidTime.with_deleted.count 341 | assert_equal 3, ParanoidBoolean.with_deleted.count 342 | assert_equal 1, ParanoidString.with_deleted.count 343 | end 344 | 345 | def test_real_removal 346 | ParanoidTime.first.destroy_fully! 347 | ParanoidBoolean.delete_all!("name = 'extremely paranoid' OR name = 'really paranoid'") 348 | ParanoidString.first.destroy_fully! 349 | 350 | assert_equal 2, ParanoidTime.count 351 | assert_equal 1, ParanoidBoolean.count 352 | assert_equal 0, ParanoidString.count 353 | assert_equal 2, ParanoidTime.with_deleted.count 354 | assert_equal 1, ParanoidBoolean.with_deleted.count 355 | assert_equal 0, ParanoidString.with_deleted.count 356 | assert_equal 0, ParanoidTime.only_deleted.count 357 | assert_equal 0, ParanoidBoolean.only_deleted.count 358 | assert_equal 0, ParanoidString.only_deleted.count 359 | 360 | ParanoidTime.first.destroy 361 | ParanoidTime.only_deleted.first.destroy 362 | 363 | assert_equal 0, ParanoidTime.only_deleted.count 364 | 365 | ParanoidTime.delete_all! 366 | 367 | assert_empty ParanoidTime.all 368 | assert_empty ParanoidTime.with_deleted 369 | end 370 | 371 | def test_non_persisted_destroy 372 | pt = ParanoidTime.new 373 | 374 | assert_nil pt.paranoid_value 375 | pt.destroy 376 | 377 | assert_not_nil pt.paranoid_value 378 | end 379 | 380 | def test_non_persisted_delete 381 | pt = ParanoidTime.new 382 | 383 | assert_nil pt.paranoid_value 384 | pt.delete 385 | 386 | assert_not_nil pt.paranoid_value 387 | end 388 | 389 | def test_non_persisted_destroy! 390 | pt = ParanoidTime.new 391 | 392 | assert_nil pt.paranoid_value 393 | pt.destroy! 394 | 395 | assert_not_nil pt.paranoid_value 396 | end 397 | 398 | def test_halted_destroy 399 | pt = ParanoidTime.create!(name: "john", destroyable: false) 400 | 401 | assert_raises ActiveRecord::RecordNotDestroyed do 402 | pt.destroy! 403 | end 404 | end 405 | 406 | def test_non_persisted_destroy_fully! 407 | pt = ParanoidTime.new 408 | 409 | assert_nil pt.paranoid_value 410 | pt.destroy_fully! 411 | 412 | assert_nil pt.paranoid_value 413 | end 414 | 415 | def test_removal_not_persisted 416 | assert ParanoidTime.new.destroy 417 | end 418 | 419 | def test_recovery 420 | assert_equal 3, ParanoidBoolean.count 421 | ParanoidBoolean.first.destroy 422 | 423 | assert_equal 2, ParanoidBoolean.count 424 | ParanoidBoolean.only_deleted.first.recover 425 | 426 | assert_equal 3, ParanoidBoolean.count 427 | 428 | assert_equal 1, ParanoidString.count 429 | ParanoidString.first.destroy 430 | 431 | assert_equal 0, ParanoidString.count 432 | ParanoidString.with_deleted.first.recover 433 | 434 | assert_equal 1, ParanoidString.count 435 | end 436 | 437 | def test_recovery! 438 | ParanoidBoolean.first.destroy 439 | ParanoidBoolean.create(name: "paranoid") 440 | 441 | assert_raise do 442 | ParanoidBoolean.only_deleted.first.recover! 443 | end 444 | end 445 | 446 | def test_recover_has_one_association 447 | parent = ParanoidBoolean.create(name: "parent") 448 | child = parent.create_paranoid_has_one_dependant(name: "child") 449 | 450 | parent.destroy 451 | 452 | assert_predicate parent.paranoid_has_one_dependant, :destroyed? 453 | 454 | parent.recover 455 | 456 | refute_predicate parent.paranoid_has_one_dependant, :destroyed? 457 | 458 | child.reload 459 | 460 | refute_predicate child, :destroyed? 461 | end 462 | 463 | def test_recover_has_many_association 464 | parent = ParanoidTime.create(name: "parent") 465 | child = parent.paranoid_has_many_dependants.create(name: "child") 466 | 467 | parent.destroy 468 | 469 | assert_predicate child, :destroyed? 470 | 471 | parent.recover 472 | 473 | assert_equal 1, parent.paranoid_has_many_dependants.count 474 | 475 | child.reload 476 | 477 | refute_predicate child, :destroyed? 478 | end 479 | 480 | # Rails does not allow saving deleted records 481 | def test_no_save_after_destroy 482 | paranoid = ParanoidString.first 483 | paranoid.destroy 484 | paranoid.name = "Let's update!" 485 | 486 | assert_not paranoid.save 487 | assert_raises ActiveRecord::RecordNotSaved do 488 | paranoid.save! 489 | end 490 | end 491 | 492 | def test_scope_chaining 493 | assert_equal 3, ParanoidBoolean.unscoped.with_deleted.count 494 | assert_equal 0, ParanoidBoolean.unscoped.only_deleted.count 495 | assert_equal 0, ParanoidBoolean.with_deleted.only_deleted.count 496 | assert_equal 3, ParanoidBoolean.with_deleted.with_deleted.count 497 | end 498 | 499 | def test_only_deleted_with_deleted_with_boolean_paranoid_column 500 | ParanoidBoolean.first.destroy 501 | 502 | assert_equal 1, ParanoidBoolean.only_deleted.count 503 | assert_equal 1, ParanoidBoolean.only_deleted.with_deleted.count 504 | end 505 | 506 | def test_with_deleted_only_deleted_with_boolean_paranoid_column 507 | ParanoidBoolean.first.destroy 508 | 509 | assert_equal 1, ParanoidBoolean.only_deleted.count 510 | assert_equal 1, ParanoidBoolean.with_deleted.only_deleted.count 511 | end 512 | 513 | def test_only_deleted_with_deleted_with_datetime_paranoid_column 514 | ParanoidTime.first.destroy 515 | 516 | assert_equal 1, ParanoidTime.only_deleted.count 517 | assert_equal 1, ParanoidTime.only_deleted.with_deleted.count 518 | end 519 | 520 | def test_with_deleted_only_deleted_with_datetime_paranoid_column 521 | ParanoidTime.first.destroy 522 | 523 | assert_equal 1, ParanoidTime.only_deleted.count 524 | assert_equal 1, ParanoidTime.with_deleted.only_deleted.count 525 | end 526 | 527 | def setup_recursive_tests 528 | @paranoid_time_object = ParanoidTime.first 529 | 530 | # Create one extra ParanoidHasManyDependant record so that we can validate 531 | # the correct dependants are recovered. 532 | ParanoidTime.where("id <> ?", @paranoid_time_object.id).first 533 | .paranoid_has_many_dependants.create(name: "should not be recovered").destroy 534 | 535 | @paranoid_boolean_count = ParanoidBoolean.count 536 | 537 | assert_equal 0, ParanoidHasManyDependant.count 538 | assert_equal 0, ParanoidBelongsDependant.count 539 | assert_equal 1, NotParanoid.count 540 | 541 | (1..3).each do |i| 542 | has_many_object = @paranoid_time_object.paranoid_has_many_dependants 543 | .create(name: "has_many_#{i}") 544 | has_many_object.create_paranoid_belongs_dependant(name: "belongs_to_#{i}") 545 | has_many_object.save 546 | 547 | paranoid_boolean = @paranoid_time_object.paranoid_booleans 548 | .create(name: "boolean_#{i}") 549 | paranoid_boolean.create_paranoid_has_one_dependant(name: "has_one_#{i}") 550 | paranoid_boolean.save 551 | 552 | @paranoid_time_object.not_paranoids.create(name: "not_paranoid_a#{i}") 553 | end 554 | 555 | @paranoid_time_object.create_not_paranoid(name: "not_paranoid_belongs_to") 556 | @paranoid_time_object.create_has_one_not_paranoid(name: "has_one_not_paranoid") 557 | 558 | assert_equal 3, ParanoidTime.count 559 | assert_equal 3, ParanoidHasManyDependant.count 560 | assert_equal 3, ParanoidBelongsDependant.count 561 | assert_equal @paranoid_boolean_count + 3, ParanoidBoolean.count 562 | assert_equal 3, ParanoidHasOneDependant.count 563 | assert_equal 5, NotParanoid.count 564 | assert_equal 1, HasOneNotParanoid.count 565 | end 566 | 567 | def test_recursive_fake_removal 568 | setup_recursive_tests 569 | 570 | @paranoid_time_object.destroy 571 | 572 | assert_equal 2, ParanoidTime.count 573 | assert_equal 0, ParanoidHasManyDependant.count 574 | assert_equal 0, ParanoidBelongsDependant.count 575 | assert_equal @paranoid_boolean_count, ParanoidBoolean.count 576 | assert_equal 0, ParanoidHasOneDependant.count 577 | assert_equal 1, NotParanoid.count 578 | assert_equal 0, HasOneNotParanoid.count 579 | 580 | assert_equal 3, ParanoidTime.with_deleted.count 581 | assert_equal 4, ParanoidHasManyDependant.with_deleted.count 582 | assert_equal 3, ParanoidBelongsDependant.with_deleted.count 583 | assert_equal @paranoid_boolean_count + 3, ParanoidBoolean.with_deleted.count 584 | assert_equal 3, ParanoidHasOneDependant.with_deleted.count 585 | end 586 | 587 | def test_recursive_real_removal 588 | setup_recursive_tests 589 | 590 | @paranoid_time_object.destroy_fully! 591 | 592 | assert_equal 0, ParanoidTime.only_deleted.count 593 | assert_equal 1, ParanoidHasManyDependant.only_deleted.count 594 | assert_equal 0, ParanoidBelongsDependant.only_deleted.count 595 | assert_equal 0, ParanoidBoolean.only_deleted.count 596 | assert_equal 0, ParanoidHasOneDependant.only_deleted.count 597 | assert_equal 1, NotParanoid.count 598 | assert_equal 0, HasOneNotParanoid.count 599 | end 600 | 601 | def test_recursive_recovery 602 | setup_recursive_tests 603 | 604 | @paranoid_time_object.destroy 605 | @paranoid_time_object.reload 606 | 607 | @paranoid_time_object.recover(recursive: true) 608 | 609 | assert_equal 3, ParanoidTime.count 610 | assert_equal 3, ParanoidHasManyDependant.count 611 | assert_equal 3, ParanoidBelongsDependant.count 612 | assert_equal @paranoid_boolean_count + 3, ParanoidBoolean.count 613 | assert_equal 3, ParanoidHasOneDependant.count 614 | assert_equal 1, NotParanoid.count 615 | assert_equal 0, HasOneNotParanoid.count 616 | end 617 | 618 | def test_recursive_recovery_dependant_window 619 | setup_recursive_tests 620 | 621 | # Stop the following from recovering: 622 | # - ParanoidHasManyDependant and its ParanoidBelongsDependant 623 | # - A single ParanoidBelongsDependant, but not its parent 624 | Time.stub :now, 2.days.ago do 625 | @paranoid_time_object.paranoid_has_many_dependants.first.destroy 626 | end 627 | Time.stub :now, 1.hour.ago do 628 | @paranoid_time_object.paranoid_has_many_dependants 629 | .last.paranoid_belongs_dependant 630 | .destroy 631 | end 632 | @paranoid_time_object.destroy 633 | @paranoid_time_object.reload 634 | 635 | @paranoid_time_object.recover(recursive: true) 636 | 637 | assert_equal 3, ParanoidTime.count 638 | assert_equal 2, ParanoidHasManyDependant.count 639 | assert_equal 1, ParanoidBelongsDependant.count 640 | assert_equal @paranoid_boolean_count + 3, ParanoidBoolean.count 641 | assert_equal 3, ParanoidHasOneDependant.count 642 | assert_equal 1, NotParanoid.count 643 | assert_equal 0, HasOneNotParanoid.count 644 | end 645 | 646 | def test_recursive_recovery_for_belongs_to_polymorphic 647 | child_1 = ParanoidAndroid.create 648 | section_1 = ParanoidSection.create(paranoid_thing: child_1) 649 | 650 | child_2 = ParanoidPolygon.create(sides: 3) 651 | section_2 = ParanoidSection.create(paranoid_thing: child_2) 652 | 653 | assert_equal section_1.paranoid_thing, child_1 654 | assert_equal section_1.paranoid_thing.class, ParanoidAndroid 655 | assert_equal section_2.paranoid_thing, child_2 656 | assert_equal section_2.paranoid_thing.class, ParanoidPolygon 657 | 658 | parent = ParanoidTime.create(name: "paranoid_parent") 659 | parent.paranoid_sections << section_1 660 | parent.paranoid_sections << section_2 661 | 662 | assert_equal 4, ParanoidTime.count 663 | assert_equal 2, ParanoidSection.count 664 | assert_equal 1, ParanoidAndroid.count 665 | assert_equal 1, ParanoidPolygon.count 666 | 667 | parent.destroy 668 | 669 | assert_equal 3, ParanoidTime.count 670 | assert_equal 0, ParanoidSection.count 671 | assert_equal 0, ParanoidAndroid.count 672 | assert_equal 0, ParanoidPolygon.count 673 | 674 | parent.reload 675 | parent.recover 676 | 677 | assert_equal 4, ParanoidTime.count 678 | assert_equal 2, ParanoidSection.count 679 | assert_equal 1, ParanoidAndroid.count 680 | assert_equal 1, ParanoidPolygon.count 681 | end 682 | 683 | def test_non_recursive_recovery 684 | setup_recursive_tests 685 | 686 | @paranoid_time_object.destroy 687 | @paranoid_time_object.reload 688 | 689 | @paranoid_time_object.recover(recursive: false) 690 | 691 | assert_equal 3, ParanoidTime.count 692 | assert_equal 0, ParanoidHasManyDependant.count 693 | assert_equal 0, ParanoidBelongsDependant.count 694 | assert_equal @paranoid_boolean_count, ParanoidBoolean.count 695 | assert_equal 0, ParanoidHasOneDependant.count 696 | assert_equal 1, NotParanoid.count 697 | assert_equal 0, HasOneNotParanoid.count 698 | end 699 | 700 | def test_dirty 701 | pt = ParanoidTime.create 702 | pt.destroy 703 | 704 | assert_not pt.changed? 705 | end 706 | 707 | def test_delete_dirty 708 | pt = ParanoidTime.create 709 | pt.delete 710 | 711 | assert_not pt.changed? 712 | end 713 | 714 | def test_destroy_fully_dirty 715 | pt = ParanoidTime.create 716 | pt.destroy_fully! 717 | 718 | assert_not pt.changed? 719 | end 720 | 721 | def test_deleted? 722 | ParanoidTime.first.destroy 723 | 724 | assert_predicate ParanoidTime.with_deleted.first, :deleted? 725 | 726 | ParanoidString.first.destroy 727 | 728 | assert_predicate ParanoidString.with_deleted.first, :deleted? 729 | end 730 | 731 | def test_delete_deleted? 732 | ParanoidTime.first.delete 733 | 734 | assert_predicate ParanoidTime.with_deleted.first, :deleted? 735 | 736 | ParanoidString.first.delete 737 | 738 | assert_predicate ParanoidString.with_deleted.first, :deleted? 739 | end 740 | 741 | def test_destroy_fully_deleted? 742 | object = ParanoidTime.first 743 | object.destroy_fully! 744 | 745 | assert_predicate object, :deleted? 746 | 747 | object = ParanoidString.first 748 | object.destroy_fully! 749 | 750 | assert_predicate object, :deleted? 751 | end 752 | 753 | def test_deleted_fully? 754 | ParanoidTime.first.destroy 755 | 756 | assert_not ParanoidTime.with_deleted.first.deleted_fully? 757 | 758 | ParanoidString.first.destroy 759 | 760 | assert_predicate ParanoidString.with_deleted.first, :deleted? 761 | end 762 | 763 | def test_delete_deleted_fully? 764 | ParanoidTime.first.delete 765 | 766 | assert_not ParanoidTime.with_deleted.first.deleted_fully? 767 | end 768 | 769 | def test_destroy_fully_deleted_fully? 770 | object = ParanoidTime.first 771 | object.destroy_fully! 772 | 773 | assert_predicate object, :deleted_fully? 774 | end 775 | 776 | def test_paranoid_destroy_callbacks 777 | @paranoid_with_callback = ParanoidWithCallback.first 778 | ParanoidWithCallback.transaction do 779 | @paranoid_with_callback.destroy 780 | end 781 | 782 | assert @paranoid_with_callback.called_before_destroy 783 | assert @paranoid_with_callback.called_after_destroy 784 | assert @paranoid_with_callback.called_after_commit_on_destroy 785 | end 786 | 787 | def test_hard_destroy_callbacks 788 | @paranoid_with_callback = ParanoidWithCallback.first 789 | 790 | ParanoidWithCallback.transaction do 791 | @paranoid_with_callback.destroy! 792 | end 793 | 794 | assert @paranoid_with_callback.called_before_destroy 795 | assert @paranoid_with_callback.called_after_destroy 796 | assert @paranoid_with_callback.called_after_commit_on_destroy 797 | end 798 | 799 | def test_recovery_callbacks 800 | @paranoid_with_callback = ParanoidWithCallback.first 801 | 802 | ParanoidWithCallback.transaction do 803 | @paranoid_with_callback.destroy 804 | 805 | assert_nil @paranoid_with_callback.called_before_recover 806 | assert_nil @paranoid_with_callback.called_after_recover 807 | 808 | @paranoid_with_callback.recover 809 | end 810 | 811 | assert @paranoid_with_callback.called_before_recover 812 | assert @paranoid_with_callback.called_after_recover 813 | end 814 | 815 | def test_recovery_callbacks_without_destroy 816 | @paranoid_with_callback = ParanoidWithCallback.first 817 | @paranoid_with_callback.recover 818 | 819 | assert_nil @paranoid_with_callback.called_before_recover 820 | assert_nil @paranoid_with_callback.called_after_recover 821 | end 822 | 823 | def test_delete_by_multiple_id_is_paranoid 824 | model_a = ParanoidBelongsDependant.create 825 | model_b = ParanoidBelongsDependant.create 826 | ParanoidBelongsDependant.delete([model_a.id, model_b.id]) 827 | 828 | assert_paranoid_deletion(model_a) 829 | assert_paranoid_deletion(model_b) 830 | end 831 | 832 | def test_destroy_by_multiple_id_is_paranoid 833 | model_a = ParanoidBelongsDependant.create 834 | model_b = ParanoidBelongsDependant.create 835 | ParanoidBelongsDependant.destroy([model_a.id, model_b.id]) 836 | 837 | assert_paranoid_deletion(model_a) 838 | assert_paranoid_deletion(model_b) 839 | end 840 | 841 | def test_delete_by_single_id_is_paranoid 842 | model = ParanoidBelongsDependant.create 843 | ParanoidBelongsDependant.delete(model.id) 844 | 845 | assert_paranoid_deletion(model) 846 | end 847 | 848 | def test_destroy_by_single_id_is_paranoid 849 | model = ParanoidBelongsDependant.create 850 | ParanoidBelongsDependant.destroy(model.id) 851 | 852 | assert_paranoid_deletion(model) 853 | end 854 | 855 | def test_instance_delete_is_paranoid 856 | model = ParanoidBelongsDependant.create 857 | model.delete 858 | 859 | assert_paranoid_deletion(model) 860 | end 861 | 862 | def test_instance_destroy_is_paranoid 863 | model = ParanoidBelongsDependant.create 864 | model.destroy 865 | 866 | assert_paranoid_deletion(model) 867 | end 868 | 869 | # Test string type columns that don't have a nil value when not deleted (Y/N for example) 870 | def test_string_type_with_no_nil_value_before_destroy 871 | ps = ParanoidString.create!(deleted: "not dead") 872 | 873 | assert_equal 1, ParanoidString.where(id: ps).count 874 | end 875 | 876 | def test_string_type_with_no_nil_value_after_destroy 877 | ps = ParanoidString.create!(deleted: "not dead") 878 | ps.destroy 879 | 880 | assert_equal 0, ParanoidString.where(id: ps).count 881 | end 882 | 883 | def test_string_type_with_no_nil_value_before_destroy_with_deleted 884 | ps = ParanoidString.create!(deleted: "not dead") 885 | 886 | assert_equal 1, ParanoidString.with_deleted.where(id: ps).count 887 | end 888 | 889 | def test_string_type_with_no_nil_value_after_destroy_with_deleted 890 | ps = ParanoidString.create!(deleted: "not dead") 891 | ps.destroy 892 | 893 | assert_equal 1, ParanoidString.with_deleted.where(id: ps).count 894 | end 895 | 896 | def test_string_type_with_no_nil_value_before_destroy_only_deleted 897 | ps = ParanoidString.create!(deleted: "not dead") 898 | 899 | assert_equal 0, ParanoidString.only_deleted.where(id: ps).count 900 | end 901 | 902 | def test_string_type_with_no_nil_value_after_destroy_only_deleted 903 | ps = ParanoidString.create!(deleted: "not dead") 904 | ps.destroy 905 | 906 | assert_equal 1, ParanoidString.only_deleted.where(id: ps).count 907 | end 908 | 909 | def test_string_type_with_no_nil_value_after_destroyed_twice 910 | ps = ParanoidString.create!(deleted: "not dead") 911 | 2.times { ps.destroy } 912 | 913 | assert_equal 0, ParanoidString.with_deleted.where(id: ps).count 914 | end 915 | 916 | # Test boolean type columns, that are not nullable 917 | def test_boolean_type_with_no_nil_value_before_destroy 918 | ps = ParanoidBooleanNotNullable.create! 919 | 920 | assert_equal 1, ParanoidBooleanNotNullable.where(id: ps).count 921 | end 922 | 923 | def test_boolean_type_with_no_nil_value_after_destroy 924 | ps = ParanoidBooleanNotNullable.create! 925 | ps.destroy 926 | 927 | assert_equal 0, ParanoidBooleanNotNullable.where(id: ps).count 928 | end 929 | 930 | def test_boolean_type_with_no_nil_value_before_destroy_with_deleted 931 | ps = ParanoidBooleanNotNullable.create! 932 | 933 | assert_equal 1, ParanoidBooleanNotNullable.with_deleted.where(id: ps).count 934 | end 935 | 936 | def test_boolean_type_with_no_nil_value_after_destroy_with_deleted 937 | ps = ParanoidBooleanNotNullable.create! 938 | ps.destroy 939 | 940 | assert_equal 1, ParanoidBooleanNotNullable.with_deleted.where(id: ps).count 941 | end 942 | 943 | def test_boolean_type_with_no_nil_value_before_destroy_only_deleted 944 | ps = ParanoidBooleanNotNullable.create! 945 | 946 | assert_equal 0, ParanoidBooleanNotNullable.only_deleted.where(id: ps).count 947 | end 948 | 949 | def test_boolean_type_with_no_nil_value_after_destroy_only_deleted 950 | ps = ParanoidBooleanNotNullable.create! 951 | ps.destroy 952 | 953 | assert_equal 1, ParanoidBooleanNotNullable.only_deleted.where(id: ps).count 954 | end 955 | 956 | def test_boolean_type_with_no_nil_value_after_destroyed_twice 957 | ps = ParanoidBooleanNotNullable.create! 958 | 2.times { ps.destroy } 959 | 960 | assert_equal 0, ParanoidBooleanNotNullable.with_deleted.where(id: ps).count 961 | end 962 | 963 | def test_boolean_type_with_no_nil_value_after_recover 964 | ps = ParanoidBooleanNotNullable.create! 965 | ps.destroy 966 | 967 | assert_equal 1, ParanoidBooleanNotNullable.only_deleted.where(id: ps).count 968 | 969 | ps.recover 970 | 971 | assert_equal 1, ParanoidBooleanNotNullable.where(id: ps).count 972 | end 973 | 974 | def test_boolean_type_with_no_nil_value_after_recover! 975 | ps = ParanoidBooleanNotNullable.create! 976 | ps.destroy 977 | 978 | assert_equal 1, ParanoidBooleanNotNullable.only_deleted.where(id: ps).count 979 | 980 | ps.recover! 981 | 982 | assert_equal 1, ParanoidBooleanNotNullable.where(id: ps).count 983 | end 984 | 985 | def test_no_double_tap_destroys_fully 986 | ps = ParanoidNoDoubleTapDestroysFully.create! 987 | 2.times { ps.destroy } 988 | 989 | assert_equal 1, ParanoidNoDoubleTapDestroysFully.with_deleted.where(id: ps).count 990 | end 991 | 992 | def test_decrement_counters_without_touch 993 | paranoid_boolean = ParanoidBoolean.create! 994 | paranoid_with_counter_cache = ParanoidWithCounterCache 995 | .create!(paranoid_boolean: paranoid_boolean) 996 | 997 | assert_equal 1, paranoid_boolean.paranoid_with_counter_caches_count 998 | updated_at = paranoid_boolean.reload.updated_at 999 | 1000 | paranoid_with_counter_cache.destroy 1001 | 1002 | assert_equal 0, paranoid_boolean.reload.paranoid_with_counter_caches_count 1003 | assert_equal updated_at, paranoid_boolean.reload.updated_at 1004 | end 1005 | 1006 | def test_decrement_custom_counters 1007 | paranoid_boolean = ParanoidBoolean.create! 1008 | paranoid_with_custom_counter_cache = ParanoidWithCustomCounterCache 1009 | .create!(paranoid_boolean: paranoid_boolean) 1010 | 1011 | assert_equal 1, paranoid_boolean.custom_counter_cache 1012 | 1013 | paranoid_with_custom_counter_cache.destroy 1014 | 1015 | assert_equal 0, paranoid_boolean.reload.custom_counter_cache 1016 | end 1017 | 1018 | def test_decrement_counters_with_touch 1019 | paranoid_boolean = ParanoidBoolean.create! 1020 | paranoid_with_counter_cache = ParanoidWithTouchAndCounterCache 1021 | .create!(paranoid_boolean: paranoid_boolean) 1022 | 1023 | assert_equal 1, paranoid_boolean.paranoid_with_touch_and_counter_caches_count 1024 | updated_at = paranoid_boolean.reload.updated_at 1025 | 1026 | paranoid_with_counter_cache.destroy 1027 | 1028 | assert_equal 0, paranoid_boolean.reload.paranoid_with_touch_and_counter_caches_count 1029 | assert_not_equal updated_at, paranoid_boolean.reload.updated_at 1030 | end 1031 | 1032 | def test_touch_belongs_to 1033 | paranoid_boolean = ParanoidBoolean.create! 1034 | paranoid_with_counter_cache = ParanoidWithTouch 1035 | .create!(paranoid_boolean: paranoid_boolean) 1036 | 1037 | updated_at = paranoid_boolean.reload.updated_at 1038 | 1039 | paranoid_with_counter_cache.destroy 1040 | 1041 | assert_not_equal updated_at, paranoid_boolean.reload.updated_at 1042 | end 1043 | 1044 | def test_destroy_with_optional_belongs_to_and_counter_cache 1045 | ps = ParanoidWithCounterCacheOnOptionalBelognsTo.create! 1046 | ps.destroy 1047 | 1048 | assert_equal 1, ParanoidWithCounterCacheOnOptionalBelognsTo.only_deleted 1049 | .where(id: ps).count 1050 | end 1051 | 1052 | def test_hard_destroy_decrement_counters 1053 | paranoid_boolean = ParanoidBoolean.create! 1054 | paranoid_with_counter_cache = ParanoidWithCounterCache 1055 | .create!(paranoid_boolean: paranoid_boolean) 1056 | 1057 | assert_equal 1, paranoid_boolean.paranoid_with_counter_caches_count 1058 | 1059 | paranoid_with_counter_cache.destroy_fully! 1060 | 1061 | assert_equal 0, paranoid_boolean.reload.paranoid_with_counter_caches_count 1062 | end 1063 | 1064 | def test_hard_destroy_decrement_custom_counters 1065 | paranoid_boolean = ParanoidBoolean.create! 1066 | paranoid_with_custom_counter_cache = ParanoidWithCustomCounterCache 1067 | .create!(paranoid_boolean: paranoid_boolean) 1068 | 1069 | assert_equal 1, paranoid_boolean.custom_counter_cache 1070 | 1071 | paranoid_with_custom_counter_cache.destroy_fully! 1072 | 1073 | assert_equal 0, paranoid_boolean.reload.custom_counter_cache 1074 | end 1075 | 1076 | def test_increment_counters 1077 | paranoid_boolean = ParanoidBoolean.create! 1078 | paranoid_with_counter_cache = ParanoidWithCounterCache 1079 | .create!(paranoid_boolean: paranoid_boolean) 1080 | 1081 | assert_equal 1, paranoid_boolean.paranoid_with_counter_caches_count 1082 | 1083 | paranoid_with_counter_cache.destroy 1084 | 1085 | assert_equal 0, paranoid_boolean.reload.paranoid_with_counter_caches_count 1086 | 1087 | paranoid_with_counter_cache.recover 1088 | 1089 | assert_equal 1, paranoid_boolean.reload.paranoid_with_counter_caches_count 1090 | end 1091 | 1092 | def test_increment_custom_counters 1093 | paranoid_boolean = ParanoidBoolean.create! 1094 | paranoid_with_custom_counter_cache = ParanoidWithCustomCounterCache 1095 | .create!(paranoid_boolean: paranoid_boolean) 1096 | 1097 | assert_equal 1, paranoid_boolean.custom_counter_cache 1098 | 1099 | paranoid_with_custom_counter_cache.destroy 1100 | 1101 | assert_equal 0, paranoid_boolean.reload.custom_counter_cache 1102 | 1103 | paranoid_with_custom_counter_cache.recover 1104 | 1105 | assert_equal 1, paranoid_boolean.reload.custom_counter_cache 1106 | end 1107 | 1108 | def test_explicitly_setting_table_name_after_acts_as_paranoid_macro 1109 | assert_equal "explicit_table.deleted_at", ParanoidWithExplicitTableNameAfterMacro 1110 | .paranoid_column_reference 1111 | end 1112 | 1113 | def test_deleted_after_time 1114 | ParanoidTime.first.destroy 1115 | 1116 | assert_equal 0, ParanoidTime.deleted_after_time(1.hour.from_now).count 1117 | assert_equal 1, ParanoidTime.deleted_after_time(1.hour.ago).count 1118 | end 1119 | 1120 | def test_deleted_before_time 1121 | ParanoidTime.first.destroy 1122 | 1123 | assert_equal 1, ParanoidTime.deleted_before_time(1.hour.from_now).count 1124 | assert_equal 0, ParanoidTime.deleted_before_time(1.hour.ago).count 1125 | end 1126 | 1127 | def test_deleted_inside_time_window 1128 | ParanoidTime.first.destroy 1129 | 1130 | assert_equal 1, ParanoidTime.deleted_inside_time_window(1.minute.ago, 2.minutes).count 1131 | assert_equal 1, 1132 | ParanoidTime.deleted_inside_time_window(1.minute.from_now, 2.minutes).count 1133 | assert_equal 0, ParanoidTime.deleted_inside_time_window(3.minutes.ago, 1.minute).count 1134 | assert_equal 0, 1135 | ParanoidTime.deleted_inside_time_window(3.minutes.from_now, 1.minute).count 1136 | end 1137 | end 1138 | -------------------------------------------------------------------------------- /test/legacy/default_scopes_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class MultipleDefaultScopesTest < ActiveSupport::TestCase 6 | class ParanoidPolygon < ActiveRecord::Base 7 | acts_as_paranoid 8 | default_scope { where("sides = ?", 3) } 9 | end 10 | 11 | def setup 12 | ActiveRecord::Schema.define(version: 1) do 13 | create_table :paranoid_polygons do |t| 14 | t.integer :sides 15 | t.datetime :deleted_at 16 | 17 | timestamps t 18 | end 19 | end 20 | 21 | ParanoidPolygon.create! sides: 3 22 | ParanoidPolygon.create! sides: 3 23 | ParanoidPolygon.create! sides: 3 24 | ParanoidPolygon.create! sides: 8 25 | 26 | assert_equal 3, ParanoidPolygon.count 27 | assert_equal 4, ParanoidPolygon.unscoped.count 28 | end 29 | 30 | def teardown 31 | teardown_db 32 | end 33 | 34 | def test_only_deleted_with_deleted_with_multiple_default_scope 35 | 3.times { ParanoidPolygon.create! sides: 3 } 36 | ParanoidPolygon.create! sides: 8 37 | ParanoidPolygon.first.destroy 38 | 39 | assert_equal 1, ParanoidPolygon.only_deleted.count 40 | assert_equal 1, ParanoidPolygon.only_deleted.with_deleted.count 41 | end 42 | 43 | def test_with_deleted_only_deleted_with_multiple_default_scope 44 | 3.times { ParanoidPolygon.create! sides: 3 } 45 | ParanoidPolygon.create! sides: 8 46 | ParanoidPolygon.first.destroy 47 | 48 | assert_equal 1, ParanoidPolygon.only_deleted.count 49 | assert_equal 1, ParanoidPolygon.with_deleted.only_deleted.count 50 | end 51 | 52 | def test_fake_removal_with_multiple_default_scope 53 | ParanoidPolygon.first.destroy 54 | 55 | assert_equal 2, ParanoidPolygon.count 56 | assert_equal 3, ParanoidPolygon.with_deleted.count 57 | assert_equal 1, ParanoidPolygon.only_deleted.count 58 | assert_equal 4, ParanoidPolygon.unscoped.count 59 | 60 | ParanoidPolygon.destroy_all 61 | 62 | assert_equal 0, ParanoidPolygon.count 63 | assert_equal 3, ParanoidPolygon.with_deleted.count 64 | assert_equal 3, ParanoidPolygon.with_deleted.count 65 | assert_equal 4, ParanoidPolygon.unscoped.count 66 | end 67 | 68 | def test_real_removal_with_multiple_default_scope 69 | # two-step 70 | ParanoidPolygon.first.destroy 71 | ParanoidPolygon.only_deleted.first.destroy 72 | 73 | assert_equal 2, ParanoidPolygon.count 74 | assert_equal 2, ParanoidPolygon.with_deleted.count 75 | assert_equal 0, ParanoidPolygon.only_deleted.count 76 | assert_equal 3, ParanoidPolygon.unscoped.count 77 | 78 | ParanoidPolygon.first.destroy_fully! 79 | 80 | assert_equal 1, ParanoidPolygon.count 81 | assert_equal 1, ParanoidPolygon.with_deleted.count 82 | assert_equal 0, ParanoidPolygon.only_deleted.count 83 | assert_equal 2, ParanoidPolygon.unscoped.count 84 | 85 | ParanoidPolygon.delete_all! 86 | 87 | assert_equal 0, ParanoidPolygon.count 88 | assert_equal 0, ParanoidPolygon.with_deleted.count 89 | assert_equal 0, ParanoidPolygon.only_deleted.count 90 | assert_equal 1, ParanoidPolygon.unscoped.count 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/legacy/dependent_recovery_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class DependentRecoveryTest < ActiveSupport::TestCase 6 | class ParanoidForest < ActiveRecord::Base 7 | acts_as_paranoid 8 | has_many :paranoid_trees, dependent: :destroy 9 | end 10 | 11 | class ParanoidTree < ActiveRecord::Base 12 | acts_as_paranoid 13 | belongs_to :paranoid_forest, optional: false 14 | end 15 | 16 | def setup 17 | ActiveRecord::Schema.define(version: 1) do 18 | create_table :paranoid_forests do |t| 19 | t.string :name 20 | t.boolean :rainforest 21 | t.datetime :deleted_at 22 | 23 | timestamps t 24 | end 25 | 26 | create_table :paranoid_trees do |t| 27 | t.integer :paranoid_forest_id 28 | t.string :name 29 | t.datetime :deleted_at 30 | 31 | timestamps t 32 | end 33 | end 34 | end 35 | 36 | def teardown 37 | teardown_db 38 | end 39 | 40 | def test_recover_dependent_records_with_required_belongs_to 41 | forest = ParanoidForest.create! name: "forest" 42 | 43 | tree = ParanoidTree.new name: "tree" 44 | 45 | refute_predicate tree, :valid? 46 | tree.paranoid_forest = forest 47 | 48 | assert_predicate tree, :valid? 49 | tree.save! 50 | 51 | forest.destroy 52 | forest.recover 53 | 54 | assert_equal 1, ParanoidTree.count 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/legacy/inheritance_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class InheritanceTest < ActiveSupport::TestCase 6 | class SuperParanoid < ActiveRecord::Base 7 | acts_as_paranoid 8 | belongs_to :has_many_inherited_super_paranoidz 9 | end 10 | 11 | class HasManyInheritedSuperParanoidz < ActiveRecord::Base 12 | has_many :super_paranoidz, class_name: "InheritedParanoid", dependent: :destroy 13 | end 14 | 15 | class InheritedParanoid < SuperParanoid 16 | acts_as_paranoid 17 | end 18 | 19 | def setup 20 | ActiveRecord::Schema.define(version: 1) do 21 | create_table :super_paranoids do |t| 22 | t.string :type 23 | t.references :has_many_inherited_super_paranoidz, 24 | index: { name: "index__sp_id_on_has_many_isp" } 25 | t.datetime :deleted_at 26 | 27 | timestamps t 28 | end 29 | 30 | create_table :has_many_inherited_super_paranoidzs do |t| 31 | t.references :super_paranoidz, index: { name: "index_has_many_isp_on_sp_id" } 32 | t.datetime :deleted_at 33 | 34 | timestamps t 35 | end 36 | end 37 | end 38 | 39 | def teardown 40 | teardown_db 41 | end 42 | 43 | def test_destroy_dependents_with_inheritance 44 | has_many_inherited_super_paranoidz = HasManyInheritedSuperParanoidz.new 45 | has_many_inherited_super_paranoidz.save 46 | has_many_inherited_super_paranoidz.super_paranoidz.create 47 | assert_nothing_raised { has_many_inherited_super_paranoidz.destroy } 48 | end 49 | 50 | def test_class_instance_variables_are_inherited 51 | assert_nothing_raised { InheritedParanoid.paranoid_column } 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/legacy/relations_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class RelationsTest < ActiveSupport::TestCase 6 | class ParanoidForest < ActiveRecord::Base 7 | acts_as_paranoid 8 | 9 | scope :rainforest, -> { where(rainforest: true) } 10 | 11 | has_many :paranoid_trees, dependent: :destroy 12 | end 13 | 14 | class ParanoidTree < ActiveRecord::Base 15 | acts_as_paranoid 16 | belongs_to :paranoid_forest 17 | validates_presence_of :name 18 | end 19 | 20 | class NotParanoidBowl < ActiveRecord::Base 21 | has_many :paranoid_chocolates, dependent: :destroy 22 | end 23 | 24 | class ParanoidChocolate < ActiveRecord::Base 25 | acts_as_paranoid 26 | belongs_to :not_paranoid_bowl 27 | validates_presence_of :name 28 | end 29 | 30 | def setup 31 | ActiveRecord::Schema.define(version: 1) do 32 | create_table :paranoid_forests do |t| 33 | t.string :name 34 | t.boolean :rainforest 35 | t.datetime :deleted_at 36 | 37 | timestamps t 38 | end 39 | 40 | create_table :paranoid_trees do |t| 41 | t.integer :paranoid_forest_id 42 | t.string :name 43 | t.datetime :deleted_at 44 | 45 | timestamps t 46 | end 47 | 48 | create_table :not_paranoid_bowls do |t| 49 | t.string :name 50 | 51 | timestamps t 52 | end 53 | 54 | create_table :paranoid_chocolates do |t| 55 | t.integer :not_paranoid_bowl_id 56 | t.string :name 57 | t.datetime :deleted_at 58 | 59 | timestamps t 60 | end 61 | end 62 | 63 | @paranoid_forest_1 = ParanoidForest.create! name: "ParanoidForest #1" 64 | @paranoid_forest_2 = ParanoidForest.create! name: "ParanoidForest #2", rainforest: true 65 | @paranoid_forest_3 = ParanoidForest.create! name: "ParanoidForest #3", rainforest: true 66 | 67 | assert_equal 3, ParanoidForest.count 68 | assert_equal 2, ParanoidForest.rainforest.count 69 | 70 | @paranoid_forest_1.paranoid_trees.create! name: "ParanoidTree #1" 71 | @paranoid_forest_1.paranoid_trees.create! name: "ParanoidTree #2" 72 | @paranoid_forest_2.paranoid_trees.create! name: "ParanoidTree #3" 73 | @paranoid_forest_2.paranoid_trees.create! name: "ParanoidTree #4" 74 | 75 | assert_equal 4, ParanoidTree.count 76 | end 77 | 78 | def teardown 79 | teardown_db 80 | end 81 | 82 | def test_filtering_with_scopes 83 | assert_equal 2, ParanoidForest.rainforest.with_deleted.count 84 | assert_equal 2, ParanoidForest.with_deleted.rainforest.count 85 | 86 | assert_equal 0, ParanoidForest.rainforest.only_deleted.count 87 | assert_equal 0, ParanoidForest.only_deleted.rainforest.count 88 | 89 | ParanoidForest.rainforest.first.destroy 90 | 91 | assert_equal 1, ParanoidForest.rainforest.count 92 | 93 | assert_equal 2, ParanoidForest.rainforest.with_deleted.count 94 | assert_equal 2, ParanoidForest.with_deleted.rainforest.count 95 | 96 | assert_equal 1, ParanoidForest.rainforest.only_deleted.count 97 | assert_equal 1, ParanoidForest.only_deleted.rainforest.count 98 | end 99 | 100 | def test_associations_filtered_by_with_deleted 101 | assert_equal 2, @paranoid_forest_1.paranoid_trees.with_deleted.count 102 | assert_equal 2, @paranoid_forest_2.paranoid_trees.with_deleted.count 103 | 104 | @paranoid_forest_1.paranoid_trees.first.destroy 105 | 106 | assert_equal 1, @paranoid_forest_1.paranoid_trees.count 107 | assert_equal 2, @paranoid_forest_1.paranoid_trees.with_deleted.count 108 | assert_equal 4, ParanoidTree.with_deleted.count 109 | 110 | @paranoid_forest_2.paranoid_trees.first.destroy 111 | 112 | assert_equal 1, @paranoid_forest_2.paranoid_trees.count 113 | assert_equal 2, @paranoid_forest_2.paranoid_trees.with_deleted.count 114 | assert_equal 4, ParanoidTree.with_deleted.count 115 | 116 | @paranoid_forest_1.paranoid_trees.first.destroy 117 | 118 | assert_equal 0, @paranoid_forest_1.paranoid_trees.count 119 | assert_equal 2, @paranoid_forest_1.paranoid_trees.with_deleted.count 120 | assert_equal 4, ParanoidTree.with_deleted.count 121 | end 122 | 123 | def test_associations_filtered_by_only_deleted 124 | assert_equal 0, @paranoid_forest_1.paranoid_trees.only_deleted.count 125 | assert_equal 0, @paranoid_forest_2.paranoid_trees.only_deleted.count 126 | 127 | @paranoid_forest_1.paranoid_trees.first.destroy 128 | 129 | assert_equal 1, @paranoid_forest_1.paranoid_trees.only_deleted.count 130 | assert_equal 1, ParanoidTree.only_deleted.count 131 | 132 | @paranoid_forest_2.paranoid_trees.first.destroy 133 | 134 | assert_equal 1, @paranoid_forest_2.paranoid_trees.only_deleted.count 135 | assert_equal 2, ParanoidTree.only_deleted.count 136 | 137 | @paranoid_forest_1.paranoid_trees.first.destroy 138 | 139 | assert_equal 2, @paranoid_forest_1.paranoid_trees.only_deleted.count 140 | assert_equal 3, ParanoidTree.only_deleted.count 141 | end 142 | 143 | def test_fake_removal_through_relation 144 | # destroy: through a relation. 145 | ParanoidForest.rainforest.destroy(@paranoid_forest_3.id) 146 | 147 | assert_equal 1, ParanoidForest.rainforest.count 148 | assert_equal 2, ParanoidForest.rainforest.with_deleted.count 149 | assert_equal 1, ParanoidForest.rainforest.only_deleted.count 150 | 151 | # destroy_all: through a relation 152 | @paranoid_forest_2.paranoid_trees.destroy_all 153 | 154 | assert_equal 0, @paranoid_forest_2.paranoid_trees.count 155 | assert_equal 2, @paranoid_forest_2.paranoid_trees.with_deleted.count 156 | end 157 | 158 | def test_fake_removal_through_has_many_relation_of_non_paranoid_model 159 | not_paranoid = NotParanoidBowl.create! name: "NotParanoid #1" 160 | not_paranoid.paranoid_chocolates.create! name: "ParanoidChocolate #1" 161 | not_paranoid.paranoid_chocolates.create! name: "ParanoidChocolate #2" 162 | 163 | not_paranoid.paranoid_chocolates.destroy_all 164 | 165 | assert_equal 0, not_paranoid.paranoid_chocolates.count 166 | assert_equal 2, not_paranoid.paranoid_chocolates.with_deleted.count 167 | end 168 | 169 | def test_real_removal_through_relation_with_destroy_fully 170 | ParanoidForest.rainforest.destroy_fully!(@paranoid_forest_3) 171 | 172 | assert_equal 1, ParanoidForest.rainforest.count 173 | assert_equal 1, ParanoidForest.rainforest.with_deleted.count 174 | assert_equal 0, ParanoidForest.rainforest.only_deleted.count 175 | end 176 | 177 | def test_two_step_real_removal_through_relation_with_destroy 178 | # destroy: two-step through a relation 179 | paranoid_tree = @paranoid_forest_1.paranoid_trees.first 180 | @paranoid_forest_1.paranoid_trees.destroy(paranoid_tree.id) 181 | @paranoid_forest_1.paranoid_trees.only_deleted.destroy(paranoid_tree.id) 182 | 183 | assert_equal 1, @paranoid_forest_1.paranoid_trees.count 184 | assert_equal 1, @paranoid_forest_1.paranoid_trees.with_deleted.count 185 | assert_equal 0, @paranoid_forest_1.paranoid_trees.only_deleted.count 186 | end 187 | 188 | def test_two_step_real_removal_through_relation_with_destroy_all 189 | # destroy_all: two-step through a relation 190 | @paranoid_forest_1.paranoid_trees.destroy_all 191 | @paranoid_forest_1.paranoid_trees.only_deleted.destroy_all 192 | 193 | assert_equal 0, @paranoid_forest_1.paranoid_trees.count 194 | assert_equal 0, @paranoid_forest_1.paranoid_trees.with_deleted.count 195 | assert_equal 0, @paranoid_forest_1.paranoid_trees.only_deleted.count 196 | end 197 | 198 | def test_real_removal_through_relation_with_delete_all_bang 199 | # delete_all!: through a relation 200 | @paranoid_forest_2.paranoid_trees.delete_all! 201 | 202 | assert_equal 0, @paranoid_forest_2.paranoid_trees.count 203 | assert_equal 0, @paranoid_forest_2.paranoid_trees.with_deleted.count 204 | assert_equal 0, @paranoid_forest_2.paranoid_trees.only_deleted.count 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /test/legacy/table_namespace_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TableNamespaceTest < ActiveSupport::TestCase 6 | module Paranoid 7 | class Blob < ActiveRecord::Base 8 | acts_as_paranoid 9 | 10 | validates_presence_of :name 11 | 12 | def self.table_name_prefix 13 | "paranoid_" 14 | end 15 | end 16 | end 17 | 18 | def setup 19 | ActiveRecord::Schema.define(version: 1) do 20 | create_table :paranoid_blobs do |t| 21 | t.string :name 22 | t.datetime :deleted_at 23 | 24 | timestamps t 25 | end 26 | end 27 | end 28 | 29 | def teardown 30 | teardown_db 31 | end 32 | 33 | def test_correct_table_name 34 | assert_equal "paranoid_blobs", Paranoid::Blob.table_name 35 | 36 | b = Paranoid::Blob.new(name: "hello!") 37 | b.save! 38 | 39 | assert_equal b, Paranoid::Blob.first 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/legacy/validations_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ValidatesUniquenessTest < ActiveSupport::TestCase 6 | class ParanoidUniqueness < ActiveRecord::Base 7 | acts_as_paranoid 8 | 9 | validates_uniqueness_of :name 10 | end 11 | 12 | class ParanoidUniquenessWithoutDeleted < ActiveRecord::Base 13 | acts_as_paranoid 14 | validates_as_paranoid 15 | 16 | validates_uniqueness_of_without_deleted :name 17 | end 18 | 19 | class ParanoidWithSerializedColumn < ActiveRecord::Base 20 | acts_as_paranoid 21 | validates_as_paranoid 22 | 23 | if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("7.2") 24 | serialize :colors, Array 25 | else 26 | serialize :colors 27 | end 28 | 29 | validates_uniqueness_of_without_deleted :colors 30 | end 31 | 32 | class ParanoidWithScopedValidation < ActiveRecord::Base 33 | acts_as_paranoid 34 | validates_uniqueness_of :name, scope: :category 35 | end 36 | 37 | def setup 38 | ActiveRecord::Schema.define(version: 1) do 39 | create_table :paranoid_uniquenesses do |t| 40 | t.string :name 41 | t.datetime :deleted_at 42 | 43 | timestamps t 44 | end 45 | 46 | create_table :paranoid_uniqueness_without_deleteds do |t| 47 | t.string :name 48 | t.datetime :deleted_at 49 | 50 | timestamps t 51 | end 52 | 53 | create_table :paranoid_with_serialized_columns do |t| 54 | t.string :name 55 | t.datetime :deleted_at 56 | t.string :colors 57 | 58 | timestamps t 59 | end 60 | 61 | create_table :paranoid_with_scoped_validations do |t| 62 | t.string :name 63 | t.string :category 64 | t.datetime :deleted_at 65 | timestamps t 66 | end 67 | end 68 | end 69 | 70 | def teardown 71 | teardown_db 72 | end 73 | 74 | def test_should_include_deleted_by_default 75 | ParanoidUniqueness.create!(name: "paranoid") 76 | ParanoidUniqueness.new(name: "paranoid").tap do |record| 77 | refute_predicate record, :valid? 78 | ParanoidUniqueness.first.destroy 79 | 80 | refute_predicate record, :valid? 81 | ParanoidUniqueness.only_deleted.first.destroy! 82 | 83 | assert_predicate record, :valid? 84 | end 85 | end 86 | 87 | def test_should_validate_without_deleted 88 | ParanoidUniquenessWithoutDeleted.create!(name: "paranoid") 89 | ParanoidUniquenessWithoutDeleted.new(name: "paranoid").tap do |record| 90 | refute_predicate record, :valid? 91 | ParanoidUniquenessWithoutDeleted.first.destroy 92 | 93 | assert_predicate record, :valid? 94 | ParanoidUniquenessWithoutDeleted.only_deleted.first.destroy! 95 | 96 | assert_predicate record, :valid? 97 | end 98 | end 99 | 100 | def test_validate_serialized_attribute_without_deleted 101 | ParanoidWithSerializedColumn.create!(name: "ParanoidWithSerializedColumn #1", 102 | colors: %w[Cyan Maroon]) 103 | record = ParanoidWithSerializedColumn.new(name: "ParanoidWithSerializedColumn #2") 104 | record.colors = %w[Cyan Maroon] 105 | 106 | refute_predicate record, :valid? 107 | 108 | record.colors = %w[Beige Turquoise] 109 | 110 | assert_predicate record, :valid? 111 | end 112 | 113 | def test_updated_serialized_attribute_validated_without_deleted 114 | record = ParanoidWithSerializedColumn.create!(name: "ParanoidWithSerializedColumn #1", 115 | colors: %w[Cyan Maroon]) 116 | record.update!(colors: %w[Beige Turquoise]) 117 | 118 | assert_predicate record, :valid? 119 | end 120 | 121 | def test_models_with_scoped_validations_can_be_multiply_deleted 122 | model_a = ParanoidWithScopedValidation.create(name: "Model A", category: "Category A") 123 | model_b = ParanoidWithScopedValidation.create(name: "Model B", category: "Category B") 124 | 125 | ParanoidWithScopedValidation.delete([model_a.id, model_b.id]) 126 | 127 | assert_paranoid_deletion(model_a) 128 | assert_paranoid_deletion(model_b) 129 | end 130 | 131 | def test_models_with_scoped_validations_can_be_multiply_destroyed 132 | model_a = ParanoidWithScopedValidation.create(name: "Model A", category: "Category A") 133 | model_b = ParanoidWithScopedValidation.create(name: "Model B", category: "Category B") 134 | 135 | ParanoidWithScopedValidation.destroy([model_a.id, model_b.id]) 136 | 137 | assert_paranoid_deletion(model_a) 138 | assert_paranoid_deletion(model_b) 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler" 4 | begin 5 | Bundler.load 6 | rescue Bundler::BundlerError => e 7 | warn e.message 8 | warn "Run `bundle install` to install missing gems" 9 | exit e.status_code 10 | end 11 | 12 | if RUBY_ENGINE == "jruby" 13 | # Workaround for issue in I18n/JRuby combo. 14 | # See https://github.com/jruby/jruby/issues/6547 and 15 | # https://github.com/ruby-i18n/i18n/issues/555 16 | require "i18n/backend" 17 | require "i18n/backend/simple" 18 | end 19 | 20 | require "simplecov" 21 | SimpleCov.start do 22 | enable_coverage :branch 23 | end 24 | 25 | # Load logger early to avoid errors when loading activesupport 26 | # See https://github.com/rails/rails/issues/54260 27 | # FIXME: Remove once we drop support for Rails 7.0 28 | require "logger" 29 | 30 | require "acts_as_paranoid" 31 | require "minitest/autorun" 32 | require "minitest/focus" 33 | 34 | # Silence deprecation halfway through the test 35 | I18n.enforce_available_locales = true 36 | 37 | ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") 38 | ActiveRecord::Schema.verbose = false 39 | 40 | log_dir = File.expand_path("../log/", __dir__) 41 | FileUtils.mkdir_p log_dir 42 | file_path = File.join(log_dir, "test.log") 43 | ActiveRecord::Base.logger = Logger.new(file_path) 44 | 45 | def timestamps(table) 46 | table.column :created_at, :timestamp, null: false 47 | table.column :updated_at, :timestamp, null: false 48 | end 49 | 50 | module ParanoidTestHelpers 51 | def assert_paranoid_deletion(model) 52 | row = find_row(model) 53 | 54 | assert_not_nil row, "#{model.class} entirely deleted" 55 | assert_not_nil row["deleted_at"], "Deleted at not set" 56 | end 57 | 58 | def assert_non_paranoid_deletion(model) 59 | row = find_row(model) 60 | 61 | assert_nil row, "#{model.class} still exists" 62 | end 63 | 64 | def find_row(model) 65 | sql = "select deleted_at from #{model.class.table_name} where id = #{model.id}" 66 | # puts sql here if you want to debug 67 | model.class.connection.select_one(sql) 68 | end 69 | 70 | def teardown_db 71 | ActiveRecord::Base.connection.data_sources.each do |table| 72 | ActiveRecord::Base.connection.drop_table(table) 73 | end 74 | end 75 | end 76 | 77 | ActiveSupport::TestCase.include ParanoidTestHelpers 78 | --------------------------------------------------------------------------------